Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/view/viewLayer.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 { FastDomNode, createFastDomNode } from '../../../base/browser/fastDomNode.js';
7
import { createTrustedTypesPolicy } from '../../../base/browser/trustedTypes.js';
8
import { BugIndicatingError } from '../../../base/common/errors.js';
9
import { EditorOption } from '../../common/config/editorOptions.js';
10
import { StringBuilder } from '../../common/core/stringBuilder.js';
11
import * as viewEvents from '../../common/viewEvents.js';
12
import { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';
13
import { ViewContext } from '../../common/viewModel/viewContext.js';
14
15
/**
16
* Represents a visible line
17
*/
18
export interface IVisibleLine extends ILine {
19
getDomNode(): HTMLElement | null;
20
setDomNode(domNode: HTMLElement): void;
21
22
/**
23
* Return null if the HTML should not be touched.
24
* Return the new HTML otherwise.
25
*/
26
renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean;
27
28
/**
29
* Layout the line.
30
*/
31
layoutLine(lineNumber: number, deltaTop: number, lineHeight: number): void;
32
}
33
34
export interface ILine {
35
onContentChanged(): void;
36
onTokensChanged(): void;
37
}
38
39
export interface ILineFactory<T extends ILine> {
40
createLine(): T;
41
}
42
43
export class RenderedLinesCollection<T extends ILine> {
44
private _lines!: T[];
45
private _rendLineNumberStart!: number;
46
47
constructor(
48
private readonly _lineFactory: ILineFactory<T>,
49
) {
50
this._set(1, []);
51
}
52
53
public flush(): void {
54
this._set(1, []);
55
}
56
57
_set(rendLineNumberStart: number, lines: T[]): void {
58
this._lines = lines;
59
this._rendLineNumberStart = rendLineNumberStart;
60
}
61
62
_get(): { rendLineNumberStart: number; lines: T[] } {
63
return {
64
rendLineNumberStart: this._rendLineNumberStart,
65
lines: this._lines
66
};
67
}
68
69
/**
70
* @returns Inclusive line number that is inside this collection
71
*/
72
public getStartLineNumber(): number {
73
return this._rendLineNumberStart;
74
}
75
76
/**
77
* @returns Inclusive line number that is inside this collection
78
*/
79
public getEndLineNumber(): number {
80
return this._rendLineNumberStart + this._lines.length - 1;
81
}
82
83
public getCount(): number {
84
return this._lines.length;
85
}
86
87
public getLine(lineNumber: number): T {
88
const lineIndex = lineNumber - this._rendLineNumberStart;
89
if (lineIndex < 0 || lineIndex >= this._lines.length) {
90
throw new BugIndicatingError('Illegal value for lineNumber');
91
}
92
return this._lines[lineIndex];
93
}
94
95
/**
96
* @returns Lines that were removed from this collection
97
*/
98
public onLinesDeleted(deleteFromLineNumber: number, deleteToLineNumber: number): T[] | null {
99
if (this.getCount() === 0) {
100
// no lines
101
return null;
102
}
103
104
const startLineNumber = this.getStartLineNumber();
105
const endLineNumber = this.getEndLineNumber();
106
107
if (deleteToLineNumber < startLineNumber) {
108
// deleting above the viewport
109
const deleteCnt = deleteToLineNumber - deleteFromLineNumber + 1;
110
this._rendLineNumberStart -= deleteCnt;
111
return null;
112
}
113
114
if (deleteFromLineNumber > endLineNumber) {
115
// deleted below the viewport
116
return null;
117
}
118
119
// Record what needs to be deleted
120
let deleteStartIndex = 0;
121
let deleteCount = 0;
122
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
123
const lineIndex = lineNumber - this._rendLineNumberStart;
124
125
if (deleteFromLineNumber <= lineNumber && lineNumber <= deleteToLineNumber) {
126
// this is a line to be deleted
127
if (deleteCount === 0) {
128
// this is the first line to be deleted
129
deleteStartIndex = lineIndex;
130
deleteCount = 1;
131
} else {
132
deleteCount++;
133
}
134
}
135
}
136
137
// Adjust this._rendLineNumberStart for lines deleted above
138
if (deleteFromLineNumber < startLineNumber) {
139
// Something was deleted above
140
let deleteAboveCount = 0;
141
142
if (deleteToLineNumber < startLineNumber) {
143
// the entire deleted lines are above
144
deleteAboveCount = deleteToLineNumber - deleteFromLineNumber + 1;
145
} else {
146
deleteAboveCount = startLineNumber - deleteFromLineNumber;
147
}
148
149
this._rendLineNumberStart -= deleteAboveCount;
150
}
151
152
const deleted = this._lines.splice(deleteStartIndex, deleteCount);
153
return deleted;
154
}
155
156
public onLinesChanged(changeFromLineNumber: number, changeCount: number): boolean {
157
const changeToLineNumber = changeFromLineNumber + changeCount - 1;
158
if (this.getCount() === 0) {
159
// no lines
160
return false;
161
}
162
163
const startLineNumber = this.getStartLineNumber();
164
const endLineNumber = this.getEndLineNumber();
165
166
let someoneNotified = false;
167
168
for (let changedLineNumber = changeFromLineNumber; changedLineNumber <= changeToLineNumber; changedLineNumber++) {
169
if (changedLineNumber >= startLineNumber && changedLineNumber <= endLineNumber) {
170
// Notify the line
171
this._lines[changedLineNumber - this._rendLineNumberStart].onContentChanged();
172
someoneNotified = true;
173
}
174
}
175
176
return someoneNotified;
177
}
178
179
public onLinesInserted(insertFromLineNumber: number, insertToLineNumber: number): T[] | null {
180
if (this.getCount() === 0) {
181
// no lines
182
return null;
183
}
184
185
const insertCnt = insertToLineNumber - insertFromLineNumber + 1;
186
const startLineNumber = this.getStartLineNumber();
187
const endLineNumber = this.getEndLineNumber();
188
189
if (insertFromLineNumber <= startLineNumber) {
190
// inserting above the viewport
191
this._rendLineNumberStart += insertCnt;
192
return null;
193
}
194
195
if (insertFromLineNumber > endLineNumber) {
196
// inserting below the viewport
197
return null;
198
}
199
200
if (insertCnt + insertFromLineNumber > endLineNumber) {
201
// insert inside the viewport in such a way that all remaining lines are pushed outside
202
const deleted = this._lines.splice(insertFromLineNumber - this._rendLineNumberStart, endLineNumber - insertFromLineNumber + 1);
203
return deleted;
204
}
205
206
// insert inside the viewport, push out some lines, but not all remaining lines
207
const newLines: T[] = [];
208
for (let i = 0; i < insertCnt; i++) {
209
newLines[i] = this._lineFactory.createLine();
210
}
211
const insertIndex = insertFromLineNumber - this._rendLineNumberStart;
212
const beforeLines = this._lines.slice(0, insertIndex);
213
const afterLines = this._lines.slice(insertIndex, this._lines.length - insertCnt);
214
const deletedLines = this._lines.slice(this._lines.length - insertCnt, this._lines.length);
215
216
this._lines = beforeLines.concat(newLines).concat(afterLines);
217
218
return deletedLines;
219
}
220
221
public onTokensChanged(ranges: { fromLineNumber: number; toLineNumber: number }[]): boolean {
222
if (this.getCount() === 0) {
223
// no lines
224
return false;
225
}
226
227
const startLineNumber = this.getStartLineNumber();
228
const endLineNumber = this.getEndLineNumber();
229
230
let notifiedSomeone = false;
231
for (let i = 0, len = ranges.length; i < len; i++) {
232
const rng = ranges[i];
233
234
if (rng.toLineNumber < startLineNumber || rng.fromLineNumber > endLineNumber) {
235
// range outside viewport
236
continue;
237
}
238
239
const from = Math.max(startLineNumber, rng.fromLineNumber);
240
const to = Math.min(endLineNumber, rng.toLineNumber);
241
242
for (let lineNumber = from; lineNumber <= to; lineNumber++) {
243
const lineIndex = lineNumber - this._rendLineNumberStart;
244
this._lines[lineIndex].onTokensChanged();
245
notifiedSomeone = true;
246
}
247
}
248
249
return notifiedSomeone;
250
}
251
}
252
253
export class VisibleLinesCollection<T extends IVisibleLine> {
254
255
public readonly domNode: FastDomNode<HTMLElement>;
256
private readonly _linesCollection: RenderedLinesCollection<T>;
257
258
constructor(
259
private readonly _viewContext: ViewContext,
260
private readonly _lineFactory: ILineFactory<T>,
261
) {
262
this.domNode = this._createDomNode();
263
this._linesCollection = new RenderedLinesCollection<T>(this._lineFactory);
264
}
265
266
private _createDomNode(): FastDomNode<HTMLElement> {
267
const domNode = createFastDomNode(document.createElement('div'));
268
domNode.setClassName('view-layer');
269
domNode.setPosition('absolute');
270
domNode.domNode.setAttribute('role', 'presentation');
271
domNode.domNode.setAttribute('aria-hidden', 'true');
272
return domNode;
273
}
274
275
// ---- begin view event handlers
276
277
public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
278
if (e.hasChanged(EditorOption.layoutInfo)) {
279
return true;
280
}
281
return false;
282
}
283
284
public onFlushed(e: viewEvents.ViewFlushedEvent, flushDom?: boolean): boolean {
285
// No need to clear the dom node because a full .innerHTML will occur in
286
// ViewLayerRenderer._render, however the fallback mechanism in the
287
// GPU renderer may cause this to be necessary as the .innerHTML call
288
// may not happen depending on the new state, leaving stale DOM nodes
289
// around.
290
if (flushDom) {
291
const start = this._linesCollection.getStartLineNumber();
292
const end = this._linesCollection.getEndLineNumber();
293
for (let i = start; i <= end; i++) {
294
this._linesCollection.getLine(i).getDomNode()?.remove();
295
}
296
}
297
this._linesCollection.flush();
298
return true;
299
}
300
301
public onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
302
return this._linesCollection.onLinesChanged(e.fromLineNumber, e.count);
303
}
304
305
public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
306
const deleted = this._linesCollection.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
307
if (deleted) {
308
// Remove from DOM
309
for (let i = 0, len = deleted.length; i < len; i++) {
310
const lineDomNode = deleted[i].getDomNode();
311
lineDomNode?.remove();
312
}
313
}
314
315
return true;
316
}
317
318
public onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
319
const deleted = this._linesCollection.onLinesInserted(e.fromLineNumber, e.toLineNumber);
320
if (deleted) {
321
// Remove from DOM
322
for (let i = 0, len = deleted.length; i < len; i++) {
323
const lineDomNode = deleted[i].getDomNode();
324
lineDomNode?.remove();
325
}
326
}
327
328
return true;
329
}
330
331
public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
332
return e.scrollTopChanged;
333
}
334
335
public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
336
return this._linesCollection.onTokensChanged(e.ranges);
337
}
338
339
public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
340
return true;
341
}
342
343
// ---- end view event handlers
344
345
public getStartLineNumber(): number {
346
return this._linesCollection.getStartLineNumber();
347
}
348
349
public getEndLineNumber(): number {
350
return this._linesCollection.getEndLineNumber();
351
}
352
353
public getVisibleLine(lineNumber: number): T {
354
return this._linesCollection.getLine(lineNumber);
355
}
356
357
public renderLines(viewportData: ViewportData): void {
358
359
const inp = this._linesCollection._get();
360
361
const renderer = new ViewLayerRenderer<T>(this.domNode.domNode, this._lineFactory, viewportData, this._viewContext);
362
363
const ctx: IRendererContext<T> = {
364
rendLineNumberStart: inp.rendLineNumberStart,
365
lines: inp.lines,
366
linesLength: inp.lines.length
367
};
368
369
// Decide if this render will do a single update (single large .innerHTML) or many updates (inserting/removing dom nodes)
370
const resCtx = renderer.render(ctx, viewportData.startLineNumber, viewportData.endLineNumber, viewportData.relativeVerticalOffset);
371
372
this._linesCollection._set(resCtx.rendLineNumberStart, resCtx.lines);
373
}
374
}
375
376
interface IRendererContext<T extends IVisibleLine> {
377
rendLineNumberStart: number;
378
lines: T[];
379
linesLength: number;
380
}
381
382
class ViewLayerRenderer<T extends IVisibleLine> {
383
384
private static _ttPolicy = createTrustedTypesPolicy('editorViewLayer', { createHTML: value => value });
385
386
constructor(
387
private readonly _domNode: HTMLElement,
388
private readonly _lineFactory: ILineFactory<T>,
389
private readonly _viewportData: ViewportData,
390
private readonly _viewContext: ViewContext
391
) {
392
}
393
394
public render(inContext: IRendererContext<T>, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext<T> {
395
396
const ctx: IRendererContext<T> = {
397
rendLineNumberStart: inContext.rendLineNumberStart,
398
lines: inContext.lines.slice(0),
399
linesLength: inContext.linesLength
400
};
401
402
if ((ctx.rendLineNumberStart + ctx.linesLength - 1 < startLineNumber) || (stopLineNumber < ctx.rendLineNumberStart)) {
403
// There is no overlap whatsoever
404
ctx.rendLineNumberStart = startLineNumber;
405
ctx.linesLength = stopLineNumber - startLineNumber + 1;
406
ctx.lines = [];
407
for (let x = startLineNumber; x <= stopLineNumber; x++) {
408
ctx.lines[x - startLineNumber] = this._lineFactory.createLine();
409
}
410
this._finishRendering(ctx, true, deltaTop);
411
return ctx;
412
}
413
414
// Update lines which will remain untouched
415
this._renderUntouchedLines(
416
ctx,
417
Math.max(startLineNumber - ctx.rendLineNumberStart, 0),
418
Math.min(stopLineNumber - ctx.rendLineNumberStart, ctx.linesLength - 1),
419
deltaTop,
420
startLineNumber
421
);
422
423
if (ctx.rendLineNumberStart > startLineNumber) {
424
// Insert lines before
425
const fromLineNumber = startLineNumber;
426
const toLineNumber = Math.min(stopLineNumber, ctx.rendLineNumberStart - 1);
427
if (fromLineNumber <= toLineNumber) {
428
this._insertLinesBefore(ctx, fromLineNumber, toLineNumber, deltaTop, startLineNumber);
429
ctx.linesLength += toLineNumber - fromLineNumber + 1;
430
}
431
} else if (ctx.rendLineNumberStart < startLineNumber) {
432
// Remove lines before
433
const removeCnt = Math.min(ctx.linesLength, startLineNumber - ctx.rendLineNumberStart);
434
if (removeCnt > 0) {
435
this._removeLinesBefore(ctx, removeCnt);
436
ctx.linesLength -= removeCnt;
437
}
438
}
439
440
ctx.rendLineNumberStart = startLineNumber;
441
442
if (ctx.rendLineNumberStart + ctx.linesLength - 1 < stopLineNumber) {
443
// Insert lines after
444
const fromLineNumber = ctx.rendLineNumberStart + ctx.linesLength;
445
const toLineNumber = stopLineNumber;
446
447
if (fromLineNumber <= toLineNumber) {
448
this._insertLinesAfter(ctx, fromLineNumber, toLineNumber, deltaTop, startLineNumber);
449
ctx.linesLength += toLineNumber - fromLineNumber + 1;
450
}
451
452
} else if (ctx.rendLineNumberStart + ctx.linesLength - 1 > stopLineNumber) {
453
// Remove lines after
454
const fromLineNumber = Math.max(0, stopLineNumber - ctx.rendLineNumberStart + 1);
455
const toLineNumber = ctx.linesLength - 1;
456
const removeCnt = toLineNumber - fromLineNumber + 1;
457
458
if (removeCnt > 0) {
459
this._removeLinesAfter(ctx, removeCnt);
460
ctx.linesLength -= removeCnt;
461
}
462
}
463
464
this._finishRendering(ctx, false, deltaTop);
465
466
return ctx;
467
}
468
469
private _renderUntouchedLines(ctx: IRendererContext<T>, startIndex: number, endIndex: number, deltaTop: number[], deltaLN: number): void {
470
const rendLineNumberStart = ctx.rendLineNumberStart;
471
const lines = ctx.lines;
472
473
for (let i = startIndex; i <= endIndex; i++) {
474
const lineNumber = rendLineNumberStart + i;
475
lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this._lineHeightForLineNumber(lineNumber));
476
}
477
}
478
479
private _insertLinesBefore(ctx: IRendererContext<T>, fromLineNumber: number, toLineNumber: number, deltaTop: number[], deltaLN: number): void {
480
const newLines: T[] = [];
481
let newLinesLen = 0;
482
for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {
483
newLines[newLinesLen++] = this._lineFactory.createLine();
484
}
485
ctx.lines = newLines.concat(ctx.lines);
486
}
487
488
private _removeLinesBefore(ctx: IRendererContext<T>, removeCount: number): void {
489
for (let i = 0; i < removeCount; i++) {
490
const lineDomNode = ctx.lines[i].getDomNode();
491
lineDomNode?.remove();
492
}
493
ctx.lines.splice(0, removeCount);
494
}
495
496
private _insertLinesAfter(ctx: IRendererContext<T>, fromLineNumber: number, toLineNumber: number, deltaTop: number[], deltaLN: number): void {
497
const newLines: T[] = [];
498
let newLinesLen = 0;
499
for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {
500
newLines[newLinesLen++] = this._lineFactory.createLine();
501
}
502
ctx.lines = ctx.lines.concat(newLines);
503
}
504
505
private _removeLinesAfter(ctx: IRendererContext<T>, removeCount: number): void {
506
const removeIndex = ctx.linesLength - removeCount;
507
508
for (let i = 0; i < removeCount; i++) {
509
const lineDomNode = ctx.lines[removeIndex + i].getDomNode();
510
lineDomNode?.remove();
511
}
512
ctx.lines.splice(removeIndex, removeCount);
513
}
514
515
private _finishRenderingNewLines(ctx: IRendererContext<T>, domNodeIsEmpty: boolean, newLinesHTML: string | TrustedHTML, wasNew: boolean[]): void {
516
if (ViewLayerRenderer._ttPolicy) {
517
newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML as string);
518
}
519
const lastChild = <HTMLElement>this._domNode.lastChild;
520
if (domNodeIsEmpty || !lastChild) {
521
this._domNode.innerHTML = newLinesHTML as string; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393;
522
} else {
523
lastChild.insertAdjacentHTML('afterend', newLinesHTML as string);
524
}
525
526
let currChild = <HTMLElement>this._domNode.lastChild;
527
for (let i = ctx.linesLength - 1; i >= 0; i--) {
528
const line = ctx.lines[i];
529
if (wasNew[i]) {
530
line.setDomNode(currChild);
531
currChild = <HTMLElement>currChild.previousSibling;
532
}
533
}
534
}
535
536
private _finishRenderingInvalidLines(ctx: IRendererContext<T>, invalidLinesHTML: string | TrustedHTML, wasInvalid: boolean[]): void {
537
const hugeDomNode = document.createElement('div');
538
539
if (ViewLayerRenderer._ttPolicy) {
540
invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML as string);
541
}
542
hugeDomNode.innerHTML = invalidLinesHTML as string;
543
544
for (let i = 0; i < ctx.linesLength; i++) {
545
const line = ctx.lines[i];
546
if (wasInvalid[i]) {
547
const source = <HTMLElement>hugeDomNode.firstChild;
548
const lineDomNode = line.getDomNode()!;
549
lineDomNode.parentNode!.replaceChild(source, lineDomNode);
550
line.setDomNode(source);
551
}
552
}
553
}
554
555
private static readonly _sb = new StringBuilder(100000);
556
557
private _finishRendering(ctx: IRendererContext<T>, domNodeIsEmpty: boolean, deltaTop: number[]): void {
558
559
const sb = ViewLayerRenderer._sb;
560
const linesLength = ctx.linesLength;
561
const lines = ctx.lines;
562
const rendLineNumberStart = ctx.rendLineNumberStart;
563
564
const wasNew: boolean[] = [];
565
{
566
sb.reset();
567
let hadNewLine = false;
568
569
for (let i = 0; i < linesLength; i++) {
570
const line = lines[i];
571
wasNew[i] = false;
572
573
const lineDomNode = line.getDomNode();
574
if (lineDomNode) {
575
// line is not new
576
continue;
577
}
578
579
const renderedLineNumber = i + rendLineNumberStart;
580
const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb);
581
if (!renderResult) {
582
// line does not need rendering
583
continue;
584
}
585
586
wasNew[i] = true;
587
hadNewLine = true;
588
}
589
590
if (hadNewLine) {
591
this._finishRenderingNewLines(ctx, domNodeIsEmpty, sb.build(), wasNew);
592
}
593
}
594
595
{
596
sb.reset();
597
598
let hadInvalidLine = false;
599
const wasInvalid: boolean[] = [];
600
601
for (let i = 0; i < linesLength; i++) {
602
const line = lines[i];
603
wasInvalid[i] = false;
604
605
if (wasNew[i]) {
606
// line was new
607
continue;
608
}
609
610
const renderedLineNumber = i + rendLineNumberStart;
611
const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb);
612
if (!renderResult) {
613
// line does not need rendering
614
continue;
615
}
616
617
wasInvalid[i] = true;
618
hadInvalidLine = true;
619
}
620
621
if (hadInvalidLine) {
622
this._finishRenderingInvalidLines(ctx, sb.build(), wasInvalid);
623
}
624
}
625
}
626
627
private _lineHeightForLineNumber(lineNumber: number): number {
628
return this._viewContext.viewLayout.getLineHeightForLineNumber(lineNumber);
629
}
630
}
631
632