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

J
Johannes Rieken 已提交
6
import { CharCode } from 'vs/base/common/charCode';
A
Alex Dima 已提交
7
import * as strings from 'vs/base/common/strings';
A
Alex Dima 已提交
8
import { IViewLineTokens } from 'vs/editor/common/core/lineTokens';
9
import { IStringBuilder, createStringBuilder } from 'vs/editor/common/core/stringBuilder';
A
Alex Dima 已提交
10
import { LineDecoration, LineDecorationsNormalizer } from 'vs/editor/common/viewLayout/lineDecorations';
11
import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel';
A
Alex Dima 已提交
12

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

A
Alex Dima 已提交
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
class LinePart {
	_linePartBrand: void;

	/**
	 * last char index of this token (not inclusive).
	 */
	public readonly endIndex: number;
	public readonly type: string;

	constructor(endIndex: number, type: string) {
		this.endIndex = endIndex;
		this.type = type;
	}
}

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
export class LineRange {
	/**
	 * Zero-based offset on which the range starts, inclusive.
	 */
	public readonly startOffset: number;

	/**
	 * Zero-based offset on which the range ends, inclusive.
	 */
	public readonly endOffset: number;

	constructor(startIndex: number, endIndex: number) {
		this.startOffset = startIndex;
		this.endOffset = endIndex;
	}

	public equals(otherLineRange: LineRange) {
		return this.startOffset === otherLineRange.startOffset
			&& this.endOffset === otherLineRange.endOffset;
	}
}

A
Alex Dima 已提交
57
export class RenderLineInput {
58

59
	public readonly useMonospaceOptimizations: boolean;
60
	public readonly canUseHalfwidthRightwardsArrow: boolean;
61
	public readonly lineContent: string;
A
Alex Dima 已提交
62
	public readonly continuesWithWrappedLine: boolean;
63
	public readonly isBasicASCII: boolean;
64
	public readonly containsRTL: boolean;
65
	public readonly fauxIndentLength: number;
66
	public readonly lineTokens: IViewLineTokens;
A
Alex Dima 已提交
67
	public readonly lineDecorations: LineDecoration[];
68
	public readonly tabSize: number;
69
	public readonly startVisibleColumn: number;
70 71
	public readonly spaceWidth: number;
	public readonly stopRenderingLineAfter: number;
A
Alex Dima 已提交
72
	public readonly renderWhitespace: RenderWhitespace;
73
	public readonly renderControlCharacters: boolean;
74
	public readonly fontLigatures: boolean;
A
Alex Dima 已提交
75

76 77 78 79 80 81
	/**
	 * Defined only when renderWhitespace is 'selection'. Selections are non-overlapping,
	 * and ordered by position within the line.
	 */
	public readonly selectionsOnLine: LineRange[] | null;

A
Alex Dima 已提交
82
	constructor(
83
		useMonospaceOptimizations: boolean,
84
		canUseHalfwidthRightwardsArrow: boolean,
A
Alex Dima 已提交
85
		lineContent: string,
A
Alex Dima 已提交
86
		continuesWithWrappedLine: boolean,
87
		isBasicASCII: boolean,
88
		containsRTL: boolean,
89
		fauxIndentLength: number,
90
		lineTokens: IViewLineTokens,
A
Alex Dima 已提交
91
		lineDecorations: LineDecoration[],
A
Alex Dima 已提交
92
		tabSize: number,
93
		startVisibleColumn: number,
94
		spaceWidth: number,
A
Alex Dima 已提交
95
		stopRenderingLineAfter: number,
96
		renderWhitespace: 'none' | 'boundary' | 'selection' | 'all',
97
		renderControlCharacters: boolean,
98 99
		fontLigatures: boolean,
		selectionsOnLine: LineRange[] | null
A
Alex Dima 已提交
100
	) {
101
		this.useMonospaceOptimizations = useMonospaceOptimizations;
102
		this.canUseHalfwidthRightwardsArrow = canUseHalfwidthRightwardsArrow;
A
Alex Dima 已提交
103
		this.lineContent = lineContent;
A
Alex Dima 已提交
104
		this.continuesWithWrappedLine = continuesWithWrappedLine;
105
		this.isBasicASCII = isBasicASCII;
106
		this.containsRTL = containsRTL;
107
		this.fauxIndentLength = fauxIndentLength;
A
Alex Dima 已提交
108
		this.lineTokens = lineTokens;
109
		this.lineDecorations = lineDecorations;
A
Alex Dima 已提交
110
		this.tabSize = tabSize;
111
		this.startVisibleColumn = startVisibleColumn;
112
		this.spaceWidth = spaceWidth;
A
Alex Dima 已提交
113
		this.stopRenderingLineAfter = stopRenderingLineAfter;
A
Alex Dima 已提交
114 115 116 117 118
		this.renderWhitespace = (
			renderWhitespace === 'all'
				? RenderWhitespace.All
				: renderWhitespace === 'boundary'
					? RenderWhitespace.Boundary
119 120 121
					: renderWhitespace === 'selection'
						? RenderWhitespace.Selection
						: RenderWhitespace.None
A
Alex Dima 已提交
122
		);
123
		this.renderControlCharacters = renderControlCharacters;
124
		this.fontLigatures = fontLigatures;
125 126 127 128 129 130 131 132 133 134 135 136
		this.selectionsOnLine = selectionsOnLine && selectionsOnLine.sort((a, b) => a.startOffset < b.startOffset ? -1 : 1);
	}

