viewLineRenderer.ts 20.5 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import { ViewLineToken } from 'vs/editor/common/core/viewLineToken';
J
Johannes Rieken 已提交
8
import { CharCode } from 'vs/base/common/charCode';
A
Alex Dima 已提交
9 10
import { Decoration, LineDecorationsNormalizer } from 'vs/editor/common/viewLayout/viewLineParts';
import * as strings from 'vs/base/common/strings';
A
Alex Dima 已提交
11

A
Alex Dima 已提交
12 13 14 15 16 17 18
export const enum RenderWhitespace {
	None = 0,
	Boundary = 1,
	All = 2
}

export class RenderLineInput {
19

20
	public readonly fontIsMonospace: boolean;
21
	public readonly lineContent: string;
22
	public readonly mightContainRTL: boolean;
23 24
	public readonly fauxIndentLength: number;
	public readonly lineTokens: ViewLineToken[];
25 26 27 28
	public readonly lineDecorations: Decoration[];
	public readonly tabSize: number;
	public readonly spaceWidth: number;
	public readonly stopRenderingLineAfter: number;
A
Alex Dima 已提交
29
	public readonly renderWhitespace: RenderWhitespace;
30
	public readonly renderControlCharacters: boolean;
A
Alex Dima 已提交
31 32

	constructor(
33
		fontIsMonospace: boolean,
A
Alex Dima 已提交
34
		lineContent: string,
35
		mightContainRTL: boolean,
36 37
		fauxIndentLength: number,
		lineTokens: ViewLineToken[],
38
		lineDecorations: Decoration[],
A
Alex Dima 已提交
39
		tabSize: number,
40
		spaceWidth: number,
A
Alex Dima 已提交
41
		stopRenderingLineAfter: number,
42
		renderWhitespace: 'none' | 'boundary' | 'all',
43
		renderControlCharacters: boolean,
A
Alex Dima 已提交
44
	) {
45
		this.fontIsMonospace = fontIsMonospace;
A
Alex Dima 已提交
46
		this.lineContent = lineContent;
47
		this.mightContainRTL = mightContainRTL;
48
		this.fauxIndentLength = fauxIndentLength;
49 50
		this.lineTokens = lineTokens;
		this.lineDecorations = lineDecorations;
A
Alex Dima 已提交
51
		this.tabSize = tabSize;
52
		this.spaceWidth = spaceWidth;
A
Alex Dima 已提交
53
		this.stopRenderingLineAfter = stopRenderingLineAfter;
A
Alex Dima 已提交
54 55 56 57 58 59 60
		this.renderWhitespace = (
			renderWhitespace === 'all'
				? RenderWhitespace.All
				: renderWhitespace === 'boundary'
					? RenderWhitespace.Boundary
					: RenderWhitespace.None
		);
61
		this.renderControlCharacters = renderControlCharacters;
A
Alex Dima 已提交
62 63
	}

A
Alex Dima 已提交
64
	public equals(other: RenderLineInput): boolean {
A
Alex Dima 已提交
65
		return (
66 67
			this.fontIsMonospace === other.fontIsMonospace
			&& this.lineContent === other.lineContent
68
			&& this.mightContainRTL === other.mightContainRTL
69
			&& this.fauxIndentLength === other.fauxIndentLength
A
Alex Dima 已提交
70 71 72 73 74
			&& this.tabSize === other.tabSize
			&& this.spaceWidth === other.spaceWidth
			&& this.stopRenderingLineAfter === other.stopRenderingLineAfter
			&& this.renderWhitespace === other.renderWhitespace
			&& this.renderControlCharacters === other.renderControlCharacters
75
			&& Decoration.equalsArr(this.lineDecorations, other.lineDecorations)
76
			&& ViewLineToken.equalsArr(this.lineTokens, other.lineTokens)
A
Alex Dima 已提交
77
		);
A
Alex Dima 已提交
78
	}
79 80
}

