Path: blob/main/src/vs/editor/common/viewLayout/viewLineRenderer.ts
5237 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 interface IRenderLineInputOptions {25useMonospaceOptimizations: boolean;26canUseHalfwidthRightwardsArrow: boolean;27lineContent: string;28continuesWithWrappedLine: boolean;29isBasicASCII: boolean;30containsRTL: boolean;31fauxIndentLength: number;32lineTokens: IViewLineTokens;33lineDecorations: LineDecoration[];34tabSize: number;35startVisibleColumn: number;36spaceWidth: number;37middotWidth: number;38wsmiddotWidth: number;39stopRenderingLineAfter: number;40renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all';41renderControlCharacters: boolean;42fontLigatures: boolean;43selectionsOnLine: OffsetRange[] | null;44textDirection: TextDirection | null;45verticalScrollbarSize: number;46renderNewLineWhenEmpty: boolean;47}4849export class RenderLineInput {5051public readonly useMonospaceOptimizations: boolean;52public readonly canUseHalfwidthRightwardsArrow: boolean;53public readonly lineContent: string;54public readonly continuesWithWrappedLine: boolean;55public readonly isBasicASCII: boolean;56public readonly containsRTL: boolean;57public readonly fauxIndentLength: number;58public readonly lineTokens: IViewLineTokens;59public readonly lineDecorations: LineDecoration[];60public readonly tabSize: number;61public readonly startVisibleColumn: number;62public readonly spaceWidth: number;63public readonly renderSpaceWidth: number;64public readonly renderSpaceCharCode: number;65public readonly stopRenderingLineAfter: number;66public readonly renderWhitespace: RenderWhitespace;67public readonly renderControlCharacters: boolean;68public readonly fontLigatures: boolean;69public readonly textDirection: TextDirection | null;70public readonly verticalScrollbarSize: number;7172/**73* Defined only when renderWhitespace is 'selection'. Selections are non-overlapping,74* and ordered by position within the line.75*/76public readonly selectionsOnLine: OffsetRange[] | null;77/**78* When rendering an empty line, whether to render a new line instead79*/80public readonly renderNewLineWhenEmpty: boolean;8182public get isLTR(): boolean {83return !this.containsRTL && this.textDirection !== TextDirection.RTL;84}8586constructor(87useMonospaceOptimizations: boolean,88canUseHalfwidthRightwardsArrow: boolean,89lineContent: string,90continuesWithWrappedLine: boolean,91isBasicASCII: boolean,92containsRTL: boolean,93fauxIndentLength: number,94lineTokens: IViewLineTokens,95lineDecorations: LineDecoration[],96tabSize: number,97startVisibleColumn: number,98spaceWidth: number,99middotWidth: number,100wsmiddotWidth: number,101stopRenderingLineAfter: number,102renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all',103renderControlCharacters: boolean,104fontLigatures: boolean,105selectionsOnLine: OffsetRange[] | null,106textDirection: TextDirection | null,107verticalScrollbarSize: number,108renderNewLineWhenEmpty: boolean = false,109) {110this.useMonospaceOptimizations = useMonospaceOptimizations;111this.canUseHalfwidthRightwardsArrow = canUseHalfwidthRightwardsArrow;112this.lineContent = lineContent;113this.continuesWithWrappedLine = continuesWithWrappedLine;114this.isBasicASCII = isBasicASCII;115this.containsRTL = containsRTL;116this.fauxIndentLength = fauxIndentLength;117this.lineTokens = lineTokens;118this.lineDecorations = lineDecorations.sort(LineDecoration.compare);119this.tabSize = tabSize;120this.startVisibleColumn = startVisibleColumn;121this.spaceWidth = spaceWidth;122this.stopRenderingLineAfter = stopRenderingLineAfter;123this.renderWhitespace = (124renderWhitespace === 'all'125? RenderWhitespace.All126: renderWhitespace === 'boundary'127? RenderWhitespace.Boundary128: renderWhitespace === 'selection'129? RenderWhitespace.Selection130: renderWhitespace === 'trailing'131? RenderWhitespace.Trailing132: RenderWhitespace.None133);134this.renderControlCharacters = renderControlCharacters;135this.fontLigatures = fontLigatures;136this.selectionsOnLine = selectionsOnLine && selectionsOnLine.sort((a, b) => a.start < b.start ? -1 : 1);137this.renderNewLineWhenEmpty = renderNewLineWhenEmpty;138this.textDirection = textDirection;139this.verticalScrollbarSize = verticalScrollbarSize;140141const wsmiddotDiff = Math.abs(wsmiddotWidth - spaceWidth);142const middotDiff = Math.abs(middotWidth - spaceWidth);143if (wsmiddotDiff < middotDiff) {144this.renderSpaceWidth = wsmiddotWidth;145this.renderSpaceCharCode = 0x2E31; // U+2E31 - WORD SEPARATOR MIDDLE DOT146} else {147this.renderSpaceWidth = middotWidth;148this.renderSpaceCharCode = 0xB7; // U+00B7 - MIDDLE DOT149}150}151152private sameSelection(otherSelections: OffsetRange[] | null): boolean {153if (this.selectionsOnLine === null) {154return otherSelections === null;155}156157if (otherSelections === null) {158return false;159}160161if (otherSelections.length !== this.selectionsOnLine.length) {162return false;163}164165for (let i = 0; i < this.selectionsOnLine.length; i++) {166if (!this.selectionsOnLine[i].equals(otherSelections[i])) {167return false;168}169}170171return true;172}173174public equals(other: RenderLineInput): boolean {175return (176this.useMonospaceOptimizations === other.useMonospaceOptimizations177&& this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow178&& this.lineContent === other.lineContent179&& this.continuesWithWrappedLine === other.continuesWithWrappedLine180&& this.isBasicASCII === other.isBasicASCII181&& this.containsRTL === other.containsRTL182&& this.fauxIndentLength === other.fauxIndentLength183&& this.tabSize === other.tabSize184&& this.startVisibleColumn === other.startVisibleColumn185&& this.spaceWidth === other.spaceWidth186&& this.renderSpaceWidth === other.renderSpaceWidth187&& this.renderSpaceCharCode === other.renderSpaceCharCode188&& this.stopRenderingLineAfter === other.stopRenderingLineAfter189&& this.renderWhitespace === other.renderWhitespace190&& this.renderControlCharacters === other.renderControlCharacters191&& this.fontLigatures === other.fontLigatures192&& LineDecoration.equalsArr(this.lineDecorations, other.lineDecorations)193&& this.lineTokens.equals(other.lineTokens)194&& this.sameSelection(other.selectionsOnLine)195&& this.textDirection === other.textDirection196&& this.verticalScrollbarSize === other.verticalScrollbarSize197&& this.renderNewLineWhenEmpty === other.renderNewLineWhenEmpty198);199}200}201202const enum CharacterMappingConstants {203PART_INDEX_MASK = 0b11111111111111110000000000000000,204CHAR_INDEX_MASK = 0b00000000000000001111111111111111,205206CHAR_INDEX_OFFSET = 0,207PART_INDEX_OFFSET = 16208}209210export class DomPosition {211constructor(212public readonly partIndex: number,213public readonly charIndex: number214) { }215}216217/**218* Provides a both direction mapping between a line's character and its rendered position.219*/220export class CharacterMapping {221222private static getPartIndex(partData: number): number {223return (partData & CharacterMappingConstants.PART_INDEX_MASK) >>> CharacterMappingConstants.PART_INDEX_OFFSET;224}225226private static getCharIndex(partData: number): number {227return (partData & CharacterMappingConstants.CHAR_INDEX_MASK) >>> CharacterMappingConstants.CHAR_INDEX_OFFSET;228}229230public readonly length: number;231private readonly _data: Uint32Array;232private readonly _horizontalOffset: Uint32Array;233234constructor(length: number, partCount: number) {235this.length = length;236this._data = new Uint32Array(this.length);237this._horizontalOffset = new Uint32Array(this.length);238}239240public setColumnInfo(column: number, partIndex: number, charIndex: number, horizontalOffset: number): void {241const partData = (242(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)243| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)244) >>> 0;245this._data[column - 1] = partData;246this._horizontalOffset[column - 1] = horizontalOffset;247}248249public getHorizontalOffset(column: number): number {250if (this._horizontalOffset.length === 0) {251// No characters on this line252return 0;253}254return this._horizontalOffset[column - 1];255}256257private charOffsetToPartData(charOffset: number): number {258if (this.length === 0) {259return 0;260}261if (charOffset < 0) {262return this._data[0];263}264if (charOffset >= this.length) {265return this._data[this.length - 1];266}267return this._data[charOffset];268}269270public getDomPosition(column: number): DomPosition {271const partData = this.charOffsetToPartData(column - 1);272const partIndex = CharacterMapping.getPartIndex(partData);273const charIndex = CharacterMapping.getCharIndex(partData);274return new DomPosition(partIndex, charIndex);275}276277public getColumn(domPosition: DomPosition, partLength: number): number {278const charOffset = this.partDataToCharOffset(domPosition.partIndex, partLength, domPosition.charIndex);279return charOffset + 1;280}281282private partDataToCharOffset(partIndex: number, partLength: number, charIndex: number): number {283if (this.length === 0) {284return 0;285}286287const searchEntry = (288(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)289| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)290) >>> 0;291292let min = 0;293let max = this.length - 1;294while (min + 1 < max) {295const mid = ((min + max) >>> 1);296const midEntry = this._data[mid];297if (midEntry === searchEntry) {298return mid;299} else if (midEntry > searchEntry) {300max = mid;301} else {302min = mid;303}304}305306if (min === max) {307return min;308}309310const minEntry = this._data[min];311const maxEntry = this._data[max];312313if (minEntry === searchEntry) {314return min;315}316if (maxEntry === searchEntry) {317return max;318}319320const minPartIndex = CharacterMapping.getPartIndex(minEntry);321const minCharIndex = CharacterMapping.getCharIndex(minEntry);322323const maxPartIndex = CharacterMapping.getPartIndex(maxEntry);324let maxCharIndex: number;325326if (minPartIndex !== maxPartIndex) {327// sitting between parts328maxCharIndex = partLength;329} else {330maxCharIndex = CharacterMapping.getCharIndex(maxEntry);331}332333const minEntryDistance = charIndex - minCharIndex;334const maxEntryDistance = maxCharIndex - charIndex;335336if (minEntryDistance <= maxEntryDistance) {337return min;338}339return max;340}341342public inflate() {343const result: [number, number, number][] = [];344for (let i = 0; i < this.length; i++) {345const partData = this._data[i];346const partIndex = CharacterMapping.getPartIndex(partData);347const charIndex = CharacterMapping.getCharIndex(partData);348const visibleColumn = this._horizontalOffset[i];349result.push([partIndex, charIndex, visibleColumn]);350}351return result;352}353}354355export const enum ForeignElementType {356None = 0,357Before = 1,358After = 2359}360361export class RenderLineOutput {362_renderLineOutputBrand: void = undefined;363364readonly characterMapping: CharacterMapping;365readonly containsForeignElements: ForeignElementType;366367constructor(characterMapping: CharacterMapping, containsForeignElements: ForeignElementType) {368this.characterMapping = characterMapping;369this.containsForeignElements = containsForeignElements;370}371}372373export function renderViewLine(input: RenderLineInput, sb: StringBuilder): RenderLineOutput {374if (input.lineContent.length === 0) {375376if (input.lineDecorations.length > 0) {377// This line is empty, but it contains inline decorations378sb.appendString(`<span>`);379380let beforeCount = 0;381let afterCount = 0;382let containsForeignElements = ForeignElementType.None;383for (const lineDecoration of input.lineDecorations) {384if (lineDecoration.type === InlineDecorationType.Before || lineDecoration.type === InlineDecorationType.After) {385sb.appendString(`<span class="`);386sb.appendString(lineDecoration.className);387sb.appendString(`"></span>`);388389if (lineDecoration.type === InlineDecorationType.Before) {390containsForeignElements |= ForeignElementType.Before;391beforeCount++;392}393if (lineDecoration.type === InlineDecorationType.After) {394containsForeignElements |= ForeignElementType.After;395afterCount++;396}397}398}399400sb.appendString(`</span>`);401402const characterMapping = new CharacterMapping(1, beforeCount + afterCount);403characterMapping.setColumnInfo(1, beforeCount, 0, 0);404405return new RenderLineOutput(406characterMapping,407containsForeignElements408);409}410411// completely empty line412if (input.renderNewLineWhenEmpty) {413sb.appendString('<span><span>\n</span></span>');414} else {415sb.appendString('<span><span></span></span>');416}417return new RenderLineOutput(418new CharacterMapping(0, 0),419ForeignElementType.None420);421}422423return _renderLine(resolveRenderLineInput(input), sb);424}425426export class RenderLineOutput2 {427constructor(428public readonly characterMapping: CharacterMapping,429public readonly html: string,430public readonly containsForeignElements: ForeignElementType431) {432}433}434435export function renderViewLine2(input: RenderLineInput): RenderLineOutput2 {436const sb = new StringBuilder(10000);437const out = renderViewLine(input, sb);438return new RenderLineOutput2(out.characterMapping, sb.build(), out.containsForeignElements);439}440441class ResolvedRenderLineInput {442constructor(443public readonly fontIsMonospace: boolean,444public readonly canUseHalfwidthRightwardsArrow: boolean,445public readonly lineContent: string,446public readonly len: number,447public readonly isOverflowing: boolean,448public readonly overflowingCharCount: number,449public readonly parts: LinePart[],450public readonly containsForeignElements: ForeignElementType,451public readonly fauxIndentLength: number,452public readonly tabSize: number,453public readonly startVisibleColumn: number,454public readonly spaceWidth: number,455public readonly renderSpaceCharCode: number,456public readonly renderWhitespace: RenderWhitespace,457public readonly renderControlCharacters: boolean,458) {459//460}461}462463function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput {464const lineContent = input.lineContent;465466let isOverflowing: boolean;467let overflowingCharCount: number;468let len: number;469470if (input.stopRenderingLineAfter !== -1 && input.stopRenderingLineAfter < lineContent.length) {471isOverflowing = true;472overflowingCharCount = lineContent.length - input.stopRenderingLineAfter;473len = input.stopRenderingLineAfter;474} else {475isOverflowing = false;476overflowingCharCount = 0;477len = lineContent.length;478}479480let tokens = transformAndRemoveOverflowing(lineContent, input.containsRTL, input.lineTokens, input.fauxIndentLength, len);481if (input.renderControlCharacters && !input.isBasicASCII) {482// Calling `extractControlCharacters` before adding (possibly empty) line parts483// for inline decorations. `extractControlCharacters` removes empty line parts.484tokens = extractControlCharacters(lineContent, tokens);485}486if (input.renderWhitespace === RenderWhitespace.All ||487input.renderWhitespace === RenderWhitespace.Boundary ||488(input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine) ||489(input.renderWhitespace === RenderWhitespace.Trailing && !input.continuesWithWrappedLine)490) {491tokens = _applyRenderWhitespace(input, lineContent, len, tokens);492}493let containsForeignElements = ForeignElementType.None;494if (input.lineDecorations.length > 0) {495for (let i = 0, len = input.lineDecorations.length; i < len; i++) {496const lineDecoration = input.lineDecorations[i];497if (lineDecoration.type === InlineDecorationType.RegularAffectingLetterSpacing) {498// Pretend there are foreign elements... although not 100% accurate.499containsForeignElements |= ForeignElementType.Before;500} else if (lineDecoration.type === InlineDecorationType.Before) {501containsForeignElements |= ForeignElementType.Before;502} else if (lineDecoration.type === InlineDecorationType.After) {503containsForeignElements |= ForeignElementType.After;504}505}506tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations);507}508if (!input.containsRTL) {509// We can never split RTL text, as it ruins the rendering510tokens = splitLargeTokens(lineContent, tokens, !input.isBasicASCII || input.fontLigatures);511} else {512// Split the first token if it contains both leading whitespace and RTL text513tokens = splitLeadingWhitespaceFromRTL(lineContent, tokens);514}515516return new ResolvedRenderLineInput(517input.useMonospaceOptimizations,518input.canUseHalfwidthRightwardsArrow,519lineContent,520len,521isOverflowing,522overflowingCharCount,523tokens,524containsForeignElements,525input.fauxIndentLength,526input.tabSize,527input.startVisibleColumn,528input.spaceWidth,529input.renderSpaceCharCode,530input.renderWhitespace,531input.renderControlCharacters532);533}534535/**536* In the rendering phase, characters are always looped until token.endIndex.537* Ensure that all tokens end before `len` and the last one ends precisely at `len`.538*/539function transformAndRemoveOverflowing(lineContent: string, lineContainsRTL: boolean, tokens: IViewLineTokens, fauxIndentLength: number, len: number): LinePart[] {540const result: LinePart[] = [];541let resultLen = 0;542543// The faux indent part of the line should have no token type544if (fauxIndentLength > 0) {545result[resultLen++] = new LinePart(fauxIndentLength, '', 0, false);546}547let startOffset = fauxIndentLength;548for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {549const endIndex = tokens.getEndOffset(tokenIndex);550if (endIndex <= fauxIndentLength) {551// The faux indent part of the line should have no token type552continue;553}554const type = tokens.getClassName(tokenIndex);555if (endIndex >= len) {556const tokenContainsRTL = (lineContainsRTL ? strings.containsRTL(lineContent.substring(startOffset, len)) : false);557result[resultLen++] = new LinePart(len, type, 0, tokenContainsRTL);558break;559}560const tokenContainsRTL = (lineContainsRTL ? strings.containsRTL(lineContent.substring(startOffset, endIndex)) : false);561result[resultLen++] = new LinePart(endIndex, type, 0, tokenContainsRTL);562startOffset = endIndex;563}564565return result;566}567568/**569* written as a const enum to get value inlining.570*/571const enum Constants {572LongToken = 50573}574575/**576* See https://github.com/microsoft/vscode/issues/6885.577* It appears that having very large spans causes very slow reading of character positions.578* So here we try to avoid that.579*/580function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: boolean): LinePart[] {581let lastTokenEndIndex = 0;582const result: LinePart[] = [];583let resultLen = 0;584585if (onlyAtSpaces) {586// Split only at spaces => we need to walk each character587for (let i = 0, len = tokens.length; i < len; i++) {588const token = tokens[i];589const tokenEndIndex = token.endIndex;590if (lastTokenEndIndex + Constants.LongToken < tokenEndIndex) {591const tokenType = token.type;592const tokenMetadata = token.metadata;593const tokenContainsRTL = token.containsRTL;594595let lastSpaceOffset = -1;596let currTokenStart = lastTokenEndIndex;597for (let j = lastTokenEndIndex; j < tokenEndIndex; j++) {598if (lineContent.charCodeAt(j) === CharCode.Space) {599lastSpaceOffset = j;600}601if (lastSpaceOffset !== -1 && j - currTokenStart >= Constants.LongToken) {602// Split at `lastSpaceOffset` + 1603result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType, tokenMetadata, tokenContainsRTL);604currTokenStart = lastSpaceOffset + 1;605lastSpaceOffset = -1;606}607}608if (currTokenStart !== tokenEndIndex) {609result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL);610}611} else {612result[resultLen++] = token;613}614615lastTokenEndIndex = tokenEndIndex;616}617} else {618// Split anywhere => we don't need to walk each character619for (let i = 0, len = tokens.length; i < len; i++) {620const token = tokens[i];621const tokenEndIndex = token.endIndex;622const diff = (tokenEndIndex - lastTokenEndIndex);623if (diff > Constants.LongToken) {624const tokenType = token.type;625const tokenMetadata = token.metadata;626const tokenContainsRTL = token.containsRTL;627const piecesCount = Math.ceil(diff / Constants.LongToken);628for (let j = 1; j < piecesCount; j++) {629const pieceEndIndex = lastTokenEndIndex + (j * Constants.LongToken);630result[resultLen++] = new LinePart(pieceEndIndex, tokenType, tokenMetadata, tokenContainsRTL);631}632result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL);633} else {634result[resultLen++] = token;635}636lastTokenEndIndex = tokenEndIndex;637}638}639640return result;641}642643/**644* Splits leading whitespace from the first token if it contains RTL text.645*/646function splitLeadingWhitespaceFromRTL(lineContent: string, tokens: LinePart[]): LinePart[] {647if (tokens.length === 0) {648return tokens;649}650651const firstToken = tokens[0];652if (!firstToken.containsRTL) {653return tokens;654}655656// Check if the first token starts with whitespace657const firstTokenEndIndex = firstToken.endIndex;658let firstNonWhitespaceIndex = 0;659for (let i = 0; i < firstTokenEndIndex; i++) {660const charCode = lineContent.charCodeAt(i);661if (charCode !== CharCode.Space && charCode !== CharCode.Tab) {662firstNonWhitespaceIndex = i;663break;664}665}666667if (firstNonWhitespaceIndex === 0) {668// No leading whitespace669return tokens;670}671672// Split the first token into leading whitespace and the rest673const result: LinePart[] = [];674result.push(new LinePart(firstNonWhitespaceIndex, firstToken.type, firstToken.metadata, false));675result.push(new LinePart(firstTokenEndIndex, firstToken.type, firstToken.metadata, firstToken.containsRTL));676677// Add remaining tokens678for (let i = 1; i < tokens.length; i++) {679result.push(tokens[i]);680}681682return result;683}684685function isControlCharacter(charCode: number): boolean {686if (charCode < 32) {687return (charCode !== CharCode.Tab);688}689if (charCode === 127) {690// DEL691return true;692}693694if (695(charCode >= 0x202A && charCode <= 0x202E)696|| (charCode >= 0x2066 && charCode <= 0x2069)697|| (charCode >= 0x200E && charCode <= 0x200F)698|| charCode === 0x061C699) {700// Unicode Directional Formatting Characters701// LRE U+202A LEFT-TO-RIGHT EMBEDDING702// RLE U+202B RIGHT-TO-LEFT EMBEDDING703// PDF U+202C POP DIRECTIONAL FORMATTING704// LRO U+202D LEFT-TO-RIGHT OVERRIDE705// RLO U+202E RIGHT-TO-LEFT OVERRIDE706// LRI U+2066 LEFT-TO-RIGHT ISOLATE707// RLI U+2067 RIGHT-TO-LEFT ISOLATE708// FSI U+2068 FIRST STRONG ISOLATE709// PDI U+2069 POP DIRECTIONAL ISOLATE710// LRM U+200E LEFT-TO-RIGHT MARK711// RLM U+200F RIGHT-TO-LEFT MARK712// ALM U+061C ARABIC LETTER MARK713return true;714}715716return false;717}718719function extractControlCharacters(lineContent: string, tokens: LinePart[]): LinePart[] {720const result: LinePart[] = [];721let lastLinePart: LinePart = new LinePart(0, '', 0, false);722let charOffset = 0;723for (const token of tokens) {724const tokenEndIndex = token.endIndex;725for (; charOffset < tokenEndIndex; charOffset++) {726const charCode = lineContent.charCodeAt(charOffset);727if (isControlCharacter(charCode)) {728if (charOffset > lastLinePart.endIndex) {729// emit previous part if it has text730lastLinePart = new LinePart(charOffset, token.type, token.metadata, token.containsRTL);731result.push(lastLinePart);732}733lastLinePart = new LinePart(charOffset + 1, 'mtkcontrol', token.metadata, false);734result.push(lastLinePart);735}736}737if (charOffset > lastLinePart.endIndex) {738// emit previous part if it has text739lastLinePart = new LinePart(tokenEndIndex, token.type, token.metadata, token.containsRTL);740result.push(lastLinePart);741}742}743return result;744}745746/**747* Whitespace is rendered by "replacing" tokens with a special-purpose `mtkw` type that is later recognized in the rendering phase.748* 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 .749* The rendering phase will generate `style="width:..."` for these tokens.750*/751function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len: number, tokens: LinePart[]): LinePart[] {752753const continuesWithWrappedLine = input.continuesWithWrappedLine;754const fauxIndentLength = input.fauxIndentLength;755const tabSize = input.tabSize;756const startVisibleColumn = input.startVisibleColumn;757const useMonospaceOptimizations = input.useMonospaceOptimizations;758const selections = input.selectionsOnLine;759const onlyBoundary = (input.renderWhitespace === RenderWhitespace.Boundary);760const onlyTrailing = (input.renderWhitespace === RenderWhitespace.Trailing);761const generateLinePartForEachWhitespace = (input.renderSpaceWidth !== input.spaceWidth);762763const result: LinePart[] = [];764let resultLen = 0;765let tokenIndex = 0;766let tokenType = tokens[tokenIndex].type;767let tokenContainsRTL = tokens[tokenIndex].containsRTL;768let tokenEndIndex = tokens[tokenIndex].endIndex;769const tokensLength = tokens.length;770771let lineIsEmptyOrWhitespace = false;772let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);773let lastNonWhitespaceIndex: number;774if (firstNonWhitespaceIndex === -1) {775lineIsEmptyOrWhitespace = true;776firstNonWhitespaceIndex = len;777lastNonWhitespaceIndex = len;778} else {779lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent);780}781782let wasInWhitespace = false;783let currentSelectionIndex = 0;784let currentSelection = selections && selections[currentSelectionIndex];785let tmpIndent = startVisibleColumn % tabSize;786for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) {787const chCode = lineContent.charCodeAt(charIndex);788789if (currentSelection && currentSelection.endExclusive <= charIndex) {790currentSelectionIndex++;791currentSelection = selections && selections[currentSelectionIndex];792}793794let isInWhitespace: boolean;795if (charIndex < firstNonWhitespaceIndex || charIndex > lastNonWhitespaceIndex) {796// in leading or trailing whitespace797isInWhitespace = true;798} else if (chCode === CharCode.Tab) {799// a tab character is rendered both in all and boundary cases800isInWhitespace = true;801} else if (chCode === CharCode.Space) {802// hit a space character803if (onlyBoundary) {804// rendering only boundary whitespace805if (wasInWhitespace) {806isInWhitespace = true;807} else {808const nextChCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null);809isInWhitespace = (nextChCode === CharCode.Space || nextChCode === CharCode.Tab);810}811} else {812isInWhitespace = true;813}814} else {815isInWhitespace = false;816}817818// If rendering whitespace on selection, check that the charIndex falls within a selection819if (isInWhitespace && selections) {820isInWhitespace = !!currentSelection && currentSelection.start <= charIndex && charIndex < currentSelection.endExclusive;821}822823// If rendering only trailing whitespace, check that the charIndex points to trailing whitespace.824if (isInWhitespace && onlyTrailing) {825isInWhitespace = lineIsEmptyOrWhitespace || charIndex > lastNonWhitespaceIndex;826}827828if (isInWhitespace && tokenContainsRTL) {829// If the token contains RTL text, breaking it up into multiple line parts830// to render whitespace might affect the browser's bidi layout.831//832// We render whitespace in such tokens only if the whitespace833// is the leading or the trailing whitespace of the line,834// which doesn't affect the browser's bidi layout.835if (charIndex >= firstNonWhitespaceIndex && charIndex <= lastNonWhitespaceIndex) {836isInWhitespace = false;837}838}839840if (wasInWhitespace) {841// was in whitespace token842if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) {843// leaving whitespace token or entering a new indent844if (generateLinePartForEachWhitespace) {845const lastEndIndex = (resultLen > 0 ? result[resultLen - 1].endIndex : fauxIndentLength);846for (let i = lastEndIndex + 1; i <= charIndex; i++) {847result[resultLen++] = new LinePart(i, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);848}849} else {850result[resultLen++] = new LinePart(charIndex, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);851}852tmpIndent = tmpIndent % tabSize;853}854} else {855// was in regular token856if (charIndex === tokenEndIndex || (isInWhitespace && charIndex > fauxIndentLength)) {857result[resultLen++] = new LinePart(charIndex, tokenType, 0, tokenContainsRTL);858tmpIndent = tmpIndent % tabSize;859}860}861862if (chCode === CharCode.Tab) {863tmpIndent = tabSize;864} else if (strings.isFullWidthCharacter(chCode)) {865tmpIndent += 2;866} else {867tmpIndent++;868}869870wasInWhitespace = isInWhitespace;871872while (charIndex === tokenEndIndex) {873tokenIndex++;874if (tokenIndex < tokensLength) {875tokenType = tokens[tokenIndex].type;876tokenContainsRTL = tokens[tokenIndex].containsRTL;877tokenEndIndex = tokens[tokenIndex].endIndex;878} else {879break;880}881}882}883884let generateWhitespace = false;885if (wasInWhitespace) {886// was in whitespace token887if (continuesWithWrappedLine && onlyBoundary) {888const lastCharCode = (len > 0 ? lineContent.charCodeAt(len - 1) : CharCode.Null);889const prevCharCode = (len > 1 ? lineContent.charCodeAt(len - 2) : CharCode.Null);890const isSingleTrailingSpace = (lastCharCode === CharCode.Space && (prevCharCode !== CharCode.Space && prevCharCode !== CharCode.Tab));891if (!isSingleTrailingSpace) {892generateWhitespace = true;893}894} else {895generateWhitespace = true;896}897}898899if (generateWhitespace) {900if (generateLinePartForEachWhitespace) {901const lastEndIndex = (resultLen > 0 ? result[resultLen - 1].endIndex : fauxIndentLength);902for (let i = lastEndIndex + 1; i <= len; i++) {903result[resultLen++] = new LinePart(i, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);904}905} else {906result[resultLen++] = new LinePart(len, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);907}908} else {909result[resultLen++] = new LinePart(len, tokenType, 0, tokenContainsRTL);910}911912return result;913}914915/**916* Inline decorations are "merged" on top of tokens.917* Special care must be taken when multiple inline decorations are at play and they overlap.918*/919function _applyInlineDecorations(lineContent: string, len: number, tokens: LinePart[], _lineDecorations: LineDecoration[]): LinePart[] {920_lineDecorations.sort(LineDecoration.compare);921const lineDecorations = LineDecorationsNormalizer.normalize(lineContent, _lineDecorations);922const lineDecorationsLen = lineDecorations.length;923924let lineDecorationIndex = 0;925const result: LinePart[] = [];926let resultLen = 0;927let lastResultEndIndex = 0;928for (let tokenIndex = 0, len = tokens.length; tokenIndex < len; tokenIndex++) {929const token = tokens[tokenIndex];930const tokenEndIndex = token.endIndex;931const tokenType = token.type;932const tokenMetadata = token.metadata;933const tokenContainsRTL = token.containsRTL;934935while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset < tokenEndIndex) {936const lineDecoration = lineDecorations[lineDecorationIndex];937938if (lineDecoration.startOffset > lastResultEndIndex) {939lastResultEndIndex = lineDecoration.startOffset;940result[resultLen++] = new LinePart(lastResultEndIndex, tokenType, tokenMetadata, tokenContainsRTL);941}942943if (lineDecoration.endOffset + 1 <= tokenEndIndex) {944// This line decoration ends before this token ends945lastResultEndIndex = lineDecoration.endOffset + 1;946result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL);947lineDecorationIndex++;948} else {949// This line decoration continues on to the next token950lastResultEndIndex = tokenEndIndex;951result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL);952break;953}954}955956if (tokenEndIndex > lastResultEndIndex) {957lastResultEndIndex = tokenEndIndex;958result[resultLen++] = new LinePart(lastResultEndIndex, tokenType, tokenMetadata, tokenContainsRTL);959}960}961962const lastTokenEndIndex = tokens[tokens.length - 1].endIndex;963if (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) {964while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) {965const lineDecoration = lineDecorations[lineDecorationIndex];966result[resultLen++] = new LinePart(lastResultEndIndex, lineDecoration.className, lineDecoration.metadata, false);967lineDecorationIndex++;968}969}970971return result;972}973974/**975* This function is on purpose not split up into multiple functions to allow runtime type inference (i.e. performance reasons).976* Notice how all the needed data is fully resolved and passed in (i.e. no other calls).977*/978function _renderLine(input: ResolvedRenderLineInput, sb: StringBuilder): RenderLineOutput {979const fontIsMonospace = input.fontIsMonospace;980const canUseHalfwidthRightwardsArrow = input.canUseHalfwidthRightwardsArrow;981const containsForeignElements = input.containsForeignElements;982const lineContent = input.lineContent;983const len = input.len;984const isOverflowing = input.isOverflowing;985const overflowingCharCount = input.overflowingCharCount;986const parts = input.parts;987const fauxIndentLength = input.fauxIndentLength;988const tabSize = input.tabSize;989const startVisibleColumn = input.startVisibleColumn;990const spaceWidth = input.spaceWidth;991const renderSpaceCharCode = input.renderSpaceCharCode;992const renderWhitespace = input.renderWhitespace;993const renderControlCharacters = input.renderControlCharacters;994995const characterMapping = new CharacterMapping(len + 1, parts.length);996let lastCharacterMappingDefined = false;997998let charIndex = 0;999let visibleColumn = startVisibleColumn;1000let charOffsetInPart = 0; // the character offset in the current part1001let charHorizontalOffset = 0; // the character horizontal position in terms of chars relative to line start10021003let partDisplacement = 0;10041005sb.appendString('<span>');10061007for (let partIndex = 0, tokensLen = parts.length; partIndex < tokensLen; partIndex++) {10081009const part = parts[partIndex];1010const partEndIndex = part.endIndex;1011const partType = part.type;1012const partContainsRTL = part.containsRTL;1013const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && part.isWhitespace());1014const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements);1015const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.isPseudoAfter());1016charOffsetInPart = 0;10171018sb.appendString('<span ');1019if (partContainsRTL) {1020sb.appendString('style="unicode-bidi:isolate" ');1021}1022sb.appendString('class="');1023sb.appendString(partRendersWhitespaceWithWidth ? 'mtkz' : partType);1024sb.appendASCIICharCode(CharCode.DoubleQuote);10251026if (partRendersWhitespace) {10271028let partWidth = 0;1029{1030let _charIndex = charIndex;1031let _visibleColumn = visibleColumn;10321033for (; _charIndex < partEndIndex; _charIndex++) {1034const charCode = lineContent.charCodeAt(_charIndex);1035const charWidth = (charCode === CharCode.Tab ? (tabSize - (_visibleColumn % tabSize)) : 1) | 0;1036partWidth += charWidth;1037if (_charIndex >= fauxIndentLength) {1038_visibleColumn += charWidth;1039}1040}1041}10421043if (partRendersWhitespaceWithWidth) {1044sb.appendString(' style="width:');1045sb.appendString(String(spaceWidth * partWidth));1046sb.appendString('px"');1047}1048sb.appendASCIICharCode(CharCode.GreaterThan);10491050for (; charIndex < partEndIndex; charIndex++) {1051characterMapping.setColumnInfo(charIndex + 1, partIndex - partDisplacement, charOffsetInPart, charHorizontalOffset);1052partDisplacement = 0;1053const charCode = lineContent.charCodeAt(charIndex);10541055let producedCharacters: number;1056let charWidth: number;10571058if (charCode === CharCode.Tab) {1059producedCharacters = (tabSize - (visibleColumn % tabSize)) | 0;1060charWidth = producedCharacters;10611062if (!canUseHalfwidthRightwardsArrow || charWidth > 1) {1063sb.appendCharCode(0x2192); // RIGHTWARDS ARROW1064} else {1065sb.appendCharCode(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW1066}1067for (let space = 2; space <= charWidth; space++) {1068sb.appendCharCode(0xA0); // 1069}10701071} else { // must be CharCode.Space1072producedCharacters = 2;1073charWidth = 1;10741075sb.appendCharCode(renderSpaceCharCode); // · or word separator middle dot1076sb.appendCharCode(0x200C); // ZERO WIDTH NON-JOINER1077}10781079charOffsetInPart += producedCharacters;1080charHorizontalOffset += charWidth;1081if (charIndex >= fauxIndentLength) {1082visibleColumn += charWidth;1083}1084}10851086} else {10871088sb.appendASCIICharCode(CharCode.GreaterThan);10891090for (; charIndex < partEndIndex; charIndex++) {1091characterMapping.setColumnInfo(charIndex + 1, partIndex - partDisplacement, charOffsetInPart, charHorizontalOffset);1092partDisplacement = 0;1093const charCode = lineContent.charCodeAt(charIndex);10941095let producedCharacters = 1;1096let charWidth = 1;10971098switch (charCode) {1099case CharCode.Tab:1100producedCharacters = (tabSize - (visibleColumn % tabSize));1101charWidth = producedCharacters;1102for (let space = 1; space <= producedCharacters; space++) {1103sb.appendCharCode(0xA0); // 1104}1105break;11061107case CharCode.Space:1108sb.appendCharCode(0xA0); // 1109break;11101111case CharCode.LessThan:1112sb.appendString('<');1113break;11141115case CharCode.GreaterThan:1116sb.appendString('>');1117break;11181119case CharCode.Ampersand:1120sb.appendString('&');1121break;11221123case CharCode.Null:1124if (renderControlCharacters) {1125// See https://unicode-table.com/en/blocks/control-pictures/1126sb.appendCharCode(9216);1127} else {1128sb.appendString('�');1129}1130break;11311132case CharCode.UTF8_BOM:1133case CharCode.LINE_SEPARATOR:1134case CharCode.PARAGRAPH_SEPARATOR:1135case CharCode.NEXT_LINE:1136sb.appendCharCode(0xFFFD);1137break;11381139default:1140if (strings.isFullWidthCharacter(charCode)) {1141charWidth++;1142}1143// See https://unicode-table.com/en/blocks/control-pictures/1144if (renderControlCharacters && charCode < 32) {1145sb.appendCharCode(9216 + charCode);1146} else if (renderControlCharacters && charCode === 127) {1147// DEL1148sb.appendCharCode(9249);1149} else if (renderControlCharacters && isControlCharacter(charCode)) {1150sb.appendString('[U+');1151sb.appendString(to4CharHex(charCode));1152sb.appendString(']');1153producedCharacters = 8;1154charWidth = producedCharacters;1155} else {1156sb.appendCharCode(charCode);1157}1158}11591160charOffsetInPart += producedCharacters;1161charHorizontalOffset += charWidth;1162if (charIndex >= fauxIndentLength) {1163visibleColumn += charWidth;1164}1165}1166}11671168if (partIsEmptyAndHasPseudoAfter) {1169partDisplacement++;1170} else {1171partDisplacement = 0;1172}11731174if (charIndex >= len && !lastCharacterMappingDefined && part.isPseudoAfter()) {1175lastCharacterMappingDefined = true;1176characterMapping.setColumnInfo(charIndex + 1, partIndex, charOffsetInPart, charHorizontalOffset);1177}11781179sb.appendString('</span>');11801181}11821183if (!lastCharacterMappingDefined) {1184// When getting client rects for the last character, we will position the1185// text range at the end of the span, insteaf of at the beginning of next span1186characterMapping.setColumnInfo(len + 1, parts.length - 1, charOffsetInPart, charHorizontalOffset);1187}11881189if (isOverflowing) {1190sb.appendString('<span class="mtkoverflow">');1191sb.appendString(nls.localize('showMore', "Show more ({0})", renderOverflowingCharCount(overflowingCharCount)));1192sb.appendString('</span>');1193}11941195sb.appendString('</span>');11961197return new RenderLineOutput(characterMapping, containsForeignElements);1198}11991200function to4CharHex(n: number): string {1201return n.toString(16).toUpperCase().padStart(4, '0');1202}12031204function renderOverflowingCharCount(n: number): string {1205if (n < 1024) {1206return nls.localize('overflow.chars', "{0} chars", n);1207}1208if (n < 1024 * 1024) {1209return `${(n / 1024).toFixed(1)} KB`;1210}1211return `${(n / 1024 / 1024).toFixed(1)} MB`;1212}121312141215