viewLineRenderer.ts 20.3 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) {
A
Alex Dima 已提交
498
				lastResultEndIndex = lineDecoration.endOffset + 1;
499
				result[resultLen++] = new ViewLineToken(lastResultEndIndex, tokenType + ' ' + lineDecoration.className);
A
Alex Dima 已提交
500 501 502 503 504
				lineDecorationIndex++;
			} else {
				break;
			}
		}
505

A
Alex Dima 已提交
506 507
		if (tokenEndIndex > lastResultEndIndex) {
			lastResultEndIndex = tokenEndIndex;
508
			result[resultLen++] = new ViewLineToken(lastResultEndIndex, tokenType);
509
		}
A
Alex Dima 已提交
510
	}
511

A
Alex Dima 已提交
512 513 514
	return result;
}

515 516 517 518
/**
 * 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 已提交
519
function _renderLine(input: ResolvedRenderLineInput): RenderLineOutput {
520
	const fontIsMonospace = input.fontIsMonospace;
521
	const containsForeignElements = input.containsForeignElements;
A
Alex Dima 已提交
522 523 524 525 526
	const lineContent = input.lineContent;
	const len = input.len;
	const isOverflowing = input.isOverflowing;
	const tokens = input.tokens;
	const tabSize = input.tabSize;
527
	const containsRTL = input.containsRTL;
A
Alex Dima 已提交
528 529 530 531
	const spaceWidth = input.spaceWidth;
	const renderWhitespace = input.renderWhitespace;
	const renderControlCharacters = input.renderControlCharacters;

532
	const characterMapping = new CharacterMapping(len + 1, tokens.length);
A
Alex Dima 已提交
533 534 535 536 537 538 539 540 541 542 543

	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 已提交
544
		charOffsetInPart = 0;
A
Alex Dima 已提交
545 546

		if (tokenRendersWhitespace) {
547 548 549

			let partContentCnt = 0;
			let partContent = '';
A
Alex Dima 已提交
550 551 552
			for (; charIndex < tokenEndIndex; charIndex++) {
				characterMapping.setPartData(charIndex, tokenIndex, charOffsetInPart);
				const charCode = lineContent.charCodeAt(charIndex);
A
Alex Dima 已提交
553

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

J
Johannes Rieken 已提交
574
				charOffsetInPart++;
A
Alex Dima 已提交
575
			}
A
Alex Dima 已提交
576

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

584
		} else {
585 586

			let partContentCnt = 0;
587
			let partContent = '';
588

A
Alex Dima 已提交
589 590 591
			for (; charIndex < tokenEndIndex; charIndex++) {
				characterMapping.setPartData(charIndex, tokenIndex, charOffsetInPart);
				const charCode = lineContent.charCodeAt(charIndex);
592 593

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

A
Alex Dima 已提交
605
					case CharCode.Space:
606
						partContent += '&nbsp;';
607
						partContentCnt++;
608 609
						break;

A
Alex Dima 已提交
610
					case CharCode.LessThan:
611
						partContent += '&lt;';
612
						partContentCnt++;
613 614
						break;

A
Alex Dima 已提交
615
					case CharCode.GreaterThan:
616
						partContent += '&gt;';
617
						partContentCnt++;
618 619
						break;

A
Alex Dima 已提交
620
					case CharCode.Ampersand:
621
						partContent += '&amp;';
622
						partContentCnt++;
623 624
						break;

A
Alex Dima 已提交
625
					case CharCode.Null:
626
						partContent += '&#00;';
627
						partContentCnt++;
628 629
						break;

A
Alex Dima 已提交
630 631
					case CharCode.UTF8_BOM:
					case CharCode.LINE_SEPARATOR_2028:
632
						partContent += '\ufffd';
633
						partContentCnt++;
634 635
						break;

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

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

J
Johannes Rieken 已提交
652
				charOffsetInPart++;
A
Alex Dima 已提交
653
			}
A
Alex Dima 已提交
654

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

A
Alex Dima 已提交
662
		}
663 664 665 666
	}

	// 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 已提交
667
	characterMapping.setPartData(len, tokens.length - 1, charOffsetInPart);
668

A
Alex Dima 已提交
669
	if (isOverflowing) {
670
		out += `<span class="vs-whitespace">&hellip;</span>`;
671
	}
A
Alex Dima 已提交
672

673 674
	out += '</span>';

675
	return new RenderLineOutput(characterMapping, out, containsRTL, containsForeignElements);
676
}