	private sameSelection(otherSelections: LineRange[] | null): boolean {
		if (this.selectionsOnLine === null) {
			return otherSelections === null;
		}

		if (otherSelections === null) {
			return false;
		}

137 138 139 140 141 142 143 144 145 146 147
		if (otherSelections.length !== this.selectionsOnLine.length) {
			return false;
		}

		for (let i = 0; i < this.selectionsOnLine.length; i++) {
			if (!this.selectionsOnLine[i].equals(otherSelections[i])) {
				return false;
			}
		}

		return true;
A
Alex Dima 已提交
148 149
	}

A
Alex Dima 已提交
150
	public equals(other: RenderLineInput): boolean {
A
Alex Dima 已提交
151
		return (
152
			this.useMonospaceOptimizations === other.useMonospaceOptimizations
153
			&& this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow
154
			&& this.lineContent === other.lineContent
A
Alex Dima 已提交
155
			&& this.continuesWithWrappedLine === other.continuesWithWrappedLine
156
			&& this.isBasicASCII === other.isBasicASCII
157
			&& this.containsRTL === other.containsRTL
158
			&& this.fauxIndentLength === other.fauxIndentLength
A
Alex Dima 已提交
159
			&& this.tabSize === other.tabSize
160
			&& this.startVisibleColumn === other.startVisibleColumn
A
Alex Dima 已提交
161 162 163 164
			&& this.spaceWidth === other.spaceWidth
			&& this.stopRenderingLineAfter === other.stopRenderingLineAfter
			&& this.renderWhitespace === other.renderWhitespace
			&& this.renderControlCharacters === other.renderControlCharacters
165
			&& this.fontLigatures === other.fontLigatures
A
Alex Dima 已提交
166
			&& LineDecoration.equalsArr(this.lineDecorations, other.lineDecorations)
A
Alex Dima 已提交
167
			&& this.lineTokens.equals(other.lineTokens)
168
			&& this.sameSelection(other.selectionsOnLine)
A
Alex Dima 已提交
169
		);
A
Alex Dima 已提交
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
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;
	}

	public readonly length: number;
195 196
	private readonly _data: Uint32Array;
	private readonly _absoluteOffsets: Uint32Array;
197 198

	constructor(length: number, partCount: number) {
199 200
		this.length = length;
		this._data = new Uint32Array(this.length);
201
		this._absoluteOffsets = new Uint32Array(this.length);
202 203
	}

204
	public setPartData(charOffset: number, partIndex: number, charIndex: number, partAbsoluteOffset: number): void {
205 206 207 208 209
		let partData = (
			(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)
			| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)
		) >>> 0;
		this._data[charOffset] = partData;
210
		this._absoluteOffsets[charOffset] = partAbsoluteOffset + charIndex;
211 212
	}

213 214
	public getAbsoluteOffsets(): Uint32Array {
		return this._absoluteOffsets;
215 216
	}

