Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/viewLines/viewLines.ts
5292 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 } from '../../../../base/browser/fastDomNode.js';
7
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../base/browser/ui/mouseCursor/mouseCursor.js';
8
import { RunOnceScheduler } from '../../../../base/common/async.js';
9
import * as platform from '../../../../base/common/platform.js';
10
import { Constants } from '../../../../base/common/uint.js';
11
import './viewLines.css';
12
import { applyFontInfo } from '../../config/domFontInfo.js';
13
import { HorizontalPosition, HorizontalRange, IViewLines, LineVisibleRanges, VisibleRanges } from '../../view/renderingContext.js';
14
import { VisibleLinesCollection } from '../../view/viewLayer.js';
15
import { PartFingerprint, PartFingerprints, ViewPart } from '../../view/viewPart.js';
16
import { DomReadingContext } from './domReadingContext.js';
17
import { ViewLine } from './viewLine.js';
18
import { EditorOption } from '../../../common/config/editorOptions.js';
19
import { Position } from '../../../common/core/position.js';
20
import { Range } from '../../../common/core/range.js';
21
import { Selection } from '../../../common/core/selection.js';
22
import { ScrollType } from '../../../common/editorCommon.js';
23
import * as viewEvents from '../../../common/viewEvents.js';
24
import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';
25
import { Viewport } from '../../../common/viewModel.js';
26
import { ViewContext } from '../../../common/viewModel/viewContext.js';
27
import { ViewLineOptions } from './viewLineOptions.js';
28
import type { ViewGpuContext } from '../../gpu/viewGpuContext.js';
29
import { TextDirection } from '../../../common/model.js';
30
31
class LastRenderedData {
32
33
private _currentVisibleRange: Range;
34
35
constructor() {
36
this._currentVisibleRange = new Range(1, 1, 1, 1);
37
}
38
39
public getCurrentVisibleRange(): Range {
40
return this._currentVisibleRange;
41
}
42
43
public setCurrentVisibleRange(currentVisibleRange: Range): void {
44
this._currentVisibleRange = currentVisibleRange;
45
}
46
}
47
48
class HorizontalRevealRangeRequest {
49
public readonly type = 'range';
50
public readonly minLineNumber: number;
51
public readonly maxLineNumber: number;
52
53
constructor(
54
public readonly minimalReveal: boolean,
55
public readonly lineNumber: number,
56
public readonly startColumn: number,
57
public readonly endColumn: number,
58
public readonly startScrollTop: number,
59
public readonly stopScrollTop: number,
60
public readonly scrollType: ScrollType
61
) {
62
this.minLineNumber = lineNumber;
63
this.maxLineNumber = lineNumber;
64
}
65
}
66
67
class HorizontalRevealSelectionsRequest {
68
public readonly type = 'selections';
69
public readonly minLineNumber: number;
70
public readonly maxLineNumber: number;
71
72
constructor(
73
public readonly minimalReveal: boolean,
74
public readonly selections: Selection[],
75
public readonly startScrollTop: number,
76
public readonly stopScrollTop: number,
77
public readonly scrollType: ScrollType
78
) {
79
let minLineNumber = selections[0].startLineNumber;
80
let maxLineNumber = selections[0].endLineNumber;
81
for (let i = 1, len = selections.length; i < len; i++) {
82
const selection = selections[i];
83
minLineNumber = Math.min(minLineNumber, selection.startLineNumber);
84
maxLineNumber = Math.max(maxLineNumber, selection.endLineNumber);
85
}
86
this.minLineNumber = minLineNumber;
87
this.maxLineNumber = maxLineNumber;
88
}
89
}
90
91
type HorizontalRevealRequest = HorizontalRevealRangeRequest | HorizontalRevealSelectionsRequest;
92
93
/**
94
* The view lines part is responsible for rendering the actual content of a
95
* file.
96
*/
97
export class ViewLines extends ViewPart implements IViewLines {
98
/**
99
* Adds this amount of pixels to the right of lines (no-one wants to type near the edge of the viewport)
100
*/
101
private static readonly HORIZONTAL_EXTRA_PX = 30;
102
103
private readonly _linesContent: FastDomNode<HTMLElement>;
104
private readonly _textRangeRestingSpot: HTMLElement;
105
private readonly _visibleLines: VisibleLinesCollection<ViewLine>;
106
private readonly domNode: FastDomNode<HTMLElement>;
107
108
// --- config
109
private _lineHeight: number;
110
private _typicalHalfwidthCharacterWidth: number;
111
private _isViewportWrapping: boolean;
112
private _revealHorizontalRightPadding: number;
113
private _cursorSurroundingLines: number;
114
private _cursorSurroundingLinesStyle: 'default' | 'all';
115
private _canUseLayerHinting: boolean;
116
private _viewLineOptions: ViewLineOptions;
117
118
// --- width
119
private _maxLineWidth: number;
120
private readonly _asyncUpdateLineWidths: RunOnceScheduler;
121
private readonly _asyncCheckMonospaceFontAssumptions: RunOnceScheduler;
122
123
private _horizontalRevealRequest: HorizontalRevealRequest | null;
124
private readonly _lastRenderedData: LastRenderedData;
125
126
// Sticky Scroll
127
private _stickyScrollEnabled: boolean;
128
private _maxNumberStickyLines: number;
129
130
constructor(context: ViewContext, viewGpuContext: ViewGpuContext | undefined, linesContent: FastDomNode<HTMLElement>) {
131
super(context);
132
133
const conf = this._context.configuration;
134
const options = this._context.configuration.options;
135
const fontInfo = options.get(EditorOption.fontInfo);
136
const wrappingInfo = options.get(EditorOption.wrappingInfo);
137
138
this._lineHeight = options.get(EditorOption.lineHeight);
139
this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
140
this._isViewportWrapping = wrappingInfo.isViewportWrapping;
141
this._revealHorizontalRightPadding = options.get(EditorOption.revealHorizontalRightPadding);
142
this._cursorSurroundingLines = options.get(EditorOption.cursorSurroundingLines);
143
this._cursorSurroundingLinesStyle = options.get(EditorOption.cursorSurroundingLinesStyle);
144
this._canUseLayerHinting = !options.get(EditorOption.disableLayerHinting);
145
this._viewLineOptions = new ViewLineOptions(conf, this._context.theme.type);
146
147
this._linesContent = linesContent;
148
this._textRangeRestingSpot = document.createElement('div');
149
this._visibleLines = new VisibleLinesCollection(this._context, {
150
createLine: () => new ViewLine(viewGpuContext, this._viewLineOptions),
151
});
152
this.domNode = this._visibleLines.domNode;
153
154
PartFingerprints.write(this.domNode, PartFingerprint.ViewLines);
155
this.domNode.setClassName(`view-lines ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
156
applyFontInfo(this.domNode, fontInfo);
157
158
// --- width & height
159
this._maxLineWidth = 0;
160
this._asyncUpdateLineWidths = new RunOnceScheduler(() => {
161
this._updateLineWidthsSlow();
162
}, 200);
163
this._asyncCheckMonospaceFontAssumptions = new RunOnceScheduler(() => {
164
this._checkMonospaceFontAssumptions();
165
}, 2000);
166
167
this._lastRenderedData = new LastRenderedData();
168
169
this._horizontalRevealRequest = null;
170
171
// sticky scroll widget
172
this._stickyScrollEnabled = options.get(EditorOption.stickyScroll).enabled;
173
this._maxNumberStickyLines = options.get(EditorOption.stickyScroll).maxLineCount;
174
}
175
176
public override dispose(): void {
177
this._asyncUpdateLineWidths.dispose();
178
this._asyncCheckMonospaceFontAssumptions.dispose();
179
super.dispose();
180
}
181
182
public getDomNode(): FastDomNode<HTMLElement> {
183
return this.domNode;
184
}
185
186
// ---- begin view event handlers
187
188
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
189
this._visibleLines.onConfigurationChanged(e);
190
if (e.hasChanged(EditorOption.wrappingInfo)) {
191
this._maxLineWidth = 0;
192
}
193
194
const options = this._context.configuration.options;
195
const fontInfo = options.get(EditorOption.fontInfo);
196
const wrappingInfo = options.get(EditorOption.wrappingInfo);
197
198
this._lineHeight = options.get(EditorOption.lineHeight);
199
this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth;
200
this._isViewportWrapping = wrappingInfo.isViewportWrapping;
201
this._revealHorizontalRightPadding = options.get(EditorOption.revealHorizontalRightPadding);
202
this._cursorSurroundingLines = options.get(EditorOption.cursorSurroundingLines);
203
this._cursorSurroundingLinesStyle = options.get(EditorOption.cursorSurroundingLinesStyle);
204
this._canUseLayerHinting = !options.get(EditorOption.disableLayerHinting);
205
206
// sticky scroll
207
this._stickyScrollEnabled = options.get(EditorOption.stickyScroll).enabled;
208
this._maxNumberStickyLines = options.get(EditorOption.stickyScroll).maxLineCount;
209
210
applyFontInfo(this.domNode, fontInfo);
211
212
this._onOptionsMaybeChanged();
213
214
if (e.hasChanged(EditorOption.layoutInfo)) {
215
this._maxLineWidth = 0;
216
}
217
218
return true;
219
}
220
private _onOptionsMaybeChanged(): boolean {
221
const conf = this._context.configuration;
222
223
const newViewLineOptions = new ViewLineOptions(conf, this._context.theme.type);
224
if (!this._viewLineOptions.equals(newViewLineOptions)) {
225
this._viewLineOptions = newViewLineOptions;
226
227
const startLineNumber = this._visibleLines.getStartLineNumber();
228
const endLineNumber = this._visibleLines.getEndLineNumber();
229
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
230
const line = this._visibleLines.getVisibleLine(lineNumber);
231
line.onOptionsChanged(this._viewLineOptions);
232
}
233
return true;
234
}
235
236
return false;
237
}
238
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
239
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
240
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
241
let r = false;
242
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
243
r = this._visibleLines.getVisibleLine(lineNumber).onSelectionChanged() || r;
244
}
245
return r;
246
}
247
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
248
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
249
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
250
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
251
this._visibleLines.getVisibleLine(lineNumber).onDecorationsChanged();
252
}
253
return true;
254
}
255
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
256
const shouldRender = this._visibleLines.onFlushed(e, this._viewLineOptions.useGpu);
257
this._maxLineWidth = 0;
258
return shouldRender;
259
}
260
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
261
return this._visibleLines.onLinesChanged(e);
262
}
263
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
264
return this._visibleLines.onLinesDeleted(e);
265
}
266
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
267
return this._visibleLines.onLinesInserted(e);
268
}
269
public override onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean {
270
// Using the future viewport here in order to handle multiple
271
// incoming reveal range requests that might all desire to be animated
272
const desiredScrollTop = this._computeScrollTopToRevealRange(this._context.viewLayout.getFutureViewport(), e.source, e.minimalReveal, e.range, e.selections, e.verticalType);
273
274
if (desiredScrollTop === -1) {
275
// marker to abort the reveal range request
276
return false;
277
}
278
279
// validate the new desired scroll top
280
let newScrollPosition = this._context.viewLayout.validateScrollPosition({ scrollTop: desiredScrollTop });
281
282
if (e.revealHorizontal) {
283
if (e.range && e.range.startLineNumber !== e.range.endLineNumber) {
284
// Two or more lines? => scroll to base (That's how you see most of the two lines)
285
newScrollPosition = {
286
scrollTop: newScrollPosition.scrollTop,
287
scrollLeft: 0
288
};
289
} else if (e.range) {
290
// We don't necessarily know the horizontal offset of this range since the line might not be in the view...
291
this._horizontalRevealRequest = new HorizontalRevealRangeRequest(e.minimalReveal, e.range.startLineNumber, e.range.startColumn, e.range.endColumn, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
292
} else if (e.selections && e.selections.length > 0) {
293
this._horizontalRevealRequest = new HorizontalRevealSelectionsRequest(e.minimalReveal, e.selections, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
294
}
295
} else {
296
this._horizontalRevealRequest = null;
297
}
298
299
const scrollTopDelta = Math.abs(this._context.viewLayout.getCurrentScrollTop() - newScrollPosition.scrollTop);
300
const scrollType = (scrollTopDelta <= this._lineHeight ? ScrollType.Immediate : e.scrollType);
301
this._context.viewModel.viewLayout.setScrollPosition(newScrollPosition, scrollType);
302
303
return true;
304
}
305
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
306
if (this._horizontalRevealRequest && e.scrollLeftChanged) {
307
// cancel any outstanding horizontal reveal request if someone else scrolls horizontally.
308
this._horizontalRevealRequest = null;
309
}
310
if (this._horizontalRevealRequest && e.scrollTopChanged) {
311
const min = Math.min(this._horizontalRevealRequest.startScrollTop, this._horizontalRevealRequest.stopScrollTop);
312
const max = Math.max(this._horizontalRevealRequest.startScrollTop, this._horizontalRevealRequest.stopScrollTop);
313
if (e.scrollTop < min || e.scrollTop > max) {
314
// cancel any outstanding horizontal reveal request if someone else scrolls vertically.
315
this._horizontalRevealRequest = null;
316
}
317
}
318
this.domNode.setWidth(e.scrollWidth);
319
return this._visibleLines.onScrollChanged(e) || e.scrollTopChanged || e.scrollLeftChanged;
320
}
321
322
public override onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
323
return this._visibleLines.onTokensChanged(e);
324
}
325
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
326
this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLineWidth);
327
return this._visibleLines.onZonesChanged(e);
328
}
329
public override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
330
return this._onOptionsMaybeChanged();
331
}
332
333
// ---- end view event handlers
334
335
// ----------- HELPERS FOR OTHERS
336
337
public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position | null {
338
const viewLineDomNode = this._getViewLineDomNode(spanNode);
339
if (viewLineDomNode === null) {
340
// Couldn't find view line node
341
return null;
342
}
343
const lineNumber = this._getLineNumberFor(viewLineDomNode);
344
345
if (lineNumber === -1) {
346
// Couldn't find view line node
347
return null;
348
}
349
350
if (lineNumber < 1 || lineNumber > this._context.viewModel.getLineCount()) {
351
// lineNumber is outside range
352
return null;
353
}
354
355
if (this._context.viewModel.getLineMaxColumn(lineNumber) === 1) {
356
// Line is empty
357
return new Position(lineNumber, 1);
358
}
359
360
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
361
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
362
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
363
// Couldn't find line
364
return null;
365
}
366
367
let column = this._visibleLines.getVisibleLine(lineNumber).getColumnOfNodeOffset(spanNode, offset);
368
const minColumn = this._context.viewModel.getLineMinColumn(lineNumber);
369
if (column < minColumn) {
370
column = minColumn;
371
}
372
return new Position(lineNumber, column);
373
}
374
375
private _getViewLineDomNode(node: HTMLElement | null): HTMLElement | null {
376
while (node && node.nodeType === 1) {
377
if (node.className === ViewLine.CLASS_NAME) {
378
return node;
379
}
380
node = node.parentElement;
381
}
382
return null;
383
}
384
385
/**
386
* @returns the line number of this view line dom node.
387
*/
388
private _getLineNumberFor(domNode: HTMLElement): number {
389
const startLineNumber = this._visibleLines.getStartLineNumber();
390
const endLineNumber = this._visibleLines.getEndLineNumber();
391
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
392
const line = this._visibleLines.getVisibleLine(lineNumber);
393
if (domNode === line.getDomNode()) {
394
return lineNumber;
395
}
396
}
397
return -1;
398
}
399
400
public getLineWidth(lineNumber: number): number {
401
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
402
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
403
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
404
// Couldn't find line
405
return -1;
406
}
407
408
const context = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
409
const result = this._visibleLines.getVisibleLine(lineNumber).getWidth(context);
410
this._updateLineWidthsSlowIfDomDidLayout(context);
411
412
return result;
413
}
414
415
public resetLineWidthCaches(): void {
416
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
417
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
418
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
419
this._visibleLines.getVisibleLine(lineNumber).resetCachedWidth();
420
}
421
}
422
423
public linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null {
424
const originalEndLineNumber = _range.endLineNumber;
425
const range = Range.intersectRanges(_range, this._lastRenderedData.getCurrentVisibleRange());
426
if (!range) {
427
return null;
428
}
429
430
const visibleRanges: LineVisibleRanges[] = [];
431
let visibleRangesLen = 0;
432
const domReadingContext = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
433
434
let nextLineModelLineNumber: number = 0;
435
if (includeNewLines) {
436
nextLineModelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;
437
}
438
439
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
440
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
441
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
442
443
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
444
continue;
445
}
446
447
const startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1;
448
const continuesInNextLine = lineNumber !== originalEndLineNumber;
449
const endColumn = continuesInNextLine ? this._context.viewModel.getLineMaxColumn(lineNumber) : range.endColumn;
450
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
451
const visibleRangesForLine = visibleLine.getVisibleRangesForRange(lineNumber, startColumn, endColumn, domReadingContext);
452
453
if (!visibleRangesForLine) {
454
continue;
455
}
456
457
if (includeNewLines && lineNumber < originalEndLineNumber) {
458
const currentLineModelLineNumber = nextLineModelLineNumber;
459
nextLineModelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber + 1, 1)).lineNumber;
460
461
if (currentLineModelLineNumber !== nextLineModelLineNumber) {
462
const floatHorizontalRange = visibleRangesForLine.ranges[visibleRangesForLine.ranges.length - 1];
463
floatHorizontalRange.width += this._typicalHalfwidthCharacterWidth;
464
if (this._context.viewModel.getTextDirection(currentLineModelLineNumber) === TextDirection.RTL) {
465
floatHorizontalRange.left -= this._typicalHalfwidthCharacterWidth;
466
}
467
}
468
}
469
470
visibleRanges[visibleRangesLen++] = new LineVisibleRanges(visibleRangesForLine.outsideRenderedLine, lineNumber, HorizontalRange.from(visibleRangesForLine.ranges), continuesInNextLine);
471
}
472
473
this._updateLineWidthsSlowIfDomDidLayout(domReadingContext);
474
475
if (visibleRangesLen === 0) {
476
return null;
477
}
478
479
return visibleRanges;
480
}
481
482
private _visibleRangesForLineRange(lineNumber: number, startColumn: number, endColumn: number): VisibleRanges | null {
483
if (lineNumber < this._visibleLines.getStartLineNumber() || lineNumber > this._visibleLines.getEndLineNumber()) {
484
return null;
485
}
486
487
const domReadingContext = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
488
const result = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(lineNumber, startColumn, endColumn, domReadingContext);
489
this._updateLineWidthsSlowIfDomDidLayout(domReadingContext);
490
491
return result;
492
}
493
494
private _lineIsRenderedRTL(lineNumber: number): boolean {
495
if (lineNumber < this._visibleLines.getStartLineNumber() || lineNumber > this._visibleLines.getEndLineNumber()) {
496
return false;
497
}
498
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
499
return visibleLine.isRenderedRTL();
500
}
501
502
public visibleRangeForPosition(position: Position): HorizontalPosition | null {
503
const visibleRanges = this._visibleRangesForLineRange(position.lineNumber, position.column, position.column);
504
if (!visibleRanges) {
505
return null;
506
}
507
return new HorizontalPosition(visibleRanges.outsideRenderedLine, visibleRanges.ranges[0].left);
508
}
509
510
// --- implementation
511
512
public updateLineWidths(): void {
513
this._updateLineWidths(false);
514
}
515
516
/**
517
* Updates the max line width if it is fast to compute.
518
* Returns true if all lines were taken into account.
519
* Returns false if some lines need to be reevaluated (in a slow fashion).
520
*/
521
private _updateLineWidthsFast(): boolean {
522
return this._updateLineWidths(true);
523
}
524
525
private _updateLineWidthsSlow(): void {
526
this._updateLineWidths(false);
527
}
528
529
/**
530
* Update the line widths using DOM layout information after someone else
531
* has caused a synchronous layout.
532
*/
533
private _updateLineWidthsSlowIfDomDidLayout(domReadingContext: DomReadingContext): void {
534
if (!domReadingContext.didDomLayout) {
535
// only proceed if we just did a layout
536
return;
537
}
538
if (!this._asyncUpdateLineWidths.isScheduled()) {
539
// reading widths is not scheduled => widths are up-to-date
540
return;
541
}
542
this._asyncUpdateLineWidths.cancel();
543
this._updateLineWidthsSlow();
544
}
545
546
private _updateLineWidths(fast: boolean): boolean {
547
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
548
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
549
550
let localMaxLineWidth = 1;
551
let allWidthsComputed = true;
552
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
553
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
554
555
if (fast && !visibleLine.getWidthIsFast()) {
556
// Cannot compute width in a fast way for this line
557
allWidthsComputed = false;
558
continue;
559
}
560
561
localMaxLineWidth = Math.max(localMaxLineWidth, visibleLine.getWidth(null));
562
}
563
564
if (allWidthsComputed && rendStartLineNumber === 1 && rendEndLineNumber === this._context.viewModel.getLineCount()) {
565
// we know the max line width for all the lines
566
this._maxLineWidth = 0;
567
}
568
569
this._ensureMaxLineWidth(localMaxLineWidth);
570
571
return allWidthsComputed;
572
}
573
574
private _checkMonospaceFontAssumptions(): void {
575
// Problems with monospace assumptions are more apparent for longer lines,
576
// as small rounding errors start to sum up, so we will select the longest
577
// line for a closer inspection
578
let longestLineNumber = -1;
579
let longestWidth = -1;
580
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
581
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
582
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
583
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
584
if (visibleLine.needsMonospaceFontCheck()) {
585
const lineWidth = visibleLine.getWidth(null);
586
if (lineWidth > longestWidth) {
587
longestWidth = lineWidth;
588
longestLineNumber = lineNumber;
589
}
590
}
591
}
592
593
if (longestLineNumber === -1) {
594
return;
595
}
596
597
if (!this._visibleLines.getVisibleLine(longestLineNumber).monospaceAssumptionsAreValid()) {
598
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
599
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
600
visibleLine.onMonospaceAssumptionsInvalidated();
601
}
602
}
603
}
604
605
public prepareRender(): void {
606
throw new Error('Not supported');
607
}
608
609
public render(): void {
610
throw new Error('Not supported');
611
}
612
613
public renderText(viewportData: ViewportData): void {
614
// (1) render lines - ensures lines are in the DOM
615
this._visibleLines.renderLines(viewportData);
616
this._lastRenderedData.setCurrentVisibleRange(viewportData.visibleRange);
617
this.domNode.setWidth(this._context.viewLayout.getScrollWidth());
618
this.domNode.setHeight(Math.min(this._context.viewLayout.getScrollHeight(), 1000000));
619
620
// (2) compute horizontal scroll position:
621
// - this must happen after the lines are in the DOM since it might need a line that rendered just now
622
// - it might change `scrollWidth` and `scrollLeft`
623
if (this._horizontalRevealRequest) {
624
625
const horizontalRevealRequest = this._horizontalRevealRequest;
626
627
// Check that we have the line that contains the horizontal range in the viewport
628
if (viewportData.startLineNumber <= horizontalRevealRequest.minLineNumber && horizontalRevealRequest.maxLineNumber <= viewportData.endLineNumber) {
629
630
this._horizontalRevealRequest = null;
631
632
// allow `visibleRangesForRange2` to work
633
this.onDidRender();
634
635
// compute new scroll position
636
const newScrollLeft = this._computeScrollLeftToReveal(horizontalRevealRequest);
637
638
if (newScrollLeft) {
639
if (!this._isViewportWrapping && !newScrollLeft.hasRTL) {
640
// ensure `scrollWidth` is large enough
641
this._ensureMaxLineWidth(newScrollLeft.maxHorizontalOffset);
642
}
643
// set `scrollLeft`
644
this._context.viewModel.viewLayout.setScrollPosition({
645
scrollLeft: newScrollLeft.scrollLeft
646
}, horizontalRevealRequest.scrollType);
647
}
648
}
649
}
650
651
// Update max line width (not so important, it is just so the horizontal scrollbar doesn't get too small)
652
if (!this._updateLineWidthsFast()) {
653
// Computing the width of some lines would be slow => delay it
654
this._asyncUpdateLineWidths.schedule();
655
} else {
656
this._asyncUpdateLineWidths.cancel();
657
}
658
659
if (platform.isLinux && !this._asyncCheckMonospaceFontAssumptions.isScheduled()) {
660
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
661
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
662
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
663
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
664
if (visibleLine.needsMonospaceFontCheck()) {
665
this._asyncCheckMonospaceFontAssumptions.schedule();
666
break;
667
}
668
}
669
}
670
671
// (3) handle scrolling
672
this._linesContent.setLayerHinting(this._canUseLayerHinting);
673
this._linesContent.setContain('strict');
674
const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta;
675
this._linesContent.setTop(-adjustedScrollTop);
676
this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft());
677
}
678
679
// --- width
680
681
private _ensureMaxLineWidth(lineWidth: number): void {
682
// When GPU rendering is enabled, ViewLinesGpu handles max line width tracking
683
if (this._viewLineOptions.useGpu) {
684
return;
685
}
686
const iLineWidth = Math.ceil(lineWidth);
687
if (this._maxLineWidth < iLineWidth) {
688
this._maxLineWidth = iLineWidth;
689
this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLineWidth);
690
}
691
}
692
693
private _computeScrollTopToRevealRange(viewport: Viewport, source: string | null | undefined, minimalReveal: boolean, range: Range | null, selections: Selection[] | null, verticalType: viewEvents.VerticalRevealType): number {
694
const viewportStartY = viewport.top;
695
const viewportHeight = viewport.height;
696
const viewportEndY = viewportStartY + viewportHeight;
697
let boxIsSingleRange: boolean;
698
let boxStartY: number;
699
let boxEndY: number;
700
701
if (selections && selections.length > 0) {
702
let minLineNumber = selections[0].startLineNumber;
703
let maxLineNumber = selections[0].endLineNumber;
704
for (let i = 1, len = selections.length; i < len; i++) {
705
const selection = selections[i];
706
minLineNumber = Math.min(minLineNumber, selection.startLineNumber);
707
maxLineNumber = Math.max(maxLineNumber, selection.endLineNumber);
708
}
709
boxIsSingleRange = false;
710
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(minLineNumber);
711
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(maxLineNumber) + this._lineHeight;
712
} else if (range) {
713
boxIsSingleRange = true;
714
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.startLineNumber);
715
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.endLineNumber) + this._lineHeight;
716
} else {
717
return -1;
718
}
719
720
const shouldIgnoreScrollOff = (source === 'mouse' || minimalReveal) && this._cursorSurroundingLinesStyle === 'default';
721
722
let paddingTop: number = 0;
723
let paddingBottom: number = 0;
724
725
if (!shouldIgnoreScrollOff) {
726
const maxLinesInViewport = (viewportHeight / this._lineHeight);
727
const surroundingLines = Math.max(this._cursorSurroundingLines, this._stickyScrollEnabled ? this._maxNumberStickyLines : 0);
728
const context = Math.min(maxLinesInViewport / 2, surroundingLines);
729
paddingTop = context * this._lineHeight;
730
paddingBottom = Math.max(0, (context - 1)) * this._lineHeight;
731
} else {
732
if (!minimalReveal) {
733
// Reveal one more line above (this case is hit when dragging)
734
paddingTop = this._lineHeight;
735
}
736
}
737
if (!minimalReveal) {
738
if (verticalType === viewEvents.VerticalRevealType.Simple || verticalType === viewEvents.VerticalRevealType.Bottom) {
739
// Reveal one line more when the last line would be covered by the scrollbar - arrow down case or revealing a line explicitly at bottom
740
paddingBottom += this._lineHeight;
741
}
742
}
743
744
boxStartY -= paddingTop;
745
boxEndY += paddingBottom;
746
let newScrollTop: number;
747
748
if (boxEndY - boxStartY > viewportHeight) {
749
// the box is larger than the viewport ... scroll to its top
750
if (!boxIsSingleRange) {
751
// do not reveal multiple cursors if there are more than fit the viewport
752
return -1;
753
}
754
newScrollTop = boxStartY;
755
} else if (verticalType === viewEvents.VerticalRevealType.NearTop || verticalType === viewEvents.VerticalRevealType.NearTopIfOutsideViewport) {
756
if (verticalType === viewEvents.VerticalRevealType.NearTopIfOutsideViewport && viewportStartY <= boxStartY && boxEndY <= viewportEndY) {
757
// Box is already in the viewport... do nothing
758
newScrollTop = viewportStartY;
759
} else {
760
// We want a gap that is 20% of the viewport, but with a minimum of 5 lines
761
const desiredGapAbove = Math.max(5 * this._lineHeight, viewportHeight * 0.2);
762
// Try to scroll just above the box with the desired gap
763
const desiredScrollTop = boxStartY - desiredGapAbove;
764
// But ensure that the box is not pushed out of viewport
765
const minScrollTop = boxEndY - viewportHeight;
766
newScrollTop = Math.max(minScrollTop, desiredScrollTop);
767
}
768
} else if (verticalType === viewEvents.VerticalRevealType.Center || verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport) {
769
if (verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport && viewportStartY <= boxStartY && boxEndY <= viewportEndY) {
770
// Box is already in the viewport... do nothing
771
newScrollTop = viewportStartY;
772
} else {
773
// Box is outside the viewport... center it
774
const boxMiddleY = (boxStartY + boxEndY) / 2;
775
newScrollTop = Math.max(0, boxMiddleY - viewportHeight / 2);
776
}
777
} else {
778
newScrollTop = this._computeMinimumScrolling(viewportStartY, viewportEndY, boxStartY, boxEndY, verticalType === viewEvents.VerticalRevealType.Top, verticalType === viewEvents.VerticalRevealType.Bottom);
779
}
780
781
return newScrollTop;
782
}
783
784
private _computeScrollLeftToReveal(horizontalRevealRequest: HorizontalRevealRequest): { scrollLeft: number; maxHorizontalOffset: number; hasRTL: boolean } | null {
785
786
const viewport = this._context.viewLayout.getCurrentViewport();
787
const layoutInfo = this._context.configuration.options.get(EditorOption.layoutInfo);
788
const viewportStartX = viewport.left;
789
const viewportEndX = viewportStartX + viewport.width - layoutInfo.verticalScrollbarWidth;
790
791
let boxStartX = Constants.MAX_SAFE_SMALL_INTEGER;
792
let boxEndX = 0;
793
let hasRTL = false;
794
if (horizontalRevealRequest.type === 'range') {
795
hasRTL = this._lineIsRenderedRTL(horizontalRevealRequest.lineNumber);
796
const visibleRanges = this._visibleRangesForLineRange(horizontalRevealRequest.lineNumber, horizontalRevealRequest.startColumn, horizontalRevealRequest.endColumn);
797
if (!visibleRanges) {
798
return null;
799
}
800
for (const visibleRange of visibleRanges.ranges) {
801
boxStartX = Math.min(boxStartX, Math.round(visibleRange.left));
802
boxEndX = Math.max(boxEndX, Math.round(visibleRange.left + visibleRange.width));
803
}
804
} else {
805
for (const selection of horizontalRevealRequest.selections) {
806
if (selection.startLineNumber !== selection.endLineNumber) {
807
return null;
808
}
809
const visibleRanges = this._visibleRangesForLineRange(selection.startLineNumber, selection.startColumn, selection.endColumn);
810
hasRTL ||= this._lineIsRenderedRTL(selection.startLineNumber);
811
if (!visibleRanges) {
812
return null;
813
}
814
for (const visibleRange of visibleRanges.ranges) {
815
boxStartX = Math.min(boxStartX, Math.round(visibleRange.left));
816
boxEndX = Math.max(boxEndX, Math.round(visibleRange.left + visibleRange.width));
817
}
818
}
819
}
820
821
if (!horizontalRevealRequest.minimalReveal) {
822
boxStartX = Math.max(0, boxStartX - ViewLines.HORIZONTAL_EXTRA_PX);
823
boxEndX += this._revealHorizontalRightPadding;
824
}
825
826
if (horizontalRevealRequest.type === 'selections' && boxEndX - boxStartX > viewport.width) {
827
return null;
828
}
829
830
const newScrollLeft = this._computeMinimumScrolling(viewportStartX, viewportEndX, boxStartX, boxEndX);
831
return {
832
scrollLeft: newScrollLeft,
833
maxHorizontalOffset: boxEndX,
834
hasRTL
835
};
836
}
837
838
private _computeMinimumScrolling(viewportStart: number, viewportEnd: number, boxStart: number, boxEnd: number, revealAtStart?: boolean, revealAtEnd?: boolean): number {
839
viewportStart = viewportStart | 0;
840
viewportEnd = viewportEnd | 0;
841
boxStart = boxStart | 0;
842
boxEnd = boxEnd | 0;
843
revealAtStart = !!revealAtStart;
844
revealAtEnd = !!revealAtEnd;
845
846
const viewportLength = viewportEnd - viewportStart;
847
const boxLength = boxEnd - boxStart;
848
849
if (boxLength < viewportLength) {
850
// The box would fit in the viewport
851
852
if (revealAtStart) {
853
return boxStart;
854
}
855
856
if (revealAtEnd) {
857
return Math.max(0, boxEnd - viewportLength);
858
}
859
860
if (boxStart < viewportStart) {
861
// The box is above the viewport
862
return boxStart;
863
} else if (boxEnd > viewportEnd) {
864
// The box is below the viewport
865
return Math.max(0, boxEnd - viewportLength);
866
}
867
} else {
868
// The box would not fit in the viewport
869
// Reveal the beginning of the box
870
return boxStart;
871
}
872
873
return viewportStart;
874
}
875
}
876
877