viewLineRenderer.ts 10.4 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';

J
Johannes Rieken 已提交
7 8
import { ViewLineToken } from 'vs/editor/common/core/viewLineToken';
import { CharCode } from 'vs/base/common/charCode';
A
Alex Dima 已提交
9
import { LineParts } from 'vs/editor/common/core/lineParts';
A
Alex Dima 已提交
10 11

export class RenderLineInput {
A
Alex Dima 已提交
12
	_renderLineInputBrand: void;
13 14 15

	lineContent: string;
	tabSize: number;
16
	spaceWidth: number;
17
	stopRenderingLineAfter: number;
18
	renderWhitespace: 'none' | 'boundary' | 'all';
19
	renderControlCharacters: boolean;
A
Alex Dima 已提交
20
	lineParts: LineParts;
A
Alex Dima 已提交
21 22 23 24

	constructor(
		lineContent: string,
		tabSize: number,
25
		spaceWidth: number,
A
Alex Dima 已提交
26
		stopRenderingLineAfter: number,
27
		renderWhitespace: 'none' | 'boundary' | 'all',
28
		renderControlCharacters: boolean,
A
Alex Dima 已提交
29
		lineParts: LineParts
A
Alex Dima 已提交
30 31 32
	) {
		this.lineContent = lineContent;
		this.tabSize = tabSize;
33
		this.spaceWidth = spaceWidth;
A
Alex Dima 已提交
34 35
		this.stopRenderingLineAfter = stopRenderingLineAfter;
		this.renderWhitespace = renderWhitespace;
36
		this.renderControlCharacters = renderControlCharacters;
A
Alex Dima 已提交
37 38 39 40 41 42 43 44 45 46 47 48 49
		this.lineParts = lineParts;
	}

	public equals(other: RenderLineInput): boolean {
		return (
			this.lineContent === other.lineContent
			&& this.tabSize === other.tabSize
			&& this.spaceWidth === other.spaceWidth
			&& this.stopRenderingLineAfter === other.stopRenderingLineAfter
			&& this.renderWhitespace === other.renderWhitespace
			&& this.renderControlCharacters === other.renderControlCharacters
			&& this.lineParts.equals(other.lineParts)
		);
A
Alex Dima 已提交
50
	}
51 52
}

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 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
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;

	constructor(length: number) {
		this.length = length;
		this._data = new Uint32Array(this.length);
	}

	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;
	}

	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 已提交
164
export class RenderLineOutput {
A
Alex Dima 已提交
165
	_renderLineOutputBrand: void;
A
Alex Dima 已提交
166

167
	readonly characterMapping: CharacterMapping;
A
Alex Dima 已提交
168 169
	readonly output: string;
	readonly isWhitespaceOnly: boolean;
A
Alex Dima 已提交
170

171 172
	constructor(characterMapping: CharacterMapping, output: string, isWhitespaceOnly: boolean) {
		this.characterMapping = characterMapping;
A
Alex Dima 已提交
173
		this.output = output;
174
		this.isWhitespaceOnly = isWhitespaceOnly;
A
Alex Dima 已提交
175
	}
176 177
}

J
Johannes Rieken 已提交
178
export function renderLine(input: RenderLineInput): RenderLineOutput {
179
	const lineText = input.lineContent;
A
Alex Dima 已提交
180
	const lineTextLength = lineText.length;
181
	const tabSize = input.tabSize;
182
	const spaceWidth = input.spaceWidth;
A
Alex Dima 已提交
183
	const actualLineParts = input.lineParts.parts;
A
Alex Dima 已提交
184
	const renderWhitespace = input.renderWhitespace;
185
	const renderControlCharacters = input.renderControlCharacters;
A
Alex Dima 已提交
186
	const charBreakIndex = (input.stopRenderingLineAfter === -1 ? lineTextLength : input.stopRenderingLineAfter - 1);
187 188

	if (lineTextLength === 0) {
A
Alex Dima 已提交
189
		return new RenderLineOutput(
190
			new CharacterMapping(0),
191
			// This is basically for IE's hit test to work
192 193
			'<span><span>&nbsp;</span></span>',
			true
A
Alex Dima 已提交
194
		);
195 196 197 198 199 200
	}

	if (actualLineParts.length === 0) {
		throw new Error('Cannot render non empty line without line parts!');
	}

A
Alex Dima 已提交
201
	return renderLineActual(lineText, lineTextLength, tabSize, spaceWidth, actualLineParts, renderWhitespace, renderControlCharacters, charBreakIndex);
A
Alex Dima 已提交
202 203
}