217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
	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;
	}
}

291 292 293 294 295 296
export const enum ForeignElementType {
	None = 0,
	Before = 1,
	After = 2
}

A
Alex Dima 已提交
297
export class RenderLineOutput {
A
Alex Dima 已提交
298
	_renderLineOutputBrand: void;
A
Alex Dima 已提交
299

300
	readonly characterMapping: CharacterMapping;
301
	readonly containsRTL: boolean;
302
	readonly containsForeignElements: ForeignElementType;
A
Alex Dima 已提交
303

304
	constructor(characterMapping: CharacterMapping, containsRTL: boolean, containsForeignElements: ForeignElementType) {
305
		this.characterMapping = characterMapping;
306
		this.containsRTL = containsRTL;
307
		this.containsForeignElements = containsForeignElements;
A
Alex Dima 已提交
308
	}
309 310
}

311
export function renderViewLine(input: RenderLineInput, sb: IStringBuilder): RenderLineOutput {
A
Alex Dima 已提交
312
	if (input.lineContent.length === 0) {
313

314
		let containsForeignElements = ForeignElementType.None;
315 316

		// This is basically for IE's hit test to work
317
		let content: string = '<span><span>\u00a0</span></span>';
318 319 320 321 322 323

		if (input.lineDecorations.length > 0) {
			// This line is empty, but it contains inline decorations
			let classNames: string[] = [];
			for (let i = 0, len = input.lineDecorations.length; i < len; i++) {
				const lineDecoration = input.lineDecorations[i];
324 325 326 327 328
				if (lineDecoration.type === InlineDecorationType.Before) {
					classNames.push(input.lineDecorations[i].className);
					containsForeignElements |= ForeignElementType.Before;
				}
				if (lineDecoration.type === InlineDecorationType.After) {
329
					classNames.push(input.lineDecorations[i].className);
330
					containsForeignElements |= ForeignElementType.After;
331 332 333
				}
			}

334
			if (containsForeignElements !== ForeignElementType.None) {
335
				content = `<span><span class="${classNames.join(' ')}"></span></span>`;
336 337 338
			}
		}

339
		sb.appendASCIIString(content);
A
Alex Dima 已提交
340
		return new RenderLineOutput(
341
			new CharacterMapping(0, 0),
342
			false,
343
			containsForeignElements
A
Alex Dima 已提交
344
		);
345 346
	}

347 348 349 350 351 352 353 354
	return _renderLine(resolveRenderLineInput(input), sb);
}

export class RenderLineOutput2 {
	constructor(
		public readonly characterMapping: CharacterMapping,
		public readonly html: string,
		public readonly containsRTL: boolean,
355
		public readonly containsForeignElements: ForeignElementType
356 357 358 359 360 361 362 363
	) {
	}
}

export function renderViewLine2(input: RenderLineInput): RenderLineOutput2 {
	let sb = createStringBuilder(10000);
	let out = renderViewLine(input, sb);
	return new RenderLineOutput2(out.characterMapping, sb.build(), out.containsRTL, out.containsForeignElements);
A
Alex Dima 已提交
364 365 366 367
}

class ResolvedRenderLineInput {
	constructor(
368
		public readonly fontIsMonospace: boolean,
369
		public readonly canUseHalfwidthRightwardsArrow: boolean,
A
Alex Dima 已提交
370 371 372
		public readonly lineContent: string,
		public readonly len: number,
		public readonly isOverflowing: boolean,
A
Alex Dima 已提交
373
		public readonly parts: LinePart[],
374
		public readonly containsForeignElements: ForeignElementType,
375
		public readonly fauxIndentLength: number,
A
Alex Dima 已提交
376
		public readonly tabSize: number,
377
		public readonly startVisibleColumn: number,
378
		public readonly containsRTL: boolean,
A
Alex Dima 已提交
379 380 381 382 383
		public readonly spaceWidth: number,
		public readonly renderWhitespace: RenderWhitespace,
		public readonly renderControlCharacters: boolean,
	) {
		//
384
	}
A
Alex Dima 已提交
385
}
386

