Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/viewLayout/viewLineRenderer.ts
5237 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as nls from '../../../nls.js';
7
import { CharCode } from '../../../base/common/charCode.js';
8
import * as strings from '../../../base/common/strings.js';
9
import { IViewLineTokens } from '../tokens/lineTokens.js';
10
import { StringBuilder } from '../core/stringBuilder.js';
11
import { LineDecoration, LineDecorationsNormalizer } from './lineDecorations.js';
12
import { LinePart, LinePartMetadata } from './linePart.js';
13
import { OffsetRange } from '../core/ranges/offsetRange.js';
14
import { InlineDecorationType } from '../viewModel/inlineDecorations.js';
15
import { TextDirection } from '../model.js';
16
17
export const enum RenderWhitespace {
18
None = 0,
19
Boundary = 1,
20
Selection = 2,
21
Trailing = 3,
22
All = 4
23
}
24
25
export interface IRenderLineInputOptions {
26
useMonospaceOptimizations: boolean;
27
canUseHalfwidthRightwardsArrow: boolean;
28
lineContent: string;
29
continuesWithWrappedLine: boolean;
30
isBasicASCII: boolean;
31
containsRTL: boolean;
32
fauxIndentLength: number;
33
lineTokens: IViewLineTokens;
34
lineDecorations: LineDecoration[];
35
tabSize: number;
36
startVisibleColumn: number;
37
spaceWidth: number;
38
middotWidth: number;
39
wsmiddotWidth: number;
40
stopRenderingLineAfter: number;
41
renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all';
42
renderControlCharacters: boolean;
43
fontLigatures: boolean;
44
selectionsOnLine: OffsetRange[] | null;
45
textDirection: TextDirection | null;
46
verticalScrollbarSize: number;
47
renderNewLineWhenEmpty: boolean;
48
}
49
50
export class RenderLineInput {
51
52
public readonly useMonospaceOptimizations: boolean;
53
public readonly canUseHalfwidthRightwardsArrow: boolean;
54
public readonly lineContent: string;
55
public readonly continuesWithWrappedLine: boolean;
56
public readonly isBasicASCII: boolean;
57
public readonly containsRTL: boolean;
58
public readonly fauxIndentLength: number;
59
public readonly lineTokens: IViewLineTokens;
60
public readonly lineDecorations: LineDecoration[];
61
public readonly tabSize: number;
62
public readonly startVisibleColumn: number;
63
public readonly spaceWidth: number;
64
public readonly renderSpaceWidth: number;
65
public readonly renderSpaceCharCode: number;
66
public readonly stopRenderingLineAfter: number;
67
public readonly renderWhitespace: RenderWhitespace;
68
public readonly renderControlCharacters: boolean;
69
public readonly fontLigatures: boolean;
70
public readonly textDirection: TextDirection | null;
71
public readonly verticalScrollbarSize: number;
72
73
/**
74
* Defined only when renderWhitespace is 'selection'. Selections are non-overlapping,
75
* and ordered by position within the line.
76
*/
77
public readonly selectionsOnLine: OffsetRange[] | null;
78
/**
79
* When rendering an empty line, whether to render a new line instead
80
*/
81
public readonly renderNewLineWhenEmpty: boolean;
82
83
public get isLTR(): boolean {
84
return !this.containsRTL && this.textDirection !== TextDirection.RTL;
85
}
86
87
constructor(
88
useMonospaceOptimizations: boolean,
89
canUseHalfwidthRightwardsArrow: boolean,
90
lineContent: string,
91
continuesWithWrappedLine: boolean,
92
isBasicASCII: boolean,
93
containsRTL: boolean,
94
fauxIndentLength: number,
95
lineTokens: IViewLineTokens,
96
lineDecorations: LineDecoration[],
97
tabSize: number,
98
startVisibleColumn: number,
99
spaceWidth: number,
100
middotWidth: number,
101
wsmiddotWidth: number,
102
stopRenderingLineAfter: number,
103
renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all',
104
renderControlCharacters: boolean,
105
fontLigatures: boolean,
106
selectionsOnLine: OffsetRange[] | null,
107
textDirection: TextDirection | null,
108
verticalScrollbarSize: number,
109
renderNewLineWhenEmpty: boolean = false,
110
) {
111
this.useMonospaceOptimizations = useMonospaceOptimizations;
112
this.canUseHalfwidthRightwardsArrow = canUseHalfwidthRightwardsArrow;
113
this.lineContent = lineContent;
114
this.continuesWithWrappedLine = continuesWithWrappedLine;
115
this.isBasicASCII = isBasicASCII;
116
this.containsRTL = containsRTL;
117
this.fauxIndentLength = fauxIndentLength;
118
this.lineTokens = lineTokens;
119
this.lineDecorations = lineDecorations.sort(LineDecoration.compare);
120
this.tabSize = tabSize;
121
this.startVisibleColumn = startVisibleColumn;
122
this.spaceWidth = spaceWidth;
123
this.stopRenderingLineAfter = stopRenderingLineAfter;
124
this.renderWhitespace = (
125
renderWhitespace === 'all'
126
? RenderWhitespace.All
127
: renderWhitespace === 'boundary'
128
? RenderWhitespace.Boundary
129
: renderWhitespace === 'selection'
130
? RenderWhitespace.Selection
131
: renderWhitespace === 'trailing'
132
? RenderWhitespace.Trailing
133
: RenderWhitespace.None
134
);
135
this.renderControlCharacters = renderControlCharacters;
136
this.fontLigatures = fontLigatures;
137
this.selectionsOnLine = selectionsOnLine && selectionsOnLine.sort((a, b) => a.start < b.start ? -1 : 1);
138
this.renderNewLineWhenEmpty = renderNewLineWhenEmpty;
139
this.textDirection = textDirection;
140
this.verticalScrollbarSize = verticalScrollbarSize;
141
142
const wsmiddotDiff = Math.abs(wsmiddotWidth - spaceWidth);
143
const middotDiff = Math.abs(middotWidth - spaceWidth);
144
if (wsmiddotDiff < middotDiff) {
145
this.renderSpaceWidth = wsmiddotWidth;
146
this.renderSpaceCharCode = 0x2E31; // U+2E31 - WORD SEPARATOR MIDDLE DOT
147
} else {
148
this.renderSpaceWidth = middotWidth;
149
this.renderSpaceCharCode = 0xB7; // U+00B7 - MIDDLE DOT
150
}
151
}
152
153
private sameSelection(otherSelections: OffsetRange[] | null): boolean {
154
if (this.selectionsOnLine === null) {
155
return otherSelections === null;
156
}
157
158
if (otherSelections === null) {
159
return false;
160
}
161
162
if (otherSelections.length !== this.selectionsOnLine.length) {
163
return false;
164
}
165
166
for (let i = 0; i < this.selectionsOnLine.length; i++) {
167
if (!this.selectionsOnLine[i].equals(otherSelections[i])) {
168
return false;
169
}
170
}
171
172
return true;
173
}
174
175
public equals(other: RenderLineInput): boolean {
176
return (
177
this.useMonospaceOptimizations === other.useMonospaceOptimizations
178
&& this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow
179
&& this.lineContent === other.lineContent
180
&& this.continuesWithWrappedLine === other.continuesWithWrappedLine
181
&& this.isBasicASCII === other.isBasicASCII
182
&& this.containsRTL === other.containsRTL
183
&& this.fauxIndentLength === other.fauxIndentLength
184
&& this.tabSize === other.tabSize
185
&& this.startVisibleColumn === other.startVisibleColumn
186
&& this.spaceWidth === other.spaceWidth
187
&& this.renderSpaceWidth === other.renderSpaceWidth
188
&& this.renderSpaceCharCode === other.renderSpaceCharCode
189
&& this.stopRenderingLineAfter === other.stopRenderingLineAfter
190
&& this.renderWhitespace === other.renderWhitespace
191
&& this.renderControlCharacters === other.renderControlCharacters
192
&& this.fontLigatures === other.fontLigatures
193
&& LineDecoration.equalsArr(this.lineDecorations, other.lineDecorations)
194
&& this.lineTokens.equals(other.lineTokens)
195
&& this.sameSelection(other.selectionsOnLine)
196
&& this.textDirection === other.textDirection
197
&& this.verticalScrollbarSize === other.verticalScrollbarSize
198
&& this.renderNewLineWhenEmpty === other.renderNewLineWhenEmpty
199
);
200
}
201
}
202
203
const enum CharacterMappingConstants {
204
PART_INDEX_MASK = 0b11111111111111110000000000000000,
205
CHAR_INDEX_MASK = 0b00000000000000001111111111111111,
206
207
CHAR_INDEX_OFFSET = 0,
208
PART_INDEX_OFFSET = 16
209
}
210
211
export class DomPosition {
212
constructor(
213
public readonly partIndex: number,
214
public readonly charIndex: number
215
) { }
216
}
217
218
/**
219
* Provides a both direction mapping between a line's character and its rendered position.
220
*/
221
export class CharacterMapping {
222
223
private static getPartIndex(partData: number): number {
224
return (partData & CharacterMappingConstants.PART_INDEX_MASK) >>> CharacterMappingConstants.PART_INDEX_OFFSET;
225
}
226
227
private static getCharIndex(partData: number): number {
228
return (partData & CharacterMappingConstants.CHAR_INDEX_MASK) >>> CharacterMappingConstants.CHAR_INDEX_OFFSET;
229
}
230
231
public readonly length: number;
232
private readonly _data: Uint32Array;
233
private readonly _horizontalOffset: Uint32Array;
234
235
constructor(length: number, partCount: number) {
236
this.length = length;
237
this._data = new Uint32Array(this.length);
238
this._horizontalOffset = new Uint32Array(this.length);
239
}
240
241
public setColumnInfo(column: number, partIndex: number, charIndex: number, horizontalOffset: number): void {
242
const partData = (
243
(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)
244
| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)
245
) >>> 0;
246
this._data[column - 1] = partData;
247
this._horizontalOffset[column - 1] = horizontalOffset;
248
}
249
250
public getHorizontalOffset(column: number): number {
251
if (this._horizontalOffset.length === 0) {
252
// No characters on this line
253
return 0;
254
}
255
return this._horizontalOffset[column - 1];
256
}
257
258
private charOffsetToPartData(charOffset: number): number {
259
if (this.length === 0) {
260
return 0;
261
}
262
if (charOffset < 0) {
263
return this._data[0];
264
}
265
if (charOffset >= this.length) {
266
return this._data[this.length - 1];
267
}
268
return this._data[charOffset];
269
}
270
271
public getDomPosition(column: number): DomPosition {
272
const partData = this.charOffsetToPartData(column - 1);
273
const partIndex = CharacterMapping.getPartIndex(partData);
274
const charIndex = CharacterMapping.getCharIndex(partData);
275
return new DomPosition(partIndex, charIndex);
276
}
277
278
public getColumn(domPosition: DomPosition, partLength: number): number {
279
const charOffset = this.partDataToCharOffset(domPosition.partIndex, partLength, domPosition.charIndex);
280
return charOffset + 1;
281
}
282
283
private partDataToCharOffset(partIndex: number, partLength: number, charIndex: number): number {
284
if (this.length === 0) {
285
return 0;
286
}
287
288
const searchEntry = (
289
(partIndex << CharacterMappingConstants.PART_INDEX_OFFSET)
290
| (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET)
291
) >>> 0;
292
293
let min = 0;
294
let max = this.length - 1;
295
while (min + 1 < max) {
296
const mid = ((min + max) >>> 1);
297
const midEntry = this._data[mid];
298
if (midEntry === searchEntry) {
299
return mid;
300
} else if (midEntry > searchEntry) {
301
max = mid;
302
} else {
303
min = mid;
304
}
305
}
306
307
if (min === max) {
308
return min;
309
}
310
311
const minEntry = this._data[min];
312
const maxEntry = this._data[max];
313
314
if (minEntry === searchEntry) {
315
return min;
316
}
317
if (maxEntry === searchEntry) {
318
return max;
319
}
320
321
const minPartIndex = CharacterMapping.getPartIndex(minEntry);
322
const minCharIndex = CharacterMapping.getCharIndex(minEntry);
323
324
const maxPartIndex = CharacterMapping.getPartIndex(maxEntry);
325
let maxCharIndex: number;
326
327
if (minPartIndex !== maxPartIndex) {
328
// sitting between parts
329
maxCharIndex = partLength;
330
} else {
331
maxCharIndex = CharacterMapping.getCharIndex(maxEntry);
332
}
333
334
const minEntryDistance = charIndex - minCharIndex;
335
const maxEntryDistance = maxCharIndex - charIndex;
336
337
if (minEntryDistance <= maxEntryDistance) {
338
return min;
339
}
340
return max;
341
}
342
343
public inflate() {
344
const result: [number, number, number][] = [];
345
for (let i = 0; i < this.length; i++) {
346
const partData = this._data[i];
347
const partIndex = CharacterMapping.getPartIndex(partData);
348
const charIndex = CharacterMapping.getCharIndex(partData);
349
const visibleColumn = this._horizontalOffset[i];
350
result.push([partIndex, charIndex, visibleColumn]);
351
}
352
return result;
353
}
354
}
355
356
export const enum ForeignElementType {
357
None = 0,
358
Before = 1,
359
After = 2
360
}
361
362
export class RenderLineOutput {
363
_renderLineOutputBrand: void = undefined;
364
365
readonly characterMapping: CharacterMapping;
366
readonly containsForeignElements: ForeignElementType;
367
368
constructor(characterMapping: CharacterMapping, containsForeignElements: ForeignElementType) {
369
this.characterMapping = characterMapping;
370
this.containsForeignElements = containsForeignElements;
371
}
372
}
373
374
export function renderViewLine(input: RenderLineInput, sb: StringBuilder): RenderLineOutput {
375
if (input.lineContent.length === 0) {
376
377
if (input.lineDecorations.length > 0) {
378
// This line is empty, but it contains inline decorations
379
sb.appendString(`<span>`);
380
381
let beforeCount = 0;
382
let afterCount = 0;
383
let containsForeignElements = ForeignElementType.None;
384
for (const lineDecoration of input.lineDecorations) {
385
if (lineDecoration.type === InlineDecorationType.Before || lineDecoration.type === InlineDecorationType.After) {
386
sb.appendString(`<span class="`);
387
sb.appendString(lineDecoration.className);
388
sb.appendString(`"></span>`);
389
390
if (lineDecoration.type === InlineDecorationType.Before) {
391
containsForeignElements |= ForeignElementType.Before;
392
beforeCount++;
393
}
394
if (lineDecoration.type === InlineDecorationType.After) {
395
containsForeignElements |= ForeignElementType.After;
396
afterCount++;
397
}
398
}
399
}
400
401
sb.appendString(`</span>`);
402
403
const characterMapping = new CharacterMapping(1, beforeCount + afterCount);
404
characterMapping.setColumnInfo(1, beforeCount, 0, 0);
405
406
return new RenderLineOutput(
407
characterMapping,
408
containsForeignElements
409
);
410
}
411
412
// completely empty line
413
if (input.renderNewLineWhenEmpty) {
414
sb.appendString('<span><span>\n</span></span>');
415
} else {
416
sb.appendString('<span><span></span></span>');
417
}
418
return new RenderLineOutput(
419
new CharacterMapping(0, 0),
420
ForeignElementType.None
421
);
422
}
423
424
return _renderLine(resolveRenderLineInput(input), sb);
425
}
426
427
export class RenderLineOutput2 {
428
constructor(
429
public readonly characterMapping: CharacterMapping,
430
public readonly html: string,
431
public readonly containsForeignElements: ForeignElementType
432
) {
433
}
434
}
435
436
export function renderViewLine2(input: RenderLineInput): RenderLineOutput2 {
437
const sb = new StringBuilder(10000);
438
const out = renderViewLine(input, sb);
439
return new RenderLineOutput2(out.characterMapping, sb.build(), out.containsForeignElements);
440
}
441
442
class ResolvedRenderLineInput {
443
constructor(
444
public readonly fontIsMonospace: boolean,
445
public readonly canUseHalfwidthRightwardsArrow: boolean,
446
public readonly lineContent: string,
447
public readonly len: number,
448
public readonly isOverflowing: boolean,
449
public readonly overflowingCharCount: number,
450
public readonly parts: LinePart[],
451
public readonly containsForeignElements: ForeignElementType,
452
public readonly fauxIndentLength: number,
453
public readonly tabSize: number,
454
public readonly startVisibleColumn: number,
455
public readonly spaceWidth: number,
456
public readonly renderSpaceCharCode: number,
457
public readonly renderWhitespace: RenderWhitespace,
458
public readonly renderControlCharacters: boolean,
459
) {
460
//
461
}
462
}
463
464
function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput {
465
const lineContent = input.lineContent;
466
467
let isOverflowing: boolean;
468
let overflowingCharCount: number;
469
let len: number;
470
471
if (input.stopRenderingLineAfter !== -1 && input.stopRenderingLineAfter < lineContent.length) {
472
isOverflowing = true;
473
overflowingCharCount = lineContent.length - input.stopRenderingLineAfter;
474
len = input.stopRenderingLineAfter;
475
} else {
476
isOverflowing = false;
477
overflowingCharCount = 0;
478
len = lineContent.length;
479
}
480
481
let tokens = transformAndRemoveOverflowing(lineContent, input.containsRTL, input.lineTokens, input.fauxIndentLength, len);
482
if (input.renderControlCharacters && !input.isBasicASCII) {
483
// Calling `extractControlCharacters` before adding (possibly empty) line parts
484
// for inline decorations. `extractControlCharacters` removes empty line parts.
485
tokens = extractControlCharacters(lineContent, tokens);
486
}
487
if (input.renderWhitespace === RenderWhitespace.All ||
488
input.renderWhitespace === RenderWhitespace.Boundary ||
489
(input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine) ||
490
(input.renderWhitespace === RenderWhitespace.Trailing && !input.continuesWithWrappedLine)
491
) {
492
tokens = _applyRenderWhitespace(input, lineContent, len, tokens);
493
}
494
let containsForeignElements = ForeignElementType.None;
495
if (input.lineDecorations.length > 0) {
496
for (let i = 0, len = input.lineDecorations.length; i < len; i++) {
497
const lineDecoration = input.lineDecorations[i];
498
if (lineDecoration.type === InlineDecorationType.RegularAffectingLetterSpacing) {
499
// Pretend there are foreign elements... although not 100% accurate.
500
containsForeignElements |= ForeignElementType.Before;
501
} else if (lineDecoration.type === InlineDecorationType.Before) {
502
containsForeignElements |= ForeignElementType.Before;
503
} else if (lineDecoration.type === InlineDecorationType.After) {
504
containsForeignElements |= ForeignElementType.After;
505
}
506
}
507
tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations);
508
}
509
if (!input.containsRTL) {
510
// We can never split RTL text, as it ruins the rendering
511
tokens = splitLargeTokens(lineContent, tokens, !input.isBasicASCII || input.fontLigatures);
512
} else {
513
// Split the first token if it contains both leading whitespace and RTL text
514
tokens = splitLeadingWhitespaceFromRTL(lineContent, tokens);
515
}
516
517
return new ResolvedRenderLineInput(
518
input.useMonospaceOptimizations,
519
input.canUseHalfwidthRightwardsArrow,
520
lineContent,
521
len,
522
isOverflowing,
523
overflowingCharCount,
524
tokens,
525
containsForeignElements,
526
input.fauxIndentLength,
527
input.tabSize,
528
input.startVisibleColumn,
529
input.spaceWidth,
530
input.renderSpaceCharCode,
531
input.renderWhitespace,
532
input.renderControlCharacters
533
);
534
}
535
536
/**
537
* In the rendering phase, characters are always looped until token.endIndex.
538
* Ensure that all tokens end before `len` and the last one ends precisely at `len`.
539
*/
540
function transformAndRemoveOverflowing(lineContent: string, lineContainsRTL: boolean, tokens: IViewLineTokens, fauxIndentLength: number, len: number): LinePart[] {
541
const result: LinePart[] = [];
542
let resultLen = 0;
543
544
// The faux indent part of the line should have no token type
545
if (fauxIndentLength > 0) {
546
result[resultLen++] = new LinePart(fauxIndentLength, '', 0, false);
547
}
548
let startOffset = fauxIndentLength;
549
for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {
550
const endIndex = tokens.getEndOffset(tokenIndex);
551
if (endIndex <= fauxIndentLength) {
552
// The faux indent part of the line should have no token type
553
continue;
554
}
555
const type = tokens.getClassName(tokenIndex);
556
if (endIndex >= len) {
557
const tokenContainsRTL = (lineContainsRTL ? strings.containsRTL(lineContent.substring(startOffset, len)) : false);
558
result[resultLen++] = new LinePart(len, type, 0, tokenContainsRTL);
559
break;
560
}
561
const tokenContainsRTL = (lineContainsRTL ? strings.containsRTL(lineContent.substring(startOffset, endIndex)) : false);
562
result[resultLen++] = new LinePart(endIndex, type, 0, tokenContainsRTL);
563
startOffset = endIndex;
564
}
565
566
return result;
567
}
568
569
/**
570
* written as a const enum to get value inlining.
571
*/
572
const enum Constants {
573
LongToken = 50
574
}
575
576
/**
577
* See https://github.com/microsoft/vscode/issues/6885.
578
* It appears that having very large spans causes very slow reading of character positions.
579
* So here we try to avoid that.
580
*/
581
function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: boolean): LinePart[] {
582
let lastTokenEndIndex = 0;
583
const result: LinePart[] = [];
584
let resultLen = 0;
585
586
if (onlyAtSpaces) {
587
// Split only at spaces => we need to walk each character
588
for (let i = 0, len = tokens.length; i < len; i++) {
589
const token = tokens[i];
590
const tokenEndIndex = token.endIndex;
591
if (lastTokenEndIndex + Constants.LongToken < tokenEndIndex) {
592
const tokenType = token.type;
593
const tokenMetadata = token.metadata;
594
const tokenContainsRTL = token.containsRTL;
595
596
let lastSpaceOffset = -1;
597
let currTokenStart = lastTokenEndIndex;
598
for (let j = lastTokenEndIndex; j < tokenEndIndex; j++) {
599
if (lineContent.charCodeAt(j) === CharCode.Space) {
600
lastSpaceOffset = j;
601
}
602
if (lastSpaceOffset !== -1 && j - currTokenStart >= Constants.LongToken) {
603
// Split at `lastSpaceOffset` + 1
604
result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType, tokenMetadata, tokenContainsRTL);
605
currTokenStart = lastSpaceOffset + 1;
606
lastSpaceOffset = -1;
607
}
608
}
609
if (currTokenStart !== tokenEndIndex) {
610
result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL);
611
}
612
} else {
613
result[resultLen++] = token;
614
}
615
616
lastTokenEndIndex = tokenEndIndex;
617
}
618
} else {
619
// Split anywhere => we don't need to walk each character
620
for (let i = 0, len = tokens.length; i < len; i++) {
621
const token = tokens[i];
622
const tokenEndIndex = token.endIndex;
623
const diff = (tokenEndIndex - lastTokenEndIndex);
624
if (diff > Constants.LongToken) {
625
const tokenType = token.type;
626
const tokenMetadata = token.metadata;
627
const tokenContainsRTL = token.containsRTL;
628
const piecesCount = Math.ceil(diff / Constants.LongToken);
629
for (let j = 1; j < piecesCount; j++) {
630
const pieceEndIndex = lastTokenEndIndex + (j * Constants.LongToken);
631
result[resultLen++] = new LinePart(pieceEndIndex, tokenType, tokenMetadata, tokenContainsRTL);
632
}
633
result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL);
634
} else {
635
result[resultLen++] = token;
636
}
637
lastTokenEndIndex = tokenEndIndex;
638
}
639
}
640
641
return result;
642
}
643
644
/**
645
* Splits leading whitespace from the first token if it contains RTL text.
646
*/
647
function splitLeadingWhitespaceFromRTL(lineContent: string, tokens: LinePart[]): LinePart[] {
648
if (tokens.length === 0) {
649
return tokens;
650
}
651
652
const firstToken = tokens[0];
653
if (!firstToken.containsRTL) {
654
return tokens;
655
}
656
657
// Check if the first token starts with whitespace
658
const firstTokenEndIndex = firstToken.endIndex;
659
let firstNonWhitespaceIndex = 0;
660
for (let i = 0; i < firstTokenEndIndex; i++) {
661
const charCode = lineContent.charCodeAt(i);
662
if (charCode !== CharCode.Space && charCode !== CharCode.Tab) {
663
firstNonWhitespaceIndex = i;
664
break;
665
}
666
}
667
668
if (firstNonWhitespaceIndex === 0) {
669
// No leading whitespace
670
return tokens;
671
}
672
673
// Split the first token into leading whitespace and the rest
674
const result: LinePart[] = [];
675
result.push(new LinePart(firstNonWhitespaceIndex, firstToken.type, firstToken.metadata, false));
676
result.push(new LinePart(firstTokenEndIndex, firstToken.type, firstToken.metadata, firstToken.containsRTL));
677
678
// Add remaining tokens
679
for (let i = 1; i < tokens.length; i++) {
680
result.push(tokens[i]);
681
}
682
683
return result;
684
}
685
686
function isControlCharacter(charCode: number): boolean {
687
if (charCode < 32) {
688
return (charCode !== CharCode.Tab);
689
}
690
if (charCode === 127) {
691
// DEL
692
return true;
693
}
694
695
if (
696
(charCode >= 0x202A && charCode <= 0x202E)
697
|| (charCode >= 0x2066 && charCode <= 0x2069)
698
|| (charCode >= 0x200E && charCode <= 0x200F)
699
|| charCode === 0x061C
700
) {
701
// Unicode Directional Formatting Characters
702
// LRE U+202A LEFT-TO-RIGHT EMBEDDING
703
// RLE U+202B RIGHT-TO-LEFT EMBEDDING
704
// PDF U+202C POP DIRECTIONAL FORMATTING
705
// LRO U+202D LEFT-TO-RIGHT OVERRIDE
706
// RLO U+202E RIGHT-TO-LEFT OVERRIDE
707
// LRI U+2066 LEFT-TO-RIGHT ISOLATE
708
// RLI U+2067 RIGHT-TO-LEFT ISOLATE
709
// FSI U+2068 FIRST STRONG ISOLATE
710
// PDI U+2069 POP DIRECTIONAL ISOLATE
711
// LRM U+200E LEFT-TO-RIGHT MARK
712
// RLM U+200F RIGHT-TO-LEFT MARK
713
// ALM U+061C ARABIC LETTER MARK
714
return true;
715
}
716
717
return false;
718
}
719
720
function extractControlCharacters(lineContent: string, tokens: LinePart[]): LinePart[] {
721
const result: LinePart[] = [];
722
let lastLinePart: LinePart = new LinePart(0, '', 0, false);
723
let charOffset = 0;
724
for (const token of tokens) {
725
const tokenEndIndex = token.endIndex;
726
for (; charOffset < tokenEndIndex; charOffset++) {
727
const charCode = lineContent.charCodeAt(charOffset);
728
if (isControlCharacter(charCode)) {
729
if (charOffset > lastLinePart.endIndex) {
730
// emit previous part if it has text
731
lastLinePart = new LinePart(charOffset, token.type, token.metadata, token.containsRTL);
732
result.push(lastLinePart);
733
}
734
lastLinePart = new LinePart(charOffset + 1, 'mtkcontrol', token.metadata, false);
735
result.push(lastLinePart);
736
}
737
}
738
if (charOffset > lastLinePart.endIndex) {
739
// emit previous part if it has text
740
lastLinePart = new LinePart(tokenEndIndex, token.type, token.metadata, token.containsRTL);
741
result.push(lastLinePart);
742
}
743
}
744
return result;
745
}
746
747
/**
748
* Whitespace is rendered by "replacing" tokens with a special-purpose `mtkw` type that is later recognized in the rendering phase.
749
* 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;.
750
* The rendering phase will generate `style="width:..."` for these tokens.
751
*/
752
function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len: number, tokens: LinePart[]): LinePart[] {
753
754
const continuesWithWrappedLine = input.continuesWithWrappedLine;
755
const fauxIndentLength = input.fauxIndentLength;
756
const tabSize = input.tabSize;
757
const startVisibleColumn = input.startVisibleColumn;
758
const useMonospaceOptimizations = input.useMonospaceOptimizations;
759
const selections = input.selectionsOnLine;
760
const onlyBoundary = (input.renderWhitespace === RenderWhitespace.Boundary);
761
const onlyTrailing = (input.renderWhitespace === RenderWhitespace.Trailing);
762
const generateLinePartForEachWhitespace = (input.renderSpaceWidth !== input.spaceWidth);
763
764
const result: LinePart[] = [];
765
let resultLen = 0;
766
let tokenIndex = 0;
767
let tokenType = tokens[tokenIndex].type;
768
let tokenContainsRTL = tokens[tokenIndex].containsRTL;
769
let tokenEndIndex = tokens[tokenIndex].endIndex;
770
const tokensLength = tokens.length;
771
772
let lineIsEmptyOrWhitespace = false;
773
let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
774
let lastNonWhitespaceIndex: number;
775
if (firstNonWhitespaceIndex === -1) {
776
lineIsEmptyOrWhitespace = true;
777
firstNonWhitespaceIndex = len;
778
lastNonWhitespaceIndex = len;
779
} else {
780
lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent);
781
}
782
783
let wasInWhitespace = false;
784
let currentSelectionIndex = 0;
785
let currentSelection = selections && selections[currentSelectionIndex];
786
let tmpIndent = startVisibleColumn % tabSize;
787
for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) {
788
const chCode = lineContent.charCodeAt(charIndex);
789
790
if (currentSelection && currentSelection.endExclusive <= charIndex) {
791
currentSelectionIndex++;
792
currentSelection = selections && selections[currentSelectionIndex];
793
}
794
795
let isInWhitespace: boolean;
796
if (charIndex < firstNonWhitespaceIndex || charIndex > lastNonWhitespaceIndex) {
797
// in leading or trailing whitespace
798
isInWhitespace = true;
799
} else if (chCode === CharCode.Tab) {
800
// a tab character is rendered both in all and boundary cases
801
isInWhitespace = true;
802
} else if (chCode === CharCode.Space) {
803
// hit a space character
804
if (onlyBoundary) {
805
// rendering only boundary whitespace
806
if (wasInWhitespace) {
807
isInWhitespace = true;
808
} else {
809
const nextChCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null);
810
isInWhitespace = (nextChCode === CharCode.Space || nextChCode === CharCode.Tab);
811
}
812
} else {
813
isInWhitespace = true;
814
}
815
} else {
816
isInWhitespace = false;
817
}
818
819
// If rendering whitespace on selection, check that the charIndex falls within a selection
820
if (isInWhitespace && selections) {
821
isInWhitespace = !!currentSelection && currentSelection.start <= charIndex && charIndex < currentSelection.endExclusive;
822
}
823
824
// If rendering only trailing whitespace, check that the charIndex points to trailing whitespace.
825
if (isInWhitespace && onlyTrailing) {
826
isInWhitespace = lineIsEmptyOrWhitespace || charIndex > lastNonWhitespaceIndex;
827
}
828
829
if (isInWhitespace && tokenContainsRTL) {
830
// If the token contains RTL text, breaking it up into multiple line parts
831
// to render whitespace might affect the browser's bidi layout.
832
//
833
// We render whitespace in such tokens only if the whitespace
834
// is the leading or the trailing whitespace of the line,
835
// which doesn't affect the browser's bidi layout.
836
if (charIndex >= firstNonWhitespaceIndex && charIndex <= lastNonWhitespaceIndex) {
837
isInWhitespace = false;
838
}
839
}
840
841
if (wasInWhitespace) {
842
// was in whitespace token
843
if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) {
844
// leaving whitespace token or entering a new indent
845
if (generateLinePartForEachWhitespace) {
846
const lastEndIndex = (resultLen > 0 ? result[resultLen - 1].endIndex : fauxIndentLength);
847
for (let i = lastEndIndex + 1; i <= charIndex; i++) {
848
result[resultLen++] = new LinePart(i, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);
849
}
850
} else {
851
result[resultLen++] = new LinePart(charIndex, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);
852
}
853
tmpIndent = tmpIndent % tabSize;
854
}
855
} else {
856
// was in regular token
857
if (charIndex === tokenEndIndex || (isInWhitespace && charIndex > fauxIndentLength)) {
858
result[resultLen++] = new LinePart(charIndex, tokenType, 0, tokenContainsRTL);
859
tmpIndent = tmpIndent % tabSize;
860
}
861
}
862
863
if (chCode === CharCode.Tab) {
864
tmpIndent = tabSize;
865
} else if (strings.isFullWidthCharacter(chCode)) {
866
tmpIndent += 2;
867
} else {
868
tmpIndent++;
869
}
870
871
wasInWhitespace = isInWhitespace;
872
873
while (charIndex === tokenEndIndex) {
874
tokenIndex++;
875
if (tokenIndex < tokensLength) {
876
tokenType = tokens[tokenIndex].type;
877
tokenContainsRTL = tokens[tokenIndex].containsRTL;
878
tokenEndIndex = tokens[tokenIndex].endIndex;
879
} else {
880
break;
881
}
882
}
883
}
884
885
let generateWhitespace = false;
886
if (wasInWhitespace) {
887
// was in whitespace token
888
if (continuesWithWrappedLine && onlyBoundary) {
889
const lastCharCode = (len > 0 ? lineContent.charCodeAt(len - 1) : CharCode.Null);
890
const prevCharCode = (len > 1 ? lineContent.charCodeAt(len - 2) : CharCode.Null);
891
const isSingleTrailingSpace = (lastCharCode === CharCode.Space && (prevCharCode !== CharCode.Space && prevCharCode !== CharCode.Tab));
892
if (!isSingleTrailingSpace) {
893
generateWhitespace = true;
894
}
895
} else {
896
generateWhitespace = true;
897
}
898
}
899
900
if (generateWhitespace) {
901
if (generateLinePartForEachWhitespace) {
902
const lastEndIndex = (resultLen > 0 ? result[resultLen - 1].endIndex : fauxIndentLength);
903
for (let i = lastEndIndex + 1; i <= len; i++) {
904
result[resultLen++] = new LinePart(i, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);
905
}
906
} else {
907
result[resultLen++] = new LinePart(len, 'mtkw', LinePartMetadata.IS_WHITESPACE, false);
908
}
909
} else {
910
result[resultLen++] = new LinePart(len, tokenType, 0, tokenContainsRTL);
911
}
912
913
return result;
914
}
915
916
/**
917
* Inline decorations are "merged" on top of tokens.
918
* Special care must be taken when multiple inline decorations are at play and they overlap.
919
*/
920
function _applyInlineDecorations(lineContent: string, len: number, tokens: LinePart[], _lineDecorations: LineDecoration[]): LinePart[] {
921
_lineDecorations.sort(LineDecoration.compare);
922
const lineDecorations = LineDecorationsNormalizer.normalize(lineContent, _lineDecorations);
923
const lineDecorationsLen = lineDecorations.length;
924
925
let lineDecorationIndex = 0;
926
const result: LinePart[] = [];
927
let resultLen = 0;
928
let lastResultEndIndex = 0;
929
for (let tokenIndex = 0, len = tokens.length; tokenIndex < len; tokenIndex++) {
930
const token = tokens[tokenIndex];
931
const tokenEndIndex = token.endIndex;
932
const tokenType = token.type;
933
const tokenMetadata = token.metadata;
934
const tokenContainsRTL = token.containsRTL;
935
936
while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset < tokenEndIndex) {
937
const lineDecoration = lineDecorations[lineDecorationIndex];
938
939
if (lineDecoration.startOffset > lastResultEndIndex) {
940
lastResultEndIndex = lineDecoration.startOffset;
941
result[resultLen++] = new LinePart(lastResultEndIndex, tokenType, tokenMetadata, tokenContainsRTL);
942
}
943
944
if (lineDecoration.endOffset + 1 <= tokenEndIndex) {
945
// This line decoration ends before this token ends
946
lastResultEndIndex = lineDecoration.endOffset + 1;
947
result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL);
948
lineDecorationIndex++;
949
} else {
950
// This line decoration continues on to the next token
951
lastResultEndIndex = tokenEndIndex;
952
result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL);
953
break;
954
}
955
}
956
957
if (tokenEndIndex > lastResultEndIndex) {
958
lastResultEndIndex = tokenEndIndex;
959
result[resultLen++] = new LinePart(lastResultEndIndex, tokenType, tokenMetadata, tokenContainsRTL);
960
}
961
}
962
963
const lastTokenEndIndex = tokens[tokens.length - 1].endIndex;
964
if (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) {
965
while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) {
966
const lineDecoration = lineDecorations[lineDecorationIndex];
967
result[resultLen++] = new LinePart(lastResultEndIndex, lineDecoration.className, lineDecoration.metadata, false);
968
lineDecorationIndex++;
969
}
970
}
971
972
return result;
973
}
974
975
/**
976
* This function is on purpose not split up into multiple functions to allow runtime type inference (i.e. performance reasons).
977
* Notice how all the needed data is fully resolved and passed in (i.e. no other calls).
978
*/
979
function _renderLine(input: ResolvedRenderLineInput, sb: StringBuilder): RenderLineOutput {
980
const fontIsMonospace = input.fontIsMonospace;
981
const canUseHalfwidthRightwardsArrow = input.canUseHalfwidthRightwardsArrow;
982
const containsForeignElements = input.containsForeignElements;
983
const lineContent = input.lineContent;
984
const len = input.len;
985
const isOverflowing = input.isOverflowing;
986
const overflowingCharCount = input.overflowingCharCount;
987
const parts = input.parts;
988
const fauxIndentLength = input.fauxIndentLength;
989
const tabSize = input.tabSize;
990
const startVisibleColumn = input.startVisibleColumn;
991
const spaceWidth = input.spaceWidth;
992
const renderSpaceCharCode = input.renderSpaceCharCode;
993
const renderWhitespace = input.renderWhitespace;
994
const renderControlCharacters = input.renderControlCharacters;
995
996
const characterMapping = new CharacterMapping(len + 1, parts.length);
997
let lastCharacterMappingDefined = false;
998
999
let charIndex = 0;
1000
let visibleColumn = startVisibleColumn;
1001
let charOffsetInPart = 0; // the character offset in the current part
1002
let charHorizontalOffset = 0; // the character horizontal position in terms of chars relative to line start
1003
1004
let partDisplacement = 0;
1005
1006
sb.appendString('<span>');
1007
1008
for (let partIndex = 0, tokensLen = parts.length; partIndex < tokensLen; partIndex++) {
1009
1010
const part = parts[partIndex];
1011
const partEndIndex = part.endIndex;
1012
const partType = part.type;
1013
const partContainsRTL = part.containsRTL;
1014
const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && part.isWhitespace());
1015
const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements);
1016
const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.isPseudoAfter());
1017
charOffsetInPart = 0;
1018
1019
sb.appendString('<span ');
1020
if (partContainsRTL) {
1021
sb.appendString('style="unicode-bidi:isolate" ');
1022
}
1023
sb.appendString('class="');
1024
sb.appendString(partRendersWhitespaceWithWidth ? 'mtkz' : partType);
1025
sb.appendASCIICharCode(CharCode.DoubleQuote);
1026
1027
if (partRendersWhitespace) {
1028
1029
let partWidth = 0;
1030
{
1031
let _charIndex = charIndex;
1032
let _visibleColumn = visibleColumn;
1033
1034
for (; _charIndex < partEndIndex; _charIndex++) {
1035
const charCode = lineContent.charCodeAt(_charIndex);
1036
const charWidth = (charCode === CharCode.Tab ? (tabSize - (_visibleColumn % tabSize)) : 1) | 0;
1037
partWidth += charWidth;
1038
if (_charIndex >= fauxIndentLength) {
1039
_visibleColumn += charWidth;
1040
}
1041
}
1042
}
1043
1044
if (partRendersWhitespaceWithWidth) {
1045
sb.appendString(' style="width:');
1046
sb.appendString(String(spaceWidth * partWidth));
1047
sb.appendString('px"');
1048
}
1049
sb.appendASCIICharCode(CharCode.GreaterThan);
1050
1051
for (; charIndex < partEndIndex; charIndex++) {
1052
characterMapping.setColumnInfo(charIndex + 1, partIndex - partDisplacement, charOffsetInPart, charHorizontalOffset);
1053
partDisplacement = 0;
1054
const charCode = lineContent.charCodeAt(charIndex);
1055
1056
let producedCharacters: number;
1057
let charWidth: number;
1058
1059
if (charCode === CharCode.Tab) {
1060
producedCharacters = (tabSize - (visibleColumn % tabSize)) | 0;
1061
charWidth = producedCharacters;
1062
1063
if (!canUseHalfwidthRightwardsArrow || charWidth > 1) {
1064
sb.appendCharCode(0x2192); // RIGHTWARDS ARROW
1065
} else {
1066
sb.appendCharCode(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW
1067
}
1068
for (let space = 2; space <= charWidth; space++) {
1069
sb.appendCharCode(0xA0); // &nbsp;
1070
}
1071
1072
} else { // must be CharCode.Space
1073
producedCharacters = 2;
1074
charWidth = 1;
1075
1076
sb.appendCharCode(renderSpaceCharCode); // &middot; or word separator middle dot
1077
sb.appendCharCode(0x200C); // ZERO WIDTH NON-JOINER
1078
}
1079
1080
charOffsetInPart += producedCharacters;
1081
charHorizontalOffset += charWidth;
1082
if (charIndex >= fauxIndentLength) {
1083
visibleColumn += charWidth;
1084
}
1085
}
1086
1087
} else {
1088
1089
sb.appendASCIICharCode(CharCode.GreaterThan);
1090
1091
for (; charIndex < partEndIndex; charIndex++) {
1092
characterMapping.setColumnInfo(charIndex + 1, partIndex - partDisplacement, charOffsetInPart, charHorizontalOffset);
1093
partDisplacement = 0;
1094
const charCode = lineContent.charCodeAt(charIndex);
1095
1096
let producedCharacters = 1;
1097
let charWidth = 1;
1098
1099
switch (charCode) {
1100
case CharCode.Tab:
1101
producedCharacters = (tabSize - (visibleColumn % tabSize));
1102
charWidth = producedCharacters;
1103
for (let space = 1; space <= producedCharacters; space++) {
1104
sb.appendCharCode(0xA0); // &nbsp;
1105
}
1106
break;
1107
1108
case CharCode.Space:
1109
sb.appendCharCode(0xA0); // &nbsp;
1110
break;
1111
1112
case CharCode.LessThan:
1113
sb.appendString('&lt;');
1114
break;
1115
1116
case CharCode.GreaterThan:
1117
sb.appendString('&gt;');
1118
break;
1119
1120
case CharCode.Ampersand:
1121
sb.appendString('&amp;');
1122
break;
1123
1124
case CharCode.Null:
1125
if (renderControlCharacters) {
1126
// See https://unicode-table.com/en/blocks/control-pictures/
1127
sb.appendCharCode(9216);
1128
} else {
1129
sb.appendString('&#00;');
1130
}
1131
break;
1132
1133
case CharCode.UTF8_BOM:
1134
case CharCode.LINE_SEPARATOR:
1135
case CharCode.PARAGRAPH_SEPARATOR:
1136
case CharCode.NEXT_LINE:
1137
sb.appendCharCode(0xFFFD);
1138
break;
1139
1140
default:
1141
if (strings.isFullWidthCharacter(charCode)) {
1142
charWidth++;
1143
}
1144
// See https://unicode-table.com/en/blocks/control-pictures/
1145
if (renderControlCharacters && charCode < 32) {
1146
sb.appendCharCode(9216 + charCode);
1147
} else if (renderControlCharacters && charCode === 127) {
1148
// DEL
1149
sb.appendCharCode(9249);
1150
} else if (renderControlCharacters && isControlCharacter(charCode)) {
1151
sb.appendString('[U+');
1152
sb.appendString(to4CharHex(charCode));
1153
sb.appendString(']');
1154
producedCharacters = 8;
1155
charWidth = producedCharacters;
1156
} else {
1157
sb.appendCharCode(charCode);
1158
}
1159
}
1160
1161
charOffsetInPart += producedCharacters;
1162
charHorizontalOffset += charWidth;
1163
if (charIndex >= fauxIndentLength) {
1164
visibleColumn += charWidth;
1165
}
1166
}
1167
}
1168
1169
if (partIsEmptyAndHasPseudoAfter) {
1170
partDisplacement++;
1171
} else {
1172
partDisplacement = 0;
1173
}
1174
1175
if (charIndex >= len && !lastCharacterMappingDefined && part.isPseudoAfter()) {
1176
lastCharacterMappingDefined = true;
1177
characterMapping.setColumnInfo(charIndex + 1, partIndex, charOffsetInPart, charHorizontalOffset);
1178
}
1179
1180
sb.appendString('</span>');
1181
1182
}
1183
1184
if (!lastCharacterMappingDefined) {
1185
// When getting client rects for the last character, we will position the
1186
// text range at the end of the span, insteaf of at the beginning of next span
1187
characterMapping.setColumnInfo(len + 1, parts.length - 1, charOffsetInPart, charHorizontalOffset);
1188
}
1189
1190
if (isOverflowing) {
1191
sb.appendString('<span class="mtkoverflow">');
1192
sb.appendString(nls.localize('showMore', "Show more ({0})", renderOverflowingCharCount(overflowingCharCount)));
1193
sb.appendString('</span>');
1194
}
1195
1196
sb.appendString('</span>');
1197
1198
return new RenderLineOutput(characterMapping, containsForeignElements);
1199
}
1200
1201
function to4CharHex(n: number): string {
1202
return n.toString(16).toUpperCase().padStart(4, '0');
1203
}
1204
1205
function renderOverflowingCharCount(n: number): string {
1206
if (n < 1024) {
1207
return nls.localize('overflow.chars', "{0} chars", n);
1208
}
1209
if (n < 1024 * 1024) {
1210
return `${(n / 1024).toFixed(1)} KB`;
1211
}
1212
return `${(n / 1024 / 1024).toFixed(1)} MB`;
1213
}
1214
1215