81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
export const enum CharacterMappingConstants {
	PART_INDEX_MASK = 0b11111111111111110000000000000000,
	CHAR_INDEX_MASK = 0b00000000000000001111111111111111,

	CHAR_INDEX_OFFSET = 0,
	PART_INDEX_OFFSET = 16
}

/**
 * Provides a both direction mapping between a line's character and its rendered position.
 */
export class CharacterMapping {

	public static getPartIndex(partData: number): number {
		return (partData & CharacterMappingConstants.PART_INDEX_MASK) >>> CharacterMappingConstants.PART_INDEX_OFFSET;
	}

	public static getCharIndex(partData: number): number {
		return (partData & CharacterMappingConstants.CHAR_INDEX_MASK) >>> CharacterMappingConstants.CHAR_INDEX_OFFSET;
	}

	private readonly _data: Uint32Array;
	public readonly length: number;

105 106 107
	private readonly _partLengths: Uint16Array;

	constructor(length: number, partCount: number) {
108 109
		this.length = length;
		this._data = new Uint32Array(this.length);
110
		this._partLengths = new Uint16Array(partCount);
111 112 113 114 115 116 117 118 119 120
	}

	public setPartData(charOffset: number, partIndex: number, charIndex: number): void {
		let partData = (
			(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)
			| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)
		) >>> 0;
		this._data[charOffset] = partData;
	}

121 122 123 124 125 126 127 128
	public setPartLength(partIndex: number, length: number): void {
		this._partLengths[partIndex] = length;
	}

	public getPartLengths(): Uint16Array {
		return this._partLengths;
	}

129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
	public charOffsetToPartData(charOffset: number): number {
		if (this.length === 0) {
			return 0;
		}
		if (charOffset < 0) {
			return this._data[0];
		}
		if (charOffset >= this.length) {
			return this._data[this.length - 1];
		}
		return this._data[charOffset];
	}

	public partDataToCharOffset(partIndex: number, partLength: number, charIndex: number): number {
		if (this.length === 0) {
			return 0;
		}

		let searchEntry = (
			(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)
			| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)
		) >>> 0;

		let min = 0;
		let max = this.length - 1;
		while (min + 1 < max) {
			let mid = ((min + max) >>> 1);
			let midEntry = this._data[mid];
			if (midEntry === searchEntry) {
				return mid;
			} else if (midEntry > searchEntry) {
				max = mid;
			} else {
				min = mid;
			}
		}

		if (min === max) {
			return min;
		}

		let minEntry = this._data[min];
		let maxEntry = this._data[max];

		if (minEntry === searchEntry) {
			return min;
		}
		if (maxEntry === searchEntry) {
			return max;
		}

		let minPartIndex = CharacterMapping.getPartIndex(minEntry);
		let minCharIndex = CharacterMapping.getCharIndex(minEntry);

		let maxPartIndex = CharacterMapping.getPartIndex(maxEntry);
		let maxCharIndex: number;

		if (minPartIndex !== maxPartIndex) {
			// sitting between parts
			maxCharIndex = partLength;
		} else {
			maxCharIndex = CharacterMapping.getCharIndex(maxEntry);
		}

		let minEntryDistance = charIndex - minCharIndex;
		let maxEntryDistance = maxCharIndex - charIndex;

		if (minEntryDistance <= maxEntryDistance) {
			return min;
		}
		return max;
	}
}

A
Alex Dima 已提交
203
export class RenderLineOutput {
A
Alex Dima 已提交
204
	_renderLineOutputBrand: void;
A
Alex Dima 已提交
205

206
	readonly characterMapping: CharacterMapping;
A
Alex Dima 已提交
207
	readonly output: string;
208
	readonly containsRTL: boolean;
209
	readonly containsForeignElements: boolean;
A
Alex Dima 已提交
210

211
	constructor(characterMapping: CharacterMapping, output: string, containsRTL: boolean, containsForeignElements: boolean) {
212
		this.characterMapping = characterMapping;
A
Alex Dima 已提交
213
		this.output = output;
214
		this.containsRTL = containsRTL;
215
		this.containsForeignElements = containsForeignElements;
A
Alex Dima 已提交
216
	}
217 218
}