A
Alex Dima 已提交
387
function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput {
388
	const useMonospaceOptimizations = input.useMonospaceOptimizations;
A
Alex Dima 已提交
389
	const lineContent = input.lineContent;
390

A
Alex Dima 已提交
391 392
	let isOverflowing: boolean;
	let len: number;
A
Alex Dima 已提交
393

A
Alex Dima 已提交
394 395 396 397 398 399 400
	if (input.stopRenderingLineAfter !== -1 && input.stopRenderingLineAfter < lineContent.length) {
		isOverflowing = true;
		len = input.stopRenderingLineAfter;
	} else {
		isOverflowing = false;
		len = lineContent.length;
	}
401

402
	let tokens = transformAndRemoveOverflowing(input.lineTokens, input.fauxIndentLength, len);
403
	if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary || (input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine)) {
404
		tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, input.startVisibleColumn, useMonospaceOptimizations, input.selectionsOnLine, input.renderWhitespace === RenderWhitespace.Boundary);
A
Alex Dima 已提交
405
	}
406
	let containsForeignElements = ForeignElementType.None;
A
Alex Dima 已提交
407
	if (input.lineDecorations.length > 0) {
408 409
		for (let i = 0, len = input.lineDecorations.length; i < len; i++) {
			const lineDecoration = input.lineDecorations[i];
410 411 412 413 414 415 416
			if (lineDecoration.type === InlineDecorationType.RegularAffectingLetterSpacing) {
				// Pretend there are foreign elements... although not 100% accurate.
				containsForeignElements |= ForeignElementType.Before;
			} else if (lineDecoration.type === InlineDecorationType.Before) {
				containsForeignElements |= ForeignElementType.Before;
			} else if (lineDecoration.type === InlineDecorationType.After) {
				containsForeignElements |= ForeignElementType.After;
417 418
			}
		}
A
Alex Dima 已提交
419 420
		tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations);
	}
421 422 423
	if (!input.containsRTL) {
		// We can never split RTL text, as it ruins the rendering
		tokens = splitLargeTokens(lineContent, tokens, !input.isBasicASCII || input.fontLigatures);
424 425
	}

A
Alex Dima 已提交
426
	return new ResolvedRenderLineInput(
427
		useMonospaceOptimizations,
428
		input.canUseHalfwidthRightwardsArrow,
A
Alex Dima 已提交
429 430 431 432
		lineContent,
		len,
		isOverflowing,
		tokens,
433
		containsForeignElements,
434
		input.fauxIndentLength,
A
Alex Dima 已提交
435
		input.tabSize,
436
		input.startVisibleColumn,
437
		input.containsRTL,
A
Alex Dima 已提交
438 439 440 441
		input.spaceWidth,
		input.renderWhitespace,
		input.renderControlCharacters
	);
442 443
}

444 445 446 447
/**
 * 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`.
 */
448
function transformAndRemoveOverflowing(tokens: IViewLineTokens, fauxIndentLength: number, len: number): LinePart[] {
449 450 451 452 453 454 455
	let result: LinePart[] = [], resultLen = 0;

	// The faux indent part of the line should have no token type
	if (fauxIndentLength > 0) {
		result[resultLen++] = new LinePart(fauxIndentLength, '');
	}

A
Alex Dima 已提交
456
	for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {
457
		const endIndex = tokens.getEndOffset(tokenIndex);
458 459 460 461
		if (endIndex <= fauxIndentLength) {
			// The faux indent part of the line should have no token type
			continue;
		}
462
		const type = tokens.getClassName(tokenIndex);
A
Alex Dima 已提交
463
		if (endIndex >= len) {
464
			result[resultLen++] = new LinePart(len, type);
465 466
			break;
		}
467
		result[resultLen++] = new LinePart(endIndex, type);
468
	}
469

470 471 472
	return result;
}

473 474 475 476 477 478 479 480 481 482 483 484
/**
 * 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.
 */
