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