A
Alex Dima 已提交
219 220
export function renderViewLine(input: RenderLineInput): RenderLineOutput {
	if (input.lineContent.length === 0) {
A
Alex Dima 已提交
221
		return new RenderLineOutput(
222
			new CharacterMapping(0, 0),
223
			// This is basically for IE's hit test to work
224
			'<span><span>&nbsp;</span></span>',
225
			false,
226
			false
A
Alex Dima 已提交
227
		);
228 229
	}

A
Alex Dima 已提交
230 231 232 233 234
	return _renderLine(resolveRenderLineInput(input));
}

class ResolvedRenderLineInput {
	constructor(
235
		public readonly fontIsMonospace: boolean,
A
Alex Dima 已提交
236 237 238
		public readonly lineContent: string,
		public readonly len: number,
		public readonly isOverflowing: boolean,
239
		public readonly tokens: ViewLineToken[],
240
		public readonly containsForeignElements: boolean,
A
Alex Dima 已提交
241
		public readonly tabSize: number,
242
		public readonly containsRTL: boolean,
A
Alex Dima 已提交
243 244 245 246 247
		public readonly spaceWidth: number,
		public readonly renderWhitespace: RenderWhitespace,
		public readonly renderControlCharacters: boolean,
	) {
		//
248
	}
A
Alex Dima 已提交
249
}
250

A
Alex Dima 已提交
251
function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput {
252
	const fontIsMonospace = input.fontIsMonospace;
A
Alex Dima 已提交
253
	const lineContent = input.lineContent;
254

A
Alex Dima 已提交
255 256
	let isOverflowing: boolean;
	let len: number;
A
Alex Dima 已提交
257

A
Alex Dima 已提交
258 259 260 261 262 263 264
	if (input.stopRenderingLineAfter !== -1 && input.stopRenderingLineAfter < lineContent.length) {
		isOverflowing = true;
		len = input.stopRenderingLineAfter;
	} else {
		isOverflowing = false;
		len = lineContent.length;
	}
265

266
	let tokens = removeOverflowing(input.lineTokens, len);
A
Alex Dima 已提交
267
	if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary) {
268
		tokens = _applyRenderWhitespace(lineContent, len, tokens, input.fauxIndentLength, input.tabSize, fontIsMonospace, input.renderWhitespace === RenderWhitespace.Boundary);
A
Alex Dima 已提交
269
	}
270
	let containsForeignElements = false;
A
Alex Dima 已提交
271
	if (input.lineDecorations.length > 0) {
272 273 274 275 276 277 278
		for (let i = 0, len = input.lineDecorations.length; i < len; i++) {
			const lineDecoration = input.lineDecorations[i];
			if (lineDecoration.insertsBeforeOrAfter) {
				containsForeignElements = true;
				break;
			}
		}
A
Alex Dima 已提交
279 280
		tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations);
	}
281
	let containsRTL = false;
282
	if (input.mightContainRTL) {
283 284 285 286
		containsRTL = strings.containsRTL(lineContent);
	}
	if (!containsRTL) {
		tokens = splitLargeTokens(tokens);
287 288
	}

A
Alex Dima 已提交
289
	return new ResolvedRenderLineInput(
290
		fontIsMonospace,
A
Alex Dima 已提交
291 292 293 294
		lineContent,
		len,
		isOverflowing,
		tokens,
295
		containsForeignElements,
A
Alex Dima 已提交
296
		input.tabSize,
297
		containsRTL,
A
Alex Dima 已提交
298 299 300 301
		input.spaceWidth,
		input.renderWhitespace,
		input.renderControlCharacters
	);
302 303
}