485
function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: boolean): LinePart[] {
486
	let lastTokenEndIndex = 0;
A
Alex Dima 已提交
487
	let result: LinePart[] = [], resultLen = 0;
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514

	if (onlyAtSpaces) {
		// Split only at spaces => we need to walk each character
		for (let i = 0, len = tokens.length; i < len; i++) {
			const token = tokens[i];
			const tokenEndIndex = token.endIndex;
			if (lastTokenEndIndex + Constants.LongToken < tokenEndIndex) {
				const tokenType = token.type;

				let lastSpaceOffset = -1;
				let currTokenStart = lastTokenEndIndex;
				for (let j = lastTokenEndIndex; j < tokenEndIndex; j++) {
					if (lineContent.charCodeAt(j) === CharCode.Space) {
						lastSpaceOffset = j;
					}
					if (lastSpaceOffset !== -1 && j - currTokenStart >= Constants.LongToken) {
						// Split at `lastSpaceOffset` + 1
						result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType);
						currTokenStart = lastSpaceOffset + 1;
						lastSpaceOffset = -1;
					}
				}
				if (currTokenStart !== tokenEndIndex) {
					result[resultLen++] = new LinePart(tokenEndIndex, tokenType);
				}
			} else {
				result[resultLen++] = token;
515
			}
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536

			lastTokenEndIndex = tokenEndIndex;
		}
	} else {
		// Split anywhere => we don't need to walk each character
		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 LinePart(pieceEndIndex, tokenType);
				}
				result[resultLen++] = new LinePart(tokenEndIndex, tokenType);
			} else {
				result[resultLen++] = token;
			}
			lastTokenEndIndex = tokenEndIndex;
537 538 539 540 541 542 543 544 545 546 547
		}
	}

	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.
 */
548
function _applyRenderWhitespace(lineContent: string, len: number, continuesWithWrappedLine: boolean, tokens: LinePart[], fauxIndentLength: number, tabSize: number, startVisibleColumn: number, useMonospaceOptimizations: boolean, selections: LineRange[] | null, onlyBoundary: boolean): LinePart[] {
A
Alex Dima 已提交
549

A
Alex Dima 已提交
550
	let result: LinePart[] = [], resultLen = 0;
A
Alex Dima 已提交
551 552 553
	let tokenIndex = 0;
	let tokenType = tokens[tokenIndex].type;
	let tokenEndIndex = tokens[tokenIndex].endIndex;
A
Alex Dima 已提交
554
	const tokensLength = tokens.length;
555

A
Alex Dima 已提交
556 557 558 559 560 561 562 563 564
	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);
	}
565

A
Alex Dima 已提交
566
	let wasInWhitespace = false;
567 568
	let currentSelectionIndex = 0;
	let currentSelection = selections && selections[currentSelectionIndex];
569
	let tmpIndent = startVisibleColumn % tabSize;
A
Alex Dima 已提交
570 571 572
	for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) {
		const chCode = lineContent.charCodeAt(charIndex);

573 574 575 576 577
		if (currentSelection && charIndex >= currentSelection.endOffset) {
			currentSelectionIndex++;
			currentSelection = selections && selections[currentSelectionIndex];
		}

A
Alex Dima 已提交
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
		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;
		}

602 603 604 605 606
		// If rendering whitespace on selection, check that the charIndex falls within a selection
		if (isInWhitespace && selections) {
			isInWhitespace = !!currentSelection && currentSelection.startOffset <= charIndex && currentSelection.endOffset > charIndex;
		}

A
Alex Dima 已提交
607 608
		if (wasInWhitespace) {
			// was in whitespace token
609
			if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) {
A
Alex Dima 已提交
610
				// leaving whitespace token or entering a new indent
A
Alex Dima 已提交
611
				result[resultLen++] = new LinePart(charIndex, 'vs-whitespace');
A
Alex Dima 已提交
612 613 614 615 616
				tmpIndent = tmpIndent % tabSize;
			}
		} else {
			// was in regular token
			if (charIndex === tokenEndIndex || (isInWhitespace && charIndex > fauxIndentLength)) {
A
Alex Dima 已提交
617
				result[resultLen++] = new LinePart(charIndex, tokenType);
A
Alex Dima 已提交
618 619 620 621 622 623
				tmpIndent = tmpIndent % tabSize;
			}
		}

		if (chCode === CharCode.Tab) {
			tmpIndent = tabSize;
624 625
		} else if (strings.isFullWidthCharacter(chCode)) {
			tmpIndent += 2;
A
Alex Dima 已提交
626 627 628 629 630 631 632 633
		} else {
			tmpIndent++;
		}

		wasInWhitespace = isInWhitespace;

		if (charIndex === tokenEndIndex) {
			tokenIndex++;
A
Alex Dima 已提交
634 635 636 637
			if (tokenIndex < tokensLength) {
				tokenType = tokens[tokenIndex].type;
				tokenEndIndex = tokens[tokenIndex].endIndex;
			}
A
Alex Dima 已提交
638 639 640
		}
	}