J
Johannes Rieken 已提交
204
function isWhitespace(type: string): boolean {
205
	return (type.indexOf('vs-whitespace') >= 0);
206 207
}

208 209 210
function isControlCharacter(characterCode: number): boolean {
	return characterCode < 32;
}
A
Alex Dima 已提交
211 212

const _controlCharacterSequenceConversionStart = 9216;
213 214 215 216
function controlCharacterToPrintable(characterCode: number): string {
	return String.fromCharCode(_controlCharacterSequenceConversionStart + characterCode);
}

217
function renderLineActual(lineText: string, lineTextLength: number, tabSize: number, spaceWidth: number, actualLineParts: ViewLineToken[], renderWhitespace: 'none' | 'boundary' | 'all', renderControlCharacters: boolean, charBreakIndex: number): RenderLineOutput {
A
Alex Dima 已提交
218 219 220 221
	lineTextLength = +lineTextLength;
	tabSize = +tabSize;
	charBreakIndex = +charBreakIndex;

A
Alex Dima 已提交
222
	let charIndex = 0;
A
Alex Dima 已提交
223
	let out = '';
A
Alex Dima 已提交
224
	let charOffsetInPart = 0;
225
	let tabsCharDelta = 0;
226
	let isWhitespaceOnly = /^\s*$/.test(lineText);
227

228 229
	let characterMapping = new CharacterMapping(Math.min(lineTextLength, charBreakIndex) + 1);

A
Alex Dima 已提交
230
	out += '<span>';
A
Alex Dima 已提交
231 232 233
	for (let partIndex = 0, partIndexLen = actualLineParts.length; partIndex < partIndexLen; partIndex++) {
		let part = actualLineParts[partIndex];

234
		let parsRendersWhitespace = (renderWhitespace !== 'none' && isWhitespace(part.type));
235

A
Alex Dima 已提交
236 237
		let toCharIndex = lineTextLength;
		if (partIndex + 1 < partIndexLen) {
A
Alex Dima 已提交
238 239
			let nextPart = actualLineParts[partIndex + 1];
			toCharIndex = Math.min(lineTextLength, nextPart.startIndex);
240 241
		}

A
Alex Dima 已提交
242
		charOffsetInPart = 0;
243
		if (parsRendersWhitespace) {
244 245 246 247

			let partContentCnt = 0;
			let partContent = '';
			for (; charIndex < toCharIndex; charIndex++) {
248
				characterMapping.setPartData(charIndex, partIndex, charOffsetInPart);
249
				let charCode = lineText.charCodeAt(charIndex);
A
Alex Dima 已提交
250

A
Alex Dima 已提交
251
				if (charCode === CharCode.Tab) {
A
Alex Dima 已提交
252 253 254 255
					let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
					tabsCharDelta += insertSpacesCount - 1;
					charOffsetInPart += insertSpacesCount - 1;
					if (insertSpacesCount > 0) {
256
						partContent += '&rarr;';
257
						partContentCnt++;
A
Alex Dima 已提交
258 259 260
						insertSpacesCount--;
					}
					while (insertSpacesCount > 0) {
261 262
						partContent += '&nbsp;';
						partContentCnt++;
A
Alex Dima 已提交
263 264
						insertSpacesCount--;
					}
265
				} else {
A
Alex Dima 已提交
266
					// must be CharCode.Space
267
					partContent += '&middot;';
268 269 270
					partContentCnt++;
				}

J
Johannes Rieken 已提交
271
				charOffsetInPart++;
272 273

				if (charIndex >= charBreakIndex) {
274
					out += `<span class="${part.type}" style="width:${(spaceWidth * partContentCnt)}px">${partContent}&hellip;</span></span>`;
275
					characterMapping.setPartData(charIndex, partIndex, charOffsetInPart);
A
Alex Dima 已提交
276
					return new RenderLineOutput(
277
						characterMapping,
278 279
						out,
						isWhitespaceOnly
A
Alex Dima 已提交
280
					);
281
				}
A
Alex Dima 已提交
282
			}
283
			out += `<span class="${part.type}" style="width:${(spaceWidth * partContentCnt)}px">${partContent}</span>`;
284
		} else {
285
			out += `<span class="${part.type}">`;
286 287

			for (; charIndex < toCharIndex; charIndex++) {
288
				characterMapping.setPartData(charIndex, partIndex, charOffsetInPart);
289 290 291
				let charCode = lineText.charCodeAt(charIndex);

				switch (charCode) {
A
Alex Dima 已提交
292
					case CharCode.Tab:
293 294 295 296 297 298 299 300 301
						let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
						tabsCharDelta += insertSpacesCount - 1;
						charOffsetInPart += insertSpacesCount - 1;
						while (insertSpacesCount > 0) {
							out += '&nbsp;';
							insertSpacesCount--;
						}
						break;

A
Alex Dima 已提交
302
					case CharCode.Space:
303 304 305
						out += '&nbsp;';
						break;

A
Alex Dima 已提交
306
					case CharCode.LessThan:
307 308 309
						out += '&lt;';
						break;

A
Alex Dima 已提交
310
					case CharCode.GreaterThan:
311 312 313
						out += '&gt;';
						break;

A
Alex Dima 已提交
314
					case CharCode.Ampersand:
315 316 317
						out += '&amp;';
						break;

A
Alex Dima 已提交
318
					case CharCode.Null:
319 320 321
						out += '&#00;';
						break;

A
Alex Dima 已提交
322 323
					case CharCode.UTF8_BOM:
					case CharCode.LINE_SEPARATOR_2028:
324 325 326
						out += '\ufffd';
						break;

A
Alex Dima 已提交
327
					case CharCode.CarriageReturn:
328 329 330 331 332
						// zero width space, because carriage return would introduce a line break
						out += '&#8203';
						break;

					default:
333 334
						if (renderControlCharacters && isControlCharacter(charCode)) {
							out += controlCharacterToPrintable(charCode);
335 336 337
						} else {
							out += lineText.charAt(charIndex);
						}
338 339
				}

J
Johannes Rieken 已提交
340
				charOffsetInPart++;
341 342 343

				if (charIndex >= charBreakIndex) {
					out += '&hellip;</span></span>';
344
					characterMapping.setPartData(charIndex, partIndex, charOffsetInPart);
A
Alex Dima 已提交
345
					return new RenderLineOutput(
346
						characterMapping,
347 348
						out,
						isWhitespaceOnly
A
Alex Dima 已提交
349
					);
350
				}
A
Alex Dima 已提交
351
			}
352 353

			out += '</span>';
A
Alex Dima 已提交
354
		}
355

356
	}
A
Alex Dima 已提交
357
	out += '</span>';
358 359 360

	// 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
361
	characterMapping.setPartData(lineTextLength, actualLineParts.length - 1, charOffsetInPart);
362

A
Alex Dima 已提交
363
	return new RenderLineOutput(
364
		characterMapping,
365 366
		out,
		isWhitespaceOnly
A
Alex Dima 已提交
367
	);
368
}