304 305 306 307
/**
 * In the rendering phase, characters are always looped until token.endIndex.
 * Ensure that all tokens end before `len` and the last one ends precisely at `len`.
 */
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
function removeOverflowing(tokens: ViewLineToken[], len: number): ViewLineToken[] {
	if (tokens.length === 0) {
		return tokens;
	}
	if (tokens[tokens.length - 1].endIndex === len) {
		return tokens;
	}
	let result: ViewLineToken[] = [];
	for (let tokenIndex = 0, tokensLen = tokens.length; tokenIndex < tokensLen; tokenIndex++) {
		const endIndex = tokens[tokenIndex].endIndex;
		if (endIndex === len) {
			result[tokenIndex] = tokens[tokenIndex];
			break;
		}
		if (endIndex > len) {
			result[tokenIndex] = new ViewLineToken(len, tokens[tokenIndex].type);
			break;
		}
		result[tokenIndex] = tokens[tokenIndex];
	}
	return result;
}

331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
/**
 * written as a const enum to get value inlining.
 */
const enum Constants {
	LongToken = 50
}

/**
 * See https://github.com/Microsoft/vscode/issues/6885.
 * It appears that having very large spans causes very slow reading of character positions.
 * So here we try to avoid that.
 */
function splitLargeTokens(tokens: ViewLineToken[]): ViewLineToken[] {
	let lastTokenEndIndex = 0;
	let result: ViewLineToken[] = [], resultLen = 0;
	for (let i = 0, len = tokens.length; i < len; i++) {
		const token = tokens[i];
		const tokenEndIndex = token.endIndex;
		let diff = (tokenEndIndex - lastTokenEndIndex);
		if (diff > Constants.LongToken) {
			const tokenType = token.type;
			const piecesCount = Math.ceil(diff / Constants.LongToken);
			for (let j = 1; j < piecesCount; j++) {
				let pieceEndIndex = lastTokenEndIndex + (j * Constants.LongToken);
				result[resultLen++] = new ViewLineToken(pieceEndIndex, tokenType);
			}
			result[resultLen++] = new ViewLineToken(tokenEndIndex, tokenType);
		} else {
			result[resultLen++] = token;
		}
		lastTokenEndIndex = tokenEndIndex;
	}

	return result;
}

/**
 * Whitespace is rendered by "replacing" tokens with a special-purpose `vs-whitespace` type that is later recognized in the rendering phase.
 * Moreover, a token is created for every visual indent because on some fonts the glyphs used for rendering whitespace (&rarr; or &middot;) do not have the same width as &nbsp;.
 * The rendering phase will generate `style="width:..."` for these tokens.
 */
372
function _applyRenderWhitespace(lineContent: string, len: number, tokens: ViewLineToken[], fauxIndentLength: number, tabSize: number, fontIsMonospace: boolean, onlyBoundary: boolean): ViewLineToken[] {
A
Alex Dima 已提交
373

374
	let result: ViewLineToken[] = [], resultLen = 0;
A
Alex Dima 已提交
375 376 377
	let tokenIndex = 0;
	let tokenType = tokens[tokenIndex].type;
	let tokenEndIndex = tokens[tokenIndex].endIndex;
378

A
Alex Dima 已提交
379
	if (fauxIndentLength > 0) {
380
		result[resultLen++] = new ViewLineToken(fauxIndentLength, '');
381 382
	}

A
Alex Dima 已提交
383 384 385 386 387 388 389 390 391
	let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
	let lastNonWhitespaceIndex: number;
	if (firstNonWhitespaceIndex === -1) {
		// The entire line is whitespace
		firstNonWhitespaceIndex = len;
		lastNonWhitespaceIndex = len;
	} else {
		lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent);
	}
392

A
Alex Dima 已提交
393 394 395 396 397 398 399 400
	let tmpIndent = 0;
	for (let charIndex = 0; charIndex < fauxIndentLength; charIndex++) {
		const chCode = lineContent.charCodeAt(charIndex);
		if (chCode === CharCode.Tab) {
			tmpIndent = tabSize;
		} else {
			tmpIndent++;
		}
401
	}