A
Alex Dima 已提交
641
	let generateWhitespace = false;
A
Alex Dima 已提交
642 643
	if (wasInWhitespace) {
		// was in whitespace token
A
Alex Dima 已提交
644 645 646 647 648 649 650 651 652 653
		if (continuesWithWrappedLine && onlyBoundary) {
			let lastCharCode = (len > 0 ? lineContent.charCodeAt(len - 1) : CharCode.Null);
			let prevCharCode = (len > 1 ? lineContent.charCodeAt(len - 2) : CharCode.Null);
			let isSingleTrailingSpace = (lastCharCode === CharCode.Space && (prevCharCode !== CharCode.Space && prevCharCode !== CharCode.Tab));
			if (!isSingleTrailingSpace) {
				generateWhitespace = true;
			}
		} else {
			generateWhitespace = true;
		}
A
Alex Dima 已提交
654 655
	}

A
Alex Dima 已提交
656 657
	result[resultLen++] = new LinePart(len, generateWhitespace ? 'vs-whitespace' : tokenType);

A
Alex Dima 已提交
658
	return result;
659 660
}

661 662 663 664
/**
 * Inline decorations are "merged" on top of tokens.
 * Special care must be taken when multiple inline decorations are at play and they overlap.
 */
A
Alex Dima 已提交
665 666
function _applyInlineDecorations(lineContent: string, len: number, tokens: LinePart[], _lineDecorations: LineDecoration[]): LinePart[] {
	_lineDecorations.sort(LineDecoration.compare);
667
	const lineDecorations = LineDecorationsNormalizer.normalize(lineContent, _lineDecorations);
A
Alex Dima 已提交
668
	const lineDecorationsLen = lineDecorations.length;
A
Alex Dima 已提交
669

A
Alex Dima 已提交
670
	let lineDecorationIndex = 0;
A
Alex Dima 已提交
671
	let result: LinePart[] = [], resultLen = 0, lastResultEndIndex = 0;
A
Alex Dima 已提交
672 673 674 675
	for (let tokenIndex = 0, len = tokens.length; tokenIndex < len; tokenIndex++) {
		const token = tokens[tokenIndex];
		const tokenEndIndex = token.endIndex;
		const tokenType = token.type;
676

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

A
Alex Dima 已提交
680 681
			if (lineDecoration.startOffset > lastResultEndIndex) {
				lastResultEndIndex = lineDecoration.startOffset;
A
Alex Dima 已提交
682
				result[resultLen++] = new LinePart(lastResultEndIndex, tokenType);
A
Alex Dima 已提交
683
			}
A
Alex Dima 已提交
684

685
			if (lineDecoration.endOffset + 1 <= tokenEndIndex) {
686
				// This line decoration ends before this token ends
A
Alex Dima 已提交
687
				lastResultEndIndex = lineDecoration.endOffset + 1;
A
Alex Dima 已提交
688
				result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className);
A
Alex Dima 已提交
689 690
				lineDecorationIndex++;
			} else {
691 692
				// This line decoration continues on to the next token
				lastResultEndIndex = tokenEndIndex;
A
Alex Dima 已提交
693
				result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className);
A
Alex Dima 已提交
694 695 696
				break;
			}
		}
697

A
Alex Dima 已提交
698 699
		if (tokenEndIndex > lastResultEndIndex) {
			lastResultEndIndex = tokenEndIndex;
A
Alex Dima 已提交
700
			result[resultLen++] = new LinePart(lastResultEndIndex, tokenType);
701
		}
A
Alex Dima 已提交
702
	}
703

