Path: blob/main/src/vs/editor/common/viewLayout/viewLineRenderer.ts
3294 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../../nls.js';6import { CharCode } from '../../../base/common/charCode.js';7import * as strings from '../../../base/common/strings.js';8import { IViewLineTokens } from '../tokens/lineTokens.js';9import { StringBuilder } from '../core/stringBuilder.js';10import { LineDecoration, LineDecorationsNormalizer } from './lineDecorations.js';11import { LinePart, LinePartMetadata } from './linePart.js';12import { OffsetRange } from '../core/ranges/offsetRange.js';13import { InlineDecorationType } from '../viewModel/inlineDecorations.js';14import { TextDirection } from '../model.js';1516export const enum RenderWhitespace {17None = 0,18Boundary = 1,19Selection = 2,20Trailing = 3,21All = 422}2324export class RenderLineInput {2526public readonly useMonospaceOptimizations: boolean;27public readonly canUseHalfwidthRightwardsArrow: boolean;28public readonly lineContent: string;29public readonly continuesWithWrappedLine: boolean;30public readonly isBasicASCII: boolean;31public readonly containsRTL: boolean;32public readonly fauxIndentLength: number;33public readonly lineTokens: IViewLineTokens;34public readonly lineDecorations: LineDecoration[];35public readonly tabSize: number;36public readonly startVisibleColumn: number;37public readonly spaceWidth: number;38public readonly renderSpaceWidth: number;39public readonly renderSpaceCharCode: number;40public readonly stopRenderingLineAfter: number;41public readonly renderWhitespace: RenderWhitespace;42public readonly renderControlCharacters: boolean;43public readonly fontLigatures: boolean;44public readonly textDirection: TextDirection | null;45public readonly verticalScrollbarSize: number;4647/**48* Defined only when renderWhitespace is 'selection'. Selections are non-overlapping,49* and ordered by position within the line.50*/51public readonly selectionsOnLine: OffsetRange[] | null;52/**53* When rendering an empty line, whether to render a new line instead54*/55public readonly renderNewLineWhenEmpty: boolean;5657public get isLTR(): boolean {58return !this.containsRTL && this.textDirection !== TextDirection.RTL;59}6061constructor(62useMonospaceOptimizations: boolean,63canUseHalfwidthRightwardsArrow: boolean,64lineContent: string,65continuesWithWrappedLine: boolean,66isBasicASCII: boolean,67containsRTL: boolean,68fauxIndentLength: number,69lineTokens: IViewLineTokens,70lineDecorations: LineDecoration[],71tabSize: number,72startVisibleColumn: number,73spaceWidth: number,74middotWidth: number,75wsmiddotWidth: number,76stopRenderingLineAfter: number,77renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all',78renderControlCharacters: boolean,79fontLigatures: boolean,80selectionsOnLine: OffsetRange[] | null,81textDirection: TextDirection | null,82verticalScrollbarSize: number,83renderNewLineWhenEmpty: boolean = false,84) {85this.useMonospaceOptimizations = useMonospaceOptimizations;86this.canUseHalfwidthRightwardsArrow = canUseHalfwidthRightwardsArrow;87this.lineContent = lineContent;88this.continuesWithWrappedLine = continuesWithWrappedLine;89this.isBasicASCII = isBasicASCII;90this.containsRTL = containsRTL;91this.fauxIndentLength = fauxIndentLength;92this.lineTokens = lineTokens;93this.lineDecorations = lineDecorations.sort(LineDecoration.compare);94this.tabSize = tabSize;95this.startVisibleColumn = startVisibleColumn;96this.spaceWidth = spaceWidth;97this.stopRenderingLineAfter = stopRenderingLineAfter;98this.renderWhitespace = (99renderWhitespace === 'all'100? RenderWhitespace.All101: renderWhitespace === 'boundary'102? RenderWhitespace.Boundary103: renderWhitespace === 'selection'104? RenderWhitespace.Selection105: renderWhitespace === 'trailing'106? RenderWhitespace.Trailing107: RenderWhitespace.None108);109this.renderControlCharacters = renderControlCharacters;110this.fontLigatures = fontLigatures;111this.selectionsOnLine = selectionsOnLine && selectionsOnLine.sort((a, b) => a.start < b.start ? -1 : 1);112this.renderNewLineWhenEmpty = renderNewLineWhenEmpty;113this.textDirection = textDirection;114this.verticalScrollbarSize = verticalScrollbarSize;115116const wsmiddotDiff = Math.abs(wsmiddotWidth - spaceWidth);117const middotDiff = Math.abs(middotWidth - spaceWidth);118if (wsmiddotDiff < middotDiff) {119this.renderSpaceWidth = wsmiddotWidth;120this.renderSpaceCharCode = 0x2E31; // U+2E31 - WORD SEPARATOR MIDDLE DOT121} else {122this.renderSpaceWidth = middotWidth;123this.renderSpaceCharCode = 0xB7; // U+00B7 - MIDDLE DOT124}125}126127private sameSelection(otherSelections: OffsetRange[] | null): boolean {128if (this.selectionsOnLine === null) {129return otherSelections === null;130}131132if (otherSelections === null) {133return false;134}135136if (otherSelections.length !== this.selectionsOnLine.length) {137return false;138}139140for (let i = 0; i < this.selectionsOnLine.length; i++) {141if (!this.selectionsOnLine[i].equals(otherSelections[i])) {142return false;143}144}145146return true;147}148149public equals(other: RenderLineInput): boolean {150return (151this.useMonospaceOptimizations === other.useMonospaceOptimizations152&& this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow153&& this.lineContent === other.lineContent154&& this.continuesWithWrappedLine === other.continuesWithWrappedLine155&& this.isBasicASCII === other.isBasicASCII156&& this.containsRTL === other.containsRTL157&& this.fauxIndentLength === other.fauxIndentLength158&& this.tabSize === other.tabSize159&& this.startVisibleColumn === other.startVisibleColumn160&& this.spaceWidth === other.spaceWidth161&& this.renderSpaceWidth === other.renderSpaceWidth162&& this.renderSpaceCharCode === other.renderSpaceCharCode163&& this.stopRenderingLineAfter === other.stopRenderingLineAfter164&& this.renderWhitespace === other.renderWhitespace165&& this.renderControlCharacters === other.renderControlCharacters166&& this.fontLigatures === other.fontLigatures167&& LineDecoration.equalsArr(this.lineDecorations, other.lineDecorations)168&& this.lineTokens.equals(other.lineTokens)169&& this.sameSelection(other.selectionsOnLine)170&& this.textDirection === other.textDirection171&& this.verticalScrollbarSize === other.verticalScrollbarSize172&& this.renderNewLineWhenEmpty === other.renderNewLineWhenEmpty173);174}175}176177const enum CharacterMappingConstants {178PART_INDEX_MASK = 0b11111111111111110000000000000000,179CHAR_INDEX_MASK = 0b00000000000000001111111111111111,180181CHAR_INDEX_OFFSET = 0,182PART_INDEX_OFFSET = 16183}184185export class DomPosition {186constructor(187public readonly partIndex: number,188public readonly charIndex: number189) { }190}191192/**193* Provides a both direction mapping between a line's character and its rendered position.194*/195export class CharacterMapping {196197private static getPartIndex(partData: number): number {198return (partData & CharacterMappingConstants.PART_INDEX_MASK) >>> CharacterMappingConstants.PART_INDEX_OFFSET;199}200201private static getCharIndex(partData: number): number {202return (partData & CharacterMappingConstants.CHAR_INDEX_MASK) >>> CharacterMappingConstants.CHAR_INDEX_OFFSET;203}204205public readonly length: number;206private readonly _data: Uint32Array;207private readonly _horizontalOffset: Uint32Array;208209constructor(length: number, partCount: number) {210this.length = length;211this._data = new Uint32Array(this.length);212this._horizontalOffset = new Uint32Array(this.length);213}214215public setColumnInfo(column: number, partIndex: number, charIndex: number, horizontalOffset: number): void {216const partData = (217(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)218| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)219) >>> 0;220this._data[column - 1] = partData;221this._horizontalOffset[column - 1] = horizontalOffset;222}223224public getHorizontalOffset(column: number): number {225if (this._horizontalOffset.length === 0) {226// No characters on this line227return 0;228}229return this._horizontalOffset[column - 1];230}231232private charOffsetToPartData(charOffset: number): number {233if (this.length === 0) {234return 0;235}236if (charOffset < 0) {237return this._data[0];238}239if (charOffset >= this.length) {240return this._data[this.length - 1];241}242return this._data[charOffset];243}244245public getDomPosition(column: number): DomPosition {246const partData = this.charOffsetToPartData(column - 1);247const partIndex = CharacterMapping.getPartIndex(partData);248const charIndex = CharacterMapping.getCharIndex(partData);249return new DomPosition(partIndex, charIndex);250}251252public getColumn(domPosition: DomPosition, partLength: number): number {253const charOffset = this.partDataToCharOffset(domPosition.partIndex, partLength, domPosition.charIndex);254return charOffset + 1;255}256257private partDataToCharOffset(partIndex: number, partLength: number, charIndex: number): number {258if (this.length === 0) {259return 0;260}261262const searchEntry = (263(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)264| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)265) >>> 0;266267let min = 0;268let max = this.length - 1;269while (min + 1 < max) {270const mid = ((min + max) >>> 1);271const midEntry = this._data[mid];272if (midEntry === searchEntry) {273return mid;274} else if (midEntry > searchEntry) {275max = mid;276} else {277min = mid;278}279}280281if (min === max) {282return min;283}284285const minEntry = this._data[min];286const maxEntry = this._data[max];287288if (minEntry === searchEntry) {289return min;290}291if (maxEntry === searchEntry) {292return max;293}294295const minPartIndex = CharacterMapping.getPartIndex(minEntry);296const minCharIndex = CharacterMapping.getCharIndex(minEntry);297298const maxPartIndex = CharacterMapping.getPartIndex(maxEntry);299let maxCharIndex: number;300301if (minPartIndex !== maxPartIndex) {302// sitting between parts303maxCharIndex = partLength;304} else {305maxCharIndex = CharacterMapping.getCharIndex(maxEntry);306}307308const minEntryDistance = charIndex - minCharIndex;309const maxEntryDistance = maxCharIndex - charIndex;310311if (minEntryDistance <= maxEntryDistance) {312return min;313}314return max;315}316317public inflate() {318const result: [number, number, number][] = [];319for (let i = 0; i < this.length; i++) {320const partData = this._data[i];321const partIndex = CharacterMapping.getPartIndex(partData);322const charIndex = CharacterMapping.getCharIndex(partData);323const visibleColumn = this._horizontalOffset[i];324result.push([partIndex, charIndex, visibleColumn]);325}326return result;327}328}329330export const enum ForeignElementType {331None = 0,332Before = 1,333After = 2334}335336export class RenderLineOutput {337_renderLineOutputBrand: void = undefined;338339readonly characterMapping: CharacterMapping;340readonly containsForeignElements: ForeignElementType;341342constructor(characterMapping: CharacterMapping, containsForeignElements: ForeignElementType) {343this.characterMapping = characterMapping;344this.containsForeignElements = containsForeignElements;345}346}347348export function renderViewLine(input: RenderLineInput, sb: StringBuilder): RenderLineOutput {349if (input.lineContent.length === 0) {350351if (input.lineDecorations.length > 0) {352// This line is empty, but it contains inline decorations353sb.appendString(`<span>`);354355let beforeCount = 0;356let afterCount = 0;357let containsForeignElements = ForeignElementType.None;358for (const lineDecoration of input.lineDecorations) {359if (lineDecoration.type === InlineDecorationType.Before || lineDecoration.type === InlineDecorationType.After) {360sb.appendString(`<span class="`);361sb.appendString(lineDecoration.className);362sb.appendString(`"></span>`);363364if (lineDecoration.type === InlineDecorationType.Before) {365containsForeignElements |= ForeignElementType.Before;366beforeCount++;367}368if (lineDecoration.type === InlineDecorationType.After) {369containsForeignElements |= ForeignElementType.After;370afterCount++;371}372}373}374375sb.appendString(`</span>`);376377const characterMapping = new CharacterMapping(1, beforeCount + afterCount);378characterMapping.setColumnInfo(1, beforeCount, 0, 0);379380return new RenderLineOutput(381characterMapping,382containsForeignElements383);384}385386// completely empty line387if (input.renderNewLineWhenEmpty) {388sb.appendString('<span><span>\n</span></span>');389} else {390sb.appendString('<span><span></span></span>');391}392return new RenderLineOutput(393new CharacterMapping(0, 0),394ForeignElementType.None395);396}397398return _renderLine(resolveRenderLineInput(input), sb);399}400401export class RenderLineOutput2 {402constructor(403public readonly characterMapping: CharacterMapping,404public readonly html: string,405public readonly containsForeignElements: ForeignElementType406) {407}408}409410export function renderViewLine2(input: RenderLineInput): RenderLineOutput2 {411const sb = new StringBuilder(10000);412const out = renderViewLine(input, sb);413return new RenderLineOutput2(out.characterMapping, sb.build(), out.containsForeignElements);414}415416class ResolvedRenderLineInput {417constructor(418public readonly fontIsMonospace: boolean,419public readonly canUseHalfwidthRightwardsArrow: boolean,420public readonly lineContent: string,421public readonly len: number,422public readonly isOverflowing: boolean,423public readonly overflowingCharCount: number,424public readonly parts: LinePart[],425public readonly containsForeignElements: ForeignElementType,426public readonly fauxIndentLength: number,427public readonly tabSize: number,428public readonly startVisibleColumn: number,429public readonly spaceWidth: number,430public readonly renderSpaceCharCode: number,431public readonly renderWhitespace: RenderWhitespace,432public readonly renderControlCharacters: boolean,433) {434//435}436}437438function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput {439const lineContent = input.lineContent;440441let isOverflowing: boolean;442let overflowingCharCount: number;443let len: number;444445if (input.stopRenderingLineAfter !== -1 && input.stopRenderingLineAfter < lineContent.length) {446isOverflowing = true;447overflowingCharCount = lineContent.length - input.stopRenderingLineAfter;448len = input.stopRenderingLineAfter;449} else {450isOverflowing = false;451overflowingCharCount = 0;452len = lineContent.length;453}454455let tokens = transformAndRemoveOverflowing(lineContent, input.containsRTL, input.lineTokens, input.fauxIndentLength, len);456if (input.renderControlCharacters && !input.isBasicASCII) {457// Calling `extractControlCharacters` before adding (possibly empty) line parts458// for inline decorations. `extractControlCharacters` removes empty line parts.459tokens = extractControlCharacters(lineContent, tokens);460}461if (input.renderWhitespace === RenderWhitespace.All ||462input.renderWhitespace === RenderWhitespace.Boundary ||463(input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine) ||464(input.renderWhitespace === RenderWhitespace.Trailing && !input.continuesWithWrappedLine)465) {466tokens = _applyRenderWhitespace(input, lineContent, len, tokens);467}468let containsForeignElements = ForeignElementType.None;469if (input.lineDecorations.length > 0) {470for (let i = 0, len = input.lineDecorations.length; i < len; i++) {471const lineDecoration = input.lineDecorations[i];472if (lineDecoration.type === InlineDecorationType.RegularAffectingLetterSpacing) {473// Pretend there are foreign elements... although not 100% accurate.474containsForeignElements |= ForeignElementType.Before;475} else if (lineDecoration.type === InlineDecorationType.Before) {476containsForeignElements |= ForeignElementType.Before;477} else if (lineDecoration.type === InlineDecorationType.After) {478containsForeignElements |= ForeignElementType.After;479}480}481tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations);482}483if (!input.containsRTL) {484// We can never split RTL text, as it ruins the rendering485tokens = splitLargeTokens(lineContent, tokens, !input.isBasicASCII || input.fontLigatures);486}487488return new ResolvedRenderLineInput(489input.useMonospaceOptimizations,490input.canUseHalfwidthRightwardsArrow,491lineContent,492len,493isOverflowing,494overflowingCharCount,495tokens,496containsForeignElements,497input.fauxIndentLength,498input.tabSize,499input.startVisibleColumn,500input.spaceWidth,501input.renderSpaceCharCode,502input.renderWhitespace,503input.renderControlCharacters504);505}506507/**508* In the rendering phase, characters are always looped until token.endIndex.509* Ensure that all tokens end before `len` and the last one ends precisely at `len`.510*/511function transformAndRemoveOverflowing(lineContent: string, lineContainsRTL: boolean, tokens: IViewLineTokens, fauxIndentLength: number, len: number): LinePart[] {512const result: LinePart[] = [];513let resultLen = 0;514515// The faux indent part of the line should have no token type516if (fauxIndentLength > 0) {517result[resultLen++] = new LinePart(fauxIndentLength, '', 0, false);518}519let startOffset = fauxIndentLength;520for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {521const endIndex = tokens.getEndOffset(tokenIndex);522if (endIndex <= fauxIndentLength) {523// The faux indent part of the line should have no token type524continue;525}526const type = tokens.getClassName(tokenIndex);527if (endIndex >= len) {528const tokenContainsRTL = (lineContainsRTL ? strings.containsRTL(lineContent.substring(startOffset, len)) : false);529result[resultLen++] = new LinePart(len, type, 0, tokenContainsRTL);530break;531}532const tokenContainsRTL = (lineContainsRTL ? strings.containsRTL(lineContent.substring(startOffset, endIndex)) : false);533result[resultLen++] = new LinePart(endIndex, type, 0, tokenContainsRTL);534startOffset = endIndex;535}536537return result;538}539540/**541* written as a const enum to get value inlining.542*/543const enum Constants {544LongToken = 50545}546547/**548* See https://github.com/microsoft/vscode/issues/6885.549* It appears that having very large spans causes very slow reading of character positions.550* So here we try to avoid that.551*/552function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: boolean): LinePart[] {553let lastTokenEndIndex = 0;554const result: LinePart[] = [];555let resultLen = 0;556557if (onlyAtSpaces) {558// Split only at spaces => we need to walk each character559for (let i = 0, len = tokens.length; i < len; i++) {560const token = tokens[i];561const tokenEndIndex = token.endIndex;562if (lastTokenEndIndex + Constants.LongToken < tokenEndIndex) {563const tokenType = token.type;564const tokenMetadata = token.metadata;565const tokenContainsRTL = token.containsRTL;566567let lastSpaceOffset = -1;568let currTokenStart = lastTokenEndIndex;569for (let j = lastTokenEndIndex; j < tokenEndIndex; j++) {570if (lineContent.charCodeAt(j) === CharCode.Space) {571lastSpaceOffset = j;572}573if (lastSpaceOffset !== -1 && j - currTokenStart >= Constants.LongToken) {574// Split at `lastSpaceOffset` + 1575result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType, tokenMetadata, tokenContainsRTL);576currTokenStart = lastSpaceOffset + 1;577lastSpaceOffset = -1;578}579}580if (currTokenStart !== tokenEndIndex) {581result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL);582}583} else {584result[resultLen++] = token;585}586587lastTokenEndIndex = tokenEndIndex;588}589} else {590// Split anywhere => we don't need to walk each character591for (let i = 0, len = tokens.length; i < len; i++) {592const token = tokens[i];593const tokenEndIndex = token.endIndex;594const diff = (tokenEndIndex - lastTokenEndIndex);595if (diff > Constants.LongToken) {596const tokenType = token.type;597const tokenMetadata = token.metadata;598const tokenContainsRTL = token.containsRTL;599const piecesCount = Math.ceil(diff / Constants.LongToken);600for (let j = 1; j < piecesCount; j++) {601const pieceEndIndex = lastTokenEndIndex + (j * Constants.LongToken);602result[resultLen++] = new LinePart(pieceEndIndex, tokenType, tokenMetadata, tokenContainsRTL);603}604result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL);605} else {606result[resultLen++] = token;607}608lastTokenEndIndex = tokenEndIndex;609}610}611612return result;613}614615function isControlCharacter(charCode: number): boolean {616if (charCode < 32) {617return (charCode !== CharCode.Tab);618}619if (charCode === 127) {620// DEL621return true;622}623624if (625(charCode >= 0x202A && charCode <= 0x202E)626|| (charCode >= 0x2066 && charCode <= 0x2069)627|| (charCode >= 0x200E && charCode <= 0x200F)628|| charCode === 0x061C629) {630// Unicode Directional Formatting Characters631// LRE U+202A LEFT-TO-RIGHT EMBEDDING632// RLE U+202B RIGHT-TO-LEFT EMBEDDING633// PDF U+202C POP DIRECTIONAL FORMATTING634// LRO U+202D LEFT-TO-RIGHT OVERRIDE635// RLO U+202E RIGHT-TO-LEFT OVERRIDE636// LRI U+2066 LEFT-TO-RIGHT ISOLATE637// RLI U+2067 RIGHT-TO-LEFT ISOLATE638// FSI U+2068 FIRST STRONG ISOLATE639// PDI U+2069 POP DIRECTIONAL ISOLATE640// LRM U+200E LEFT-TO-RIGHT MARK641// RLM U+200F RIGHT-TO-LEFT MARK642// ALM U+061C ARABIC LETTER MARK643return true;644}645646return false;647}648649function extractControlCharacters(lineContent: string, tokens: LinePart[]): LinePart[] {650const result: LinePart[] = [];651let lastLinePart: LinePart = new LinePart(0, '', 0, false);652let charOffset = 0;653for (const token of tokens) {654const tokenEndIndex = token.endIndex;655for (; charOffset < tokenEndIndex; charOffset++) {656const charCode = lineContent.charCodeAt(charOffset);657if (isControlCharacter(charCode)) {658if (charOffset > lastLinePart.endIndex) {659// emit previous part if it has text660lastLinePart = new LinePart(charOffset, token.type, token.metadata, token.containsRTL);661result.push(lastLinePart);662}663lastLinePart = new LinePart(charOffset + 1, 'mtkcontrol', token.metadata, false);664result.push(lastLinePart);665}666}667if (charOffset > lastLinePart.endIndex) {668// emit previous part if it has text669lastLinePart = new LinePart(tokenEndIndex, token.type, token.metadata, token.containsRTL);670result.push(lastLinePart);671}672}673return result;674}675676/**677* Whitespace is rendered by "replacing" tokens with a special-purpose `mtkw` type that is later recognized in the rendering phase.678* Moreover, a token is created for every visual indent because on some fonts the glyphs used for rendering whitespace (→ or ·) do not have the same width as .679* The rendering phase will generate `style="width:..."` for these tokens.680*/681function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len: number, tokens: LinePart[]): LinePart[] {682683const continuesWithWrappedLine = input.continuesWithWrappedLine;684const fauxIndentLength = input.fauxIndentLength;685const tabSize = input.tabSize;686const startVisibleColumn = input.startVisibleColumn;687const useMonospaceOptimizations = input.useMonospaceOptimizations;688const selections = input.selectionsOnLine;689const onlyBoundary = (input.renderWhitespace === RenderWhitespace.Boundary);690const onlyTrailing = (input.renderWhitespace === RenderWhitespace.Trailing);691const generateLinePartForEachWhitespace = (input.renderSpaceWidth !== input.spaceWidth);692693const result: LinePart[] = [];694let resultLen = 0;695let tokenIndex = 0;696let tokenType = tokens[tokenIndex].type;697let tokenContainsRTL = tokens[tokenIndex].containsRTL;698let tokenEndIndex = tokens[tokenIndex].endIndex;699const tokensLength = tokens.length;700701let lineIsEmptyOrWhitespace = false;702let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);703let lastNonWhitespaceIndex: number;704if (firstNonWhitespaceIndex === -1) {705lineIsEmptyOrWhitespace = true;706firstNonWhitespaceIndex = len;707lastNonWhitespaceIndex = len;708} else {709lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent);710}711712let wasInWhitespace = false;713let currentSelectionIndex = 0;714let currentSelection = selections && selections[currentSelectionIndex];715let tmpIndent = startVisibleColumn % tabSize;716for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) {717const chCode = lineContent.charCodeAt(charIndex);718719if (currentSelection && currentSelection.endExclusive <= charIndex) {720currentSelectionIndex++;721currentSelection = selections && selections[currentSelectionIndex];722}723724let isInWhitespace: boolean;725if (charIndex < firstNonWhitespaceIndex || charIndex > lastNonWhitespaceIndex) {726// in leading or trailing whitespace727isInWhitespace = true;728} else if (chCode === CharCode.Tab) {729// a tab character is rendered both in all and boundary cases730isInWhitespace = true;731} else if (chCode === CharCode.Space) {732// hit a space character733if (onlyBoundary) {734// rendering only boundary whitespace735if (wasInWhitespace) {736isInWhitespace = true;737} else {738const nextChCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null);739isInWhitespace = (nextChCode === CharCode.Space || nextChCode === CharCode.Tab);740}741} else {742isInWhitespace = true;743}744} else {745isInWhitespace = false;746}747748// If rendering whitespace on selection, check that the charIndex falls within a selection749if (isInWhitespace && selections) {750isInWhitespace = !!currentSelection && currentSelection.start <= charIndex && charIndex < currentSelection.endExclusive;751}752753// If rendering only trailing whitespace, check that the charIndex points to trailing whitespace.754if (isInWhitespace && onlyTrailing) {755isInWhitespace = lineIsEmptyOrWhitespace || charIndex > lastNonWhitespaceIndex;756}757758if (isInWhitespace && tokenContainsRTL) {759// If the token contains RTL text, breaking it up into multiple line parts760// to render whitespace might affect the browser's bidi layout.761//762// We render whitespace in such tokens only if the whitespace763// is the leading or the trailing whitespace of the line,764// which doesn't affect the browser's bidi layout.765if (charIndex >= firstNonWhitespaceIndex && charIndex <= lastNonWhitespaceIndex) {766isInWhitespace = false;767}768}769770if (wasInWhitespace) {771// was in whitespace token772if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) {773// leaving whitespace token or entering a new indent774if (generateLinePartForEachWhitespace) {775const lastEndIndex = (resultLen > 0 ? result[resultLen - 1].endIndex : fauxIndentLength);776for (let i = lastEndIndex + 1; i <= charIndex; i++) {777result[resultLen++] = new LinePart(i, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);778}779} else {780result[resultLen++] = new LinePart(charIndex, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);781}782tmpIndent = tmpIndent % tabSize;783}784} else {785// was in regular token786if (charIndex === tokenEndIndex || (isInWhitespace && charIndex > fauxIndentLength)) {787result[resultLen++] = new LinePart(charIndex, tokenType, 0, tokenContainsRTL);788tmpIndent = tmpIndent % tabSize;789}790}791792if (chCode === CharCode.Tab) {793tmpIndent = tabSize;794} else if (strings.isFullWidthCharacter(chCode)) {795tmpIndent += 2;796} else {797tmpIndent++;798}799800wasInWhitespace = isInWhitespace;801802while (charIndex === tokenEndIndex) {803tokenIndex++;804if (tokenIndex < tokensLength) {805tokenType = tokens[tokenIndex].type;806tokenContainsRTL = tokens[tokenIndex].containsRTL;807tokenEndIndex = tokens[tokenIndex].endIndex;808} else {809break;810}811}812}813814let generateWhitespace = false;815if (wasInWhitespace) {816// was in whitespace token817if (continuesWithWrappedLine && onlyBoundary) {818const lastCharCode = (len > 0 ? lineContent.charCodeAt(len - 1) : CharCode.Null);819const prevCharCode = (len > 1 ? lineContent.charCodeAt(len - 2) : CharCode.Null);820const isSingleTrailingSpace = (lastCharCode === CharCode.Space && (prevCharCode !== CharCode.Space && prevCharCode !== CharCode.Tab));821if (!isSingleTrailingSpace) {822generateWhitespace = true;823}824} else {825generateWhitespace = true;826}827}828829if (generateWhitespace) {830if (generateLinePartForEachWhitespace) {831const lastEndIndex = (resultLen > 0 ? result[resultLen - 1].endIndex : fauxIndentLength);832for (let i = lastEndIndex + 1; i <= len; i++) {833result[resultLen++] = new LinePart(i, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);834}835} else {836result[resultLen++] = new LinePart(len, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);837}838} else {839result[resultLen++] = new LinePart(len, tokenType, 0, tokenContainsRTL);840}841842return result;843}844845/**846* Inline decorations are "merged" on top of tokens.847* Special care must be taken when multiple inline decorations are at play and they overlap.848*/849function _applyInlineDecorations(lineContent: string, len: number, tokens: LinePart[], _lineDecorations: LineDecoration[]): LinePart[] {850_lineDecorations.sort(LineDecoration.compare);851const lineDecorations = LineDecorationsNormalizer.normalize(lineContent, _lineDecorations);852const lineDecorationsLen = lineDecorations.length;853854let lineDecorationIndex = 0;855const result: LinePart[] = [];856let resultLen = 0;857let lastResultEndIndex = 0;858for (let tokenIndex = 0, len = tokens.length; tokenIndex < len; tokenIndex++) {859const token = tokens[tokenIndex];860const tokenEndIndex = token.endIndex;861const tokenType = token.type;862const tokenMetadata = token.metadata;863const tokenContainsRTL = token.containsRTL;864865while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset < tokenEndIndex) {866const lineDecoration = lineDecorations[lineDecorationIndex];867868if (lineDecoration.startOffset > lastResultEndIndex) {869lastResultEndIndex = lineDecoration.startOffset;870result[resultLen++] = new LinePart(lastResultEndIndex, tokenType, tokenMetadata, tokenContainsRTL);871}872873if (lineDecoration.endOffset + 1 <= tokenEndIndex) {874// This line decoration ends before this token ends875lastResultEndIndex = lineDecoration.endOffset + 1;876result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL);877lineDecorationIndex++;878} else {879// This line decoration continues on to the next token880lastResultEndIndex = tokenEndIndex;881result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL);882break;883}884}885886if (tokenEndIndex > lastResultEndIndex) {887lastResultEndIndex = tokenEndIndex;888result[resultLen++] = new LinePart(lastResultEndIndex, tokenType, tokenMetadata, tokenContainsRTL);889}890}891892const lastTokenEndIndex = tokens[tokens.length - 1].endIndex;893if (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) {894while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) {895const lineDecoration = lineDecorations[lineDecorationIndex];896result[resultLen++] = new LinePart(lastResultEndIndex, lineDecoration.className, lineDecoration.metadata, false);897lineDecorationIndex++;898}899}900901return result;902}903904/**905* This function is on purpose not split up into multiple functions to allow runtime type inference (i.e. performance reasons).906* Notice how all the needed data is fully resolved and passed in (i.e. no other calls).907*/908function _renderLine(input: ResolvedRenderLineInput, sb: StringBuilder): RenderLineOutput {909const fontIsMonospace = input.fontIsMonospace;910const canUseHalfwidthRightwardsArrow = input.canUseHalfwidthRightwardsArrow;911const containsForeignElements = input.containsForeignElements;912const lineContent = input.lineContent;913const len = input.len;914const isOverflowing = input.isOverflowing;915const overflowingCharCount = input.overflowingCharCount;916const parts = input.parts;917const fauxIndentLength = input.fauxIndentLength;918const tabSize = input.tabSize;919const startVisibleColumn = input.startVisibleColumn;920const spaceWidth = input.spaceWidth;921const renderSpaceCharCode = input.renderSpaceCharCode;922const renderWhitespace = input.renderWhitespace;923const renderControlCharacters = input.renderControlCharacters;924925const characterMapping = new CharacterMapping(len + 1, parts.length);926let lastCharacterMappingDefined = false;927928let charIndex = 0;929let visibleColumn = startVisibleColumn;930let charOffsetInPart = 0; // the character offset in the current part931let charHorizontalOffset = 0; // the character horizontal position in terms of chars relative to line start932933let partDisplacement = 0;934935sb.appendString('<span>');936937for (let partIndex = 0, tokensLen = parts.length; partIndex < tokensLen; partIndex++) {938939const part = parts[partIndex];940const partEndIndex = part.endIndex;941const partType = part.type;942const partContainsRTL = part.containsRTL;943const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && part.isWhitespace());944const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements);945const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.isPseudoAfter());946charOffsetInPart = 0;947948sb.appendString('<span ');949if (partContainsRTL) {950sb.appendString('style="unicode-bidi:isolate" ');951}952sb.appendString('class="');953sb.appendString(partRendersWhitespaceWithWidth ? 'mtkz' : partType);954sb.appendASCIICharCode(CharCode.DoubleQuote);955956if (partRendersWhitespace) {957958let partWidth = 0;959{960let _charIndex = charIndex;961let _visibleColumn = visibleColumn;962963for (; _charIndex < partEndIndex; _charIndex++) {964const charCode = lineContent.charCodeAt(_charIndex);965const charWidth = (charCode === CharCode.Tab ? (tabSize - (_visibleColumn % tabSize)) : 1) | 0;966partWidth += charWidth;967if (_charIndex >= fauxIndentLength) {968_visibleColumn += charWidth;969}970}971}972973if (partRendersWhitespaceWithWidth) {974sb.appendString(' style="width:');975sb.appendString(String(spaceWidth * partWidth));976sb.appendString('px"');977}978sb.appendASCIICharCode(CharCode.GreaterThan);979980for (; charIndex < partEndIndex; charIndex++) {981characterMapping.setColumnInfo(charIndex + 1, partIndex - partDisplacement, charOffsetInPart, charHorizontalOffset);982partDisplacement = 0;983const charCode = lineContent.charCodeAt(charIndex);984985let producedCharacters: number;986let charWidth: number;987988if (charCode === CharCode.Tab) {989producedCharacters = (tabSize - (visibleColumn % tabSize)) | 0;990charWidth = producedCharacters;991992if (!canUseHalfwidthRightwardsArrow || charWidth > 1) {993sb.appendCharCode(0x2192); // RIGHTWARDS ARROW994} else {995sb.appendCharCode(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW996}997for (let space = 2; space <= charWidth; space++) {998sb.appendCharCode(0xA0); // 999}10001001} else { // must be CharCode.Space1002producedCharacters = 2;1003charWidth = 1;10041005sb.appendCharCode(renderSpaceCharCode); // · or word separator middle dot1006sb.appendCharCode(0x200C); // ZERO WIDTH NON-JOINER1007}10081009charOffsetInPart += producedCharacters;1010charHorizontalOffset += charWidth;1011if (charIndex >= fauxIndentLength) {1012visibleColumn += charWidth;1013}1014}10151016} else {10171018sb.appendASCIICharCode(CharCode.GreaterThan);10191020for (; charIndex < partEndIndex; charIndex++) {1021characterMapping.setColumnInfo(charIndex + 1, partIndex - partDisplacement, charOffsetInPart, charHorizontalOffset);1022partDisplacement = 0;1023const charCode = lineContent.charCodeAt(charIndex);10241025let producedCharacters = 1;1026let charWidth = 1;10271028switch (charCode) {1029case CharCode.Tab:1030producedCharacters = (tabSize - (visibleColumn % tabSize));1031charWidth = producedCharacters;1032for (let space = 1; space <= producedCharacters; space++) {1033sb.appendCharCode(0xA0); // 1034}1035break;10361037case CharCode.Space:1038sb.appendCharCode(0xA0); // 1039break;10401041case CharCode.LessThan:1042sb.appendString('<');1043break;10441045case CharCode.GreaterThan:1046sb.appendString('>');1047break;10481049case CharCode.Ampersand:1050sb.appendString('&');1051break;10521053case CharCode.Null:1054if (renderControlCharacters) {1055// See https://unicode-table.com/en/blocks/control-pictures/1056sb.appendCharCode(9216);1057} else {1058sb.appendString('�');1059}1060break;10611062case CharCode.UTF8_BOM:1063case CharCode.LINE_SEPARATOR:1064case CharCode.PARAGRAPH_SEPARATOR:1065case CharCode.NEXT_LINE:1066sb.appendCharCode(0xFFFD);1067break;10681069default:1070if (strings.isFullWidthCharacter(charCode)) {1071charWidth++;1072}1073// See https://unicode-table.com/en/blocks/control-pictures/1074if (renderControlCharacters && charCode < 32) {1075sb.appendCharCode(9216 + charCode);1076} else if (renderControlCharacters && charCode === 127) {1077// DEL1078sb.appendCharCode(9249);1079} else if (renderControlCharacters && isControlCharacter(charCode)) {1080sb.appendString('[U+');1081sb.appendString(to4CharHex(charCode));1082sb.appendString(']');1083producedCharacters = 8;1084charWidth = producedCharacters;1085} else {1086sb.appendCharCode(charCode);1087}1088}10891090charOffsetInPart += producedCharacters;1091charHorizontalOffset += charWidth;1092if (charIndex >= fauxIndentLength) {1093visibleColumn += charWidth;1094}1095}1096}10971098if (partIsEmptyAndHasPseudoAfter) {1099partDisplacement++;1100} else {1101partDisplacement = 0;1102}11031104if (charIndex >= len && !lastCharacterMappingDefined && part.isPseudoAfter()) {1105lastCharacterMappingDefined = true;1106characterMapping.setColumnInfo(charIndex + 1, partIndex, charOffsetInPart, charHorizontalOffset);1107}11081109sb.appendString('</span>');11101111}11121113if (!lastCharacterMappingDefined) {1114// When getting client rects for the last character, we will position the1115// text range at the end of the span, insteaf of at the beginning of next span1116characterMapping.setColumnInfo(len + 1, parts.length - 1, charOffsetInPart, charHorizontalOffset);1117}11181119if (isOverflowing) {1120sb.appendString('<span class="mtkoverflow">');1121sb.appendString(nls.localize('showMore', "Show more ({0})", renderOverflowingCharCount(overflowingCharCount)));1122sb.appendString('</span>');1123}11241125sb.appendString('</span>');11261127return new RenderLineOutput(characterMapping, containsForeignElements);1128}11291130function to4CharHex(n: number): string {1131return n.toString(16).toUpperCase().padStart(4, '0');1132}11331134function renderOverflowingCharCount(n: number): string {1135if (n < 1024) {1136return nls.localize('overflow.chars', "{0} chars", n);1137}1138if (n < 1024 * 1024) {1139return `${(n / 1024).toFixed(1)} KB`;1140}1141return `${(n / 1024 / 1024).toFixed(1)} MB`;1142}114311441145