A
Alex Dima 已提交
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
	tmpIndent = tmpIndent % tabSize;

	let wasInWhitespace = false;
	for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) {
		const chCode = lineContent.charCodeAt(charIndex);

		let isInWhitespace: boolean;
		if (charIndex < firstNonWhitespaceIndex || charIndex > lastNonWhitespaceIndex) {
			// in leading or trailing whitespace
			isInWhitespace = true;
		} else if (chCode === CharCode.Tab) {
			// a tab character is rendered both in all and boundary cases
			isInWhitespace = true;
		} else if (chCode === CharCode.Space) {
			// hit a space character
			if (onlyBoundary) {
				// rendering only boundary whitespace
				if (wasInWhitespace) {
					isInWhitespace = true;
				} else {
					const nextChCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null);
					isInWhitespace = (nextChCode === CharCode.Space || nextChCode === CharCode.Tab);
				}
			} else {
				isInWhitespace = true;
			}
		} else {
			isInWhitespace = false;
		}

		if (wasInWhitespace) {
			// was in whitespace token
434
			if (!isInWhitespace || (!fontIsMonospace && tmpIndent >= tabSize)) {
A
Alex Dima 已提交
435
				// leaving whitespace token or entering a new indent
436
				result[resultLen++] = new ViewLineToken(charIndex, 'vs-whitespace');
A
Alex Dima 已提交
437 438 439 440 441
				tmpIndent = tmpIndent % tabSize;
			}
		} else {
			// was in regular token
			if (charIndex === tokenEndIndex || (isInWhitespace && charIndex > fauxIndentLength)) {
442
				result[resultLen++] = new ViewLineToken(charIndex, tokenType);
A
Alex Dima 已提交
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
				tmpIndent = tmpIndent % tabSize;
			}
		}

		if (chCode === CharCode.Tab) {
			tmpIndent = tabSize;
		} else {
			tmpIndent++;
		}

		wasInWhitespace = isInWhitespace;

		if (charIndex === tokenEndIndex) {
			tokenIndex++;
			tokenType = tokens[tokenIndex].type;
			tokenEndIndex = tokens[tokenIndex].endIndex;
		}
	}

	if (wasInWhitespace) {
		// was in whitespace token
464
		result[resultLen++] = new ViewLineToken(len, 'vs-whitespace');
A
Alex Dima 已提交
465 466
	} else {
		// was in regular token
467
		result[resultLen++] = new ViewLineToken(len, tokenType);
A
Alex Dima 已提交
468 469 470
	}

	return result;
471 472
}

473 474 475 476
/**
 * Inline decorations are "merged" on top of tokens.
 * Special care must be taken when multiple inline decorations are at play and they overlap.
 */
477
function _applyInlineDecorations(lineContent: string, len: number, tokens: ViewLineToken[], _lineDecorations: Decoration[]): ViewLineToken[] {
A
Alex Dima 已提交
478 479 480
	_lineDecorations.sort(Decoration.compare);
	const lineDecorations = LineDecorationsNormalizer.normalize(_lineDecorations);
	const lineDecorationsLen = lineDecorations.length;
A
Alex Dima 已提交
481

A
Alex Dima 已提交
482
	let lineDecorationIndex = 0;
483
	let result: ViewLineToken[] = [], resultLen = 0, lastResultEndIndex = 0;
A
Alex Dima 已提交
484 485 486 487
	for (let tokenIndex = 0, len = tokens.length; tokenIndex < len; tokenIndex++) {
		const token = tokens[tokenIndex];
		const tokenEndIndex = token.endIndex;
		const tokenType = token.type;
488

A
Alex Dima 已提交
489 490
		while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset < tokenEndIndex) {
			const lineDecoration = lineDecorations[lineDecorationIndex];
491

A
Alex Dima 已提交
492 493
			if (lineDecoration.startOffset > lastResultEndIndex) {
				lastResultEndIndex = lineDecoration.startOffset;
494
				result[resultLen++] = new ViewLineToken(lastResultEndIndex, tokenType);
A
Alex Dima 已提交
495
			}
A
Alex Dima 已提交
496

497
			if (lineDecoration.endOffset + 1 <= tokenEndIndex) {
498
				// This line decoration ends before this token ends
A
Alex Dima 已提交
499
				lastResultEndIndex = lineDecoration.endOffset + 1;
500
				result[resultLen++] = new ViewLineToken(lastResultEndIndex, tokenType + ' ' + lineDecoration.className);
A
Alex Dima 已提交
501 502
				lineDecorationIndex++;
			} else {
503 504 505
				// This line decoration continues on to the next token
				lastResultEndIndex = tokenEndIndex;
				result[resultLen++] = new ViewLineToken(lastResultEndIndex, tokenType + ' ' + lineDecoration.className);
A
Alex Dima 已提交
506 507 508
				break;
			}
		}
509

A
Alex Dima 已提交
510 511
		if (tokenEndIndex > lastResultEndIndex) {
			lastResultEndIndex = tokenEndIndex;
512
			result[resultLen++] = new ViewLineToken(lastResultEndIndex, tokenType);
513
		}
A
Alex Dima 已提交
514
	}