704 705 706 707 708 709 710 711
	const lastTokenEndIndex = tokens[tokens.length - 1].endIndex;
	if (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) {
		let classNames: string[] = [];
		while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) {
			classNames.push(lineDecorations[lineDecorationIndex].className);
			lineDecorationIndex++;
		}
		result[resultLen++] = new LinePart(lastResultEndIndex, classNames.join(' '));
712 713
	}

A
Alex Dima 已提交
714 715 716
	return result;
}

717 718 719 720
/**
 * 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).
 */
721
function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): RenderLineOutput {
722
	const fontIsMonospace = input.fontIsMonospace;
723
	const canUseHalfwidthRightwardsArrow = input.canUseHalfwidthRightwardsArrow;
724
	const containsForeignElements = input.containsForeignElements;
A
Alex Dima 已提交
725 726 727
	const lineContent = input.lineContent;
	const len = input.len;
	const isOverflowing = input.isOverflowing;
A
Alex Dima 已提交
728
	const parts = input.parts;
729
	const fauxIndentLength = input.fauxIndentLength;
A
Alex Dima 已提交
730
	const tabSize = input.tabSize;
731
	const startVisibleColumn = input.startVisibleColumn;
732
	const containsRTL = input.containsRTL;
A
Alex Dima 已提交
733 734 735 736
	const spaceWidth = input.spaceWidth;
	const renderWhitespace = input.renderWhitespace;
	const renderControlCharacters = input.renderControlCharacters;

A
Alex Dima 已提交
737
	const characterMapping = new CharacterMapping(len + 1, parts.length);
A
Alex Dima 已提交
738 739

	let charIndex = 0;
740
	let visibleColumn = startVisibleColumn;
A
Alex Dima 已提交
741 742
	let charOffsetInPart = 0;

743 744 745
	let prevPartContentCnt = 0;
	let partAbsoluteOffset = 0;

746 747
	sb.appendASCIIString('<span>');

A
Alex Dima 已提交
748
	for (let partIndex = 0, tokensLen = parts.length; partIndex < tokensLen; partIndex++) {
749 750
		partAbsoluteOffset += prevPartContentCnt;

A
Alex Dima 已提交
751 752 753 754
		const part = parts[partIndex];
		const partEndIndex = part.endIndex;
		const partType = part.type;
		const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && (partType.indexOf('vs-whitespace') >= 0));
A
Alex Dima 已提交
755
		charOffsetInPart = 0;
A
Alex Dima 已提交
756

757 758 759 760
		sb.appendASCIIString('<span class="');
		sb.appendASCIIString(partType);
		sb.appendASCII(CharCode.DoubleQuote);

A
Alex Dima 已提交
761
		if (partRendersWhitespace) {
762 763

			let partContentCnt = 0;
764 765
			{
				let _charIndex = charIndex;
766
				let _visibleColumn = visibleColumn;
767 768 769

				for (; _charIndex < partEndIndex; _charIndex++) {
					const charCode = lineContent.charCodeAt(_charIndex);
770 771 772 773
					const charWidth = (charCode === CharCode.Tab ? (tabSize - (_visibleColumn % tabSize)) : 1) | 0;
					partContentCnt += charWidth;
					if (_charIndex >= fauxIndentLength) {
						_visibleColumn += charWidth;
774 775 776 777
					}
				}
			}

778 779 780
			if (!fontIsMonospace) {
				const partIsOnlyWhitespace = (partType === 'vs-whitespace');
				if (partIsOnlyWhitespace || !containsForeignElements) {
781
					sb.appendASCIIString(' style="display:inline-block;width:');
782 783 784
					sb.appendASCIIString(String(spaceWidth * partContentCnt));
					sb.appendASCIIString('px"');
				}
785 786 787
			}
			sb.appendASCII(CharCode.GreaterThan);

A
Alex Dima 已提交
788
			for (; charIndex < partEndIndex; charIndex++) {
789
				characterMapping.setPartData(charIndex, partIndex, charOffsetInPart, partAbsoluteOffset);
A
Alex Dima 已提交
790
				const charCode = lineContent.charCodeAt(charIndex);
791
				let charWidth: number;
A
Alex Dima 已提交
792

A
Alex Dima 已提交
793
				if (charCode === CharCode.Tab) {
794 795 796 797 798 799
					charWidth = (tabSize - (visibleColumn % tabSize)) | 0;

					if (!canUseHalfwidthRightwardsArrow || charWidth > 1) {
						sb.write1(0x2192); // RIGHTWARDS ARROW
					} else {
						sb.write1(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW
A
Alex Dima 已提交
800
					}
801
					for (let space = 2; space <= charWidth; space++) {
802
						sb.write1(0xA0); // &nbsp;
A
Alex Dima 已提交
803
					}
804 805 806 807

				} else { // must be CharCode.Space
					charWidth = 1;

808
					sb.write1(0xB7); // &middot;
809 810
				}

811 812 813 814
				charOffsetInPart += charWidth;
				if (charIndex >= fauxIndentLength) {
					visibleColumn += charWidth;
				}
A
Alex Dima 已提交
815
			}
A
Alex Dima 已提交
816

817
			prevPartContentCnt = partContentCnt;
A
Alex Dima 已提交
818

819
		} else {
820 821

			let partContentCnt = 0;
822 823 824 825 826

			if (containsRTL) {
				sb.appendASCIIString(' dir="ltr"');
			}
			sb.appendASCII(CharCode.GreaterThan);
827

A
Alex Dima 已提交
828
			for (; charIndex < partEndIndex; charIndex++) {
829
				characterMapping.setPartData(charIndex, partIndex, charOffsetInPart, partAbsoluteOffset);
A
Alex Dima 已提交
830
				const charCode = lineContent.charCodeAt(charIndex);
831

832 833 834
				let producedCharacters = 1;
				let charWidth = 1;

835
				switch (charCode) {
A
Alex Dima 已提交
836
					case CharCode.Tab:
837 838 839
						producedCharacters = (tabSize - (visibleColumn % tabSize));
						charWidth = producedCharacters;
						for (let space = 1; space <= producedCharacters; space++) {
840
							sb.write1(0xA0); // &nbsp;
841 842 843
						}
						break;

A
Alex Dima 已提交
844
					case CharCode.Space:
845
						sb.write1(0xA0); // &nbsp;
846 847
						break;

A
Alex Dima 已提交
848
					case CharCode.LessThan:
849
						sb.appendASCIIString('&lt;');
850 851
						break;

A
Alex Dima 已提交
852
					case CharCode.GreaterThan:
853
						sb.appendASCIIString('&gt;');
854 855
						break;

A
Alex Dima 已提交
856
					case CharCode.Ampersand:
857
						sb.appendASCIIString('&amp;');
858 859
						break;

A
Alex Dima 已提交
860
					case CharCode.Null:
861
						sb.appendASCIIString('&#00;');
862 863
						break;

A
Alex Dima 已提交
864 865
					case CharCode.UTF8_BOM:
					case CharCode.LINE_SEPARATOR_2028:
866
						sb.write1(0xFFFD);
867 868 869
						break;

					default:
870
						if (strings.isFullWidthCharacter(charCode)) {
871
							charWidth++;
872
						}
A
Alex Dima 已提交
873
						if (renderControlCharacters && charCode < 32) {
874
							sb.write1(9216 + charCode);
875
						} else {
876
							sb.write1(charCode);
877
						}
878 879
				}

880 881 882 883 884
				charOffsetInPart += producedCharacters;
				partContentCnt += producedCharacters;
				if (charIndex >= fauxIndentLength) {
					visibleColumn += charWidth;
				}
A
Alex Dima 已提交
885
			}
A
Alex Dima 已提交
886

887
			prevPartContentCnt = partContentCnt;
A
Alex Dima 已提交
888
		}
889 890 891

		sb.appendASCIIString('</span>');

892 893 894 895
	}

	// 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
896
	characterMapping.setPartData(len, parts.length - 1, charOffsetInPart, partAbsoluteOffset);
897

A
Alex Dima 已提交
898
	if (isOverflowing) {
899
		sb.appendASCIIString('<span>&hellip;</span>');
900
	}
A
Alex Dima 已提交
901

902
	sb.appendASCIIString('</span>');
903

904
	return new RenderLineOutput(characterMapping, containsRTL, containsForeignElements);
905
}