515

A
Alex Dima 已提交
516 517 518
	return result;
}

519 520 521 522
/**
 * This function is on purpose not split up into multiple functions to allow runtime type inference (i.e. performance reasons).
 * Notice how all the needed data is fully resolved and passed in (i.e. no other calls).
 */
A
Alex Dima 已提交
523
function _renderLine(input: ResolvedRenderLineInput): RenderLineOutput {
524
	const fontIsMonospace = input.fontIsMonospace;
525
	const containsForeignElements = input.containsForeignElements;
A
Alex Dima 已提交
526 527 528 529 530
	const lineContent = input.lineContent;
	const len = input.len;
	const isOverflowing = input.isOverflowing;
	const tokens = input.tokens;
	const tabSize = input.tabSize;
531
	const containsRTL = input.containsRTL;
A
Alex Dima 已提交
532 533 534 535
	const spaceWidth = input.spaceWidth;
	const renderWhitespace = input.renderWhitespace;
	const renderControlCharacters = input.renderControlCharacters;

536
	const characterMapping = new CharacterMapping(len + 1, tokens.length);
A
Alex Dima 已提交
537 538 539 540 541 542 543 544 545 546 547

	let charIndex = 0;
	let tabsCharDelta = 0;
	let charOffsetInPart = 0;

	let out = '<span>';
	for (let tokenIndex = 0, tokensLen = tokens.length; tokenIndex < tokensLen; tokenIndex++) {
		const token = tokens[tokenIndex];
		const tokenEndIndex = token.endIndex;
		const tokenType = token.type;
		const tokenRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && (tokenType.indexOf('vs-whitespace') >= 0));
A
Alex Dima 已提交
548
		charOffsetInPart = 0;
A
Alex Dima 已提交
549 550

		if (tokenRendersWhitespace) {
551 552 553

			let partContentCnt = 0;
			let partContent = '';
A
Alex Dima 已提交
554 555 556
			for (; charIndex < tokenEndIndex; charIndex++) {
				characterMapping.setPartData(charIndex, tokenIndex, charOffsetInPart);
				const charCode = lineContent.charCodeAt(charIndex);
A
Alex Dima 已提交
557

A
Alex Dima 已提交
558
				if (charCode === CharCode.Tab) {
A
Alex Dima 已提交
559 560 561 562
					let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
					tabsCharDelta += insertSpacesCount - 1;
					charOffsetInPart += insertSpacesCount - 1;
					if (insertSpacesCount > 0) {
563
						partContent += '&rarr;';
564
						partContentCnt++;
A
Alex Dima 已提交
565 566 567
						insertSpacesCount--;
					}
					while (insertSpacesCount > 0) {
568 569
						partContent += '&nbsp;';
						partContentCnt++;
A
Alex Dima 已提交
570 571
						insertSpacesCount--;
					}
572
				} else {
A
Alex Dima 已提交
573
					// must be CharCode.Space
574
					partContent += '&middot;';
575 576 577
					partContentCnt++;
				}

J
Johannes Rieken 已提交
578
				charOffsetInPart++;
A
Alex Dima 已提交
579
			}
A
Alex Dima 已提交
580

581
			characterMapping.setPartLength(tokenIndex, partContentCnt);
582 583 584 585 586
			if (fontIsMonospace) {
				out += `<span class="${tokenType}">${partContent}</span>`;
			} else {
				out += `<span class="${tokenType}" style="width:${spaceWidth * partContentCnt}px">${partContent}</span>`;
			}
A
Alex Dima 已提交
587

588
		} else {
589 590

			let partContentCnt = 0;
591
			let partContent = '';
592

A
Alex Dima 已提交
593 594 595
			for (; charIndex < tokenEndIndex; charIndex++) {
				characterMapping.setPartData(charIndex, tokenIndex, charOffsetInPart);
				const charCode = lineContent.charCodeAt(charIndex);
596 597

				switch (charCode) {
A
Alex Dima 已提交
598
					case CharCode.Tab:
599 600 601 602
						let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
						tabsCharDelta += insertSpacesCount - 1;
						charOffsetInPart += insertSpacesCount - 1;
						while (insertSpacesCount > 0) {
603
							partContent += '&nbsp;';
604
							partContentCnt++;
605 606 607 608
							insertSpacesCount--;
						}
						break;

A
Alex Dima 已提交
609
					case CharCode.Space:
610
						partContent += '&nbsp;';
611
						partContentCnt++;
612 613
						break;

A
Alex Dima 已提交
614
					case CharCode.LessThan:
615
						partContent += '&lt;';
616
						partContentCnt++;
617 618
						break;

A
Alex Dima 已提交
619
					case CharCode.GreaterThan:
620
						partContent += '&gt;';
621
						partContentCnt++;
622 623
						break;

A
Alex Dima 已提交
624
					case CharCode.Ampersand:
625
						partContent += '&amp;';
626
						partContentCnt++;
627 628
						break;

A
Alex Dima 已提交
629
					case CharCode.Null:
630
						partContent += '&#00;';
631
						partContentCnt++;
632 633
						break;

A
Alex Dima 已提交
634 635
					case CharCode.UTF8_BOM:
					case CharCode.LINE_SEPARATOR_2028:
636
						partContent += '\ufffd';
637
						partContentCnt++;
638 639
						break;

A
Alex Dima 已提交
640
					case CharCode.CarriageReturn:
641
						// zero width space, because carriage return would introduce a line break
642
						partContent += '&#8203';
643
						partContentCnt++;
644 645 646
						break;

					default:
A
Alex Dima 已提交
647 648
						if (renderControlCharacters && charCode < 32) {
							partContent += String.fromCharCode(9216 + charCode);
649
							partContentCnt++;
650
						} else {
651 652
							partContent += String.fromCharCode(charCode);
							partContentCnt++;
653
						}
654 655
				}

J
Johannes Rieken 已提交
656
				charOffsetInPart++;
A
Alex Dima 已提交
657
			}
A
Alex Dima 已提交
658

659
			characterMapping.setPartLength(tokenIndex, partContentCnt);
660
			if (containsRTL) {
661 662 663 664
				out += `<span dir="ltr" class="${tokenType}">${partContent}</span>`;
			} else {
				out += `<span class="${tokenType}">${partContent}</span>`;
			}
A
Alex Dima 已提交
665

A
Alex Dima 已提交
666
		}
667 668 669 670
	}

	// When getting client rects for the last character, we will position the
	// text range at the end of the span, insteaf of at the beginning of next span
A
Alex Dima 已提交
671
	characterMapping.setPartData(len, tokens.length - 1, charOffsetInPart);
672

A
Alex Dima 已提交
673
	if (isOverflowing) {
674
		out += `<span class="vs-whitespace">&hellip;</span>`;
675
	}
A
Alex Dima 已提交
676

677 678
	out += '</span>';

679
	return new RenderLineOutput(characterMapping, out, containsRTL, containsForeignElements);
680
}