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
3296 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
if (true/*e.inlineDecorationsChanged*/) {
249
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
250
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
251
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
252
this._visibleLines.getVisibleLine(lineNumber).onDecorationsChanged();
253
}
254
}
255
return true;
256
}
257
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
258
const shouldRender = this._visibleLines.onFlushed(e, this._viewLineOptions.useGpu);
259
this._maxLineWidth = 0;
260
return shouldRender;
261
}
262
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
263
return this._visibleLines.onLinesChanged(e);
264
}
265
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
266
return this._visibleLines.onLinesDeleted(e);
267
}
268
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
269
return this._visibleLines.onLinesInserted(e);
270
}
271
public override onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean {
272
// Using the future viewport here in order to handle multiple
273
// incoming reveal range requests that might all desire to be animated
274
const desiredScrollTop = this._computeScrollTopToRevealRange(this._context.viewLayout.getFutureViewport(), e.source, e.minimalReveal, e.range, e.selections, e.verticalType);
275
276
if (desiredScrollTop === -1) {
277
// marker to abort the reveal range request
278
return false;
279
}
280
281
// validate the new desired scroll top
282
let newScrollPosition = this._context.viewLayout.validateScrollPosition({ scrollTop: desiredScrollTop });
283
284
if (e.revealHorizontal) {
285
if (e.range && e.range.startLineNumber !== e.range.endLineNumber) {
286
// Two or more lines? => scroll to base (That's how you see most of the two lines)
287
newScrollPosition = {
288
scrollTop: newScrollPosition.scrollTop,
289
scrollLeft: 0
290
};
291
} else if (e.range) {
292
// We don't necessarily know the horizontal offset of this range since the line might not be in the view...
293
this._horizontalRevealRequest = new HorizontalRevealRangeRequest(e.minimalReveal, e.range.startLineNumber, e.range.startColumn, e.range.endColumn, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
294
} else if (e.selections && e.selections.length > 0) {
295
this._horizontalRevealRequest = new HorizontalRevealSelectionsRequest(e.minimalReveal, e.selections, this._context.viewLayout.getCurrentScrollTop(), newScrollPosition.scrollTop, e.scrollType);
296
}
297
} else {
298
this._horizontalRevealRequest = null;
299
}
300
301
const scrollTopDelta = Math.abs(this._context.viewLayout.getCurrentScrollTop() - newScrollPosition.scrollTop);
302
const scrollType = (scrollTopDelta <= this._lineHeight ? ScrollType.Immediate : e.scrollType);
303
this._context.viewModel.viewLayout.setScrollPosition(newScrollPosition, scrollType);
304
305
return true;
306
}
307
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
308
if (this._horizontalRevealRequest && e.scrollLeftChanged) {
309
// cancel any outstanding horizontal reveal request if someone else scrolls horizontally.
310
this._horizontalRevealRequest = null;
311
}
312
if (this._horizontalRevealRequest && e.scrollTopChanged) {
313
const min = Math.min(this._horizontalRevealRequest.startScrollTop, this._horizontalRevealRequest.stopScrollTop);
314
const max = Math.max(this._horizontalRevealRequest.startScrollTop, this._horizontalRevealRequest.stopScrollTop);
315
if (e.scrollTop < min || e.scrollTop > max) {
316
// cancel any outstanding horizontal reveal request if someone else scrolls vertically.
317
this._horizontalRevealRequest = null;
318
}
319
}
320
this.domNode.setWidth(e.scrollWidth);
321
return this._visibleLines.onScrollChanged(e) || true;
322
}
323
324
public override onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean {
325
return this._visibleLines.onTokensChanged(e);
326
}
327
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
328
this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLineWidth);
329
return this._visibleLines.onZonesChanged(e);
330
}
331
public override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean {
332
return this._onOptionsMaybeChanged();
333
}
334
335
// ---- end view event handlers
336
337
// ----------- HELPERS FOR OTHERS
338
339
public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position | null {
340
const viewLineDomNode = this._getViewLineDomNode(spanNode);
341
if (viewLineDomNode === null) {
342
// Couldn't find view line node
343
return null;
344
}
345
const lineNumber = this._getLineNumberFor(viewLineDomNode);
346
347
if (lineNumber === -1) {
348
// Couldn't find view line node
349
return null;
350
}
351
352
if (lineNumber < 1 || lineNumber > this._context.viewModel.getLineCount()) {
353
// lineNumber is outside range
354
return null;
355
}
356
357
if (this._context.viewModel.getLineMaxColumn(lineNumber) === 1) {
358
// Line is empty
359
return new Position(lineNumber, 1);
360
}
361
362
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
363
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
364
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
365
// Couldn't find line
366
return null;
367
}
368
369
let column = this._visibleLines.getVisibleLine(lineNumber).getColumnOfNodeOffset(spanNode, offset);
370
const minColumn = this._context.viewModel.getLineMinColumn(lineNumber);
371
if (column < minColumn) {
372
column = minColumn;
373
}
374
return new Position(lineNumber, column);
375
}
376
377
private _getViewLineDomNode(node: HTMLElement | null): HTMLElement | null {
378
while (node && node.nodeType === 1) {
379
if (node.className === ViewLine.CLASS_NAME) {
380
return node;
381
}
382
node = node.parentElement;
383
}
384
return null;
385
}
386
387
/**
388
* @returns the line number of this view line dom node.
389
*/
390
private _getLineNumberFor(domNode: HTMLElement): number {
391
const startLineNumber = this._visibleLines.getStartLineNumber();
392
const endLineNumber = this._visibleLines.getEndLineNumber();
393
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
394
const line = this._visibleLines.getVisibleLine(lineNumber);
395
if (domNode === line.getDomNode()) {
396
return lineNumber;
397
}
398
}
399
return -1;
400
}
401
402
public getLineWidth(lineNumber: number): number {
403
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
404
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
405
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
406
// Couldn't find line
407
return -1;
408
}
409
410
const context = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
411
const result = this._visibleLines.getVisibleLine(lineNumber).getWidth(context);
412
this._updateLineWidthsSlowIfDomDidLayout(context);
413
414
return result;
415
}
416
417
public linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null {
418
if (this.shouldRender()) {
419
// Cannot read from the DOM because it is dirty
420
// i.e. the model & the dom are out of sync, so I'd be reading something stale
421
return null;
422
}
423
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 (this.shouldRender()) {
484
// Cannot read from the DOM because it is dirty
485
// i.e. the model & the dom are out of sync, so I'd be reading something stale
486
return null;
487
}
488
489
if (lineNumber < this._visibleLines.getStartLineNumber() || lineNumber > this._visibleLines.getEndLineNumber()) {
490
return null;
491
}
492
493
const domReadingContext = new DomReadingContext(this.domNode.domNode, this._textRangeRestingSpot);
494
const result = this._visibleLines.getVisibleLine(lineNumber).getVisibleRangesForRange(lineNumber, startColumn, endColumn, domReadingContext);
495
this._updateLineWidthsSlowIfDomDidLayout(domReadingContext);
496
497
return result;
498
}
499
500
private _lineIsRenderedRTL(lineNumber: number): boolean {
501
if (lineNumber < this._visibleLines.getStartLineNumber() || lineNumber > this._visibleLines.getEndLineNumber()) {
502
return false;
503
}
504
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
505
return visibleLine.isRenderedRTL();
506
}
507
508
public visibleRangeForPosition(position: Position): HorizontalPosition | null {
509
const visibleRanges = this._visibleRangesForLineRange(position.lineNumber, position.column, position.column);
510
if (!visibleRanges) {
511
return null;
512
}
513
return new HorizontalPosition(visibleRanges.outsideRenderedLine, visibleRanges.ranges[0].left);
514
}
515
516
// --- implementation
517
518
public updateLineWidths(): void {
519
this._updateLineWidths(false);
520
}
521
522
/**
523
* Updates the max line width if it is fast to compute.
524
* Returns true if all lines were taken into account.
525
* Returns false if some lines need to be reevaluated (in a slow fashion).
526
*/
527
private _updateLineWidthsFast(): boolean {
528
return this._updateLineWidths(true);
529
}
530
531
private _updateLineWidthsSlow(): void {
532
this._updateLineWidths(false);
533
}
534
535
/**
536
* Update the line widths using DOM layout information after someone else
537
* has caused a synchronous layout.
538
*/
539
private _updateLineWidthsSlowIfDomDidLayout(domReadingContext: DomReadingContext): void {
540
if (!domReadingContext.didDomLayout) {
541
// only proceed if we just did a layout
542
return;
543
}
544
if (this._asyncUpdateLineWidths.isScheduled()) {
545
// reading widths is not scheduled => widths are up-to-date
546
return;
547
}
548
this._asyncUpdateLineWidths.cancel();
549
this._updateLineWidthsSlow();
550
}
551
552
private _updateLineWidths(fast: boolean): boolean {
553
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
554
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
555
556
let localMaxLineWidth = 1;
557
let allWidthsComputed = true;
558
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
559
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
560
561
if (fast && !visibleLine.getWidthIsFast()) {
562
// Cannot compute width in a fast way for this line
563
allWidthsComputed = false;
564
continue;
565
}
566
567
localMaxLineWidth = Math.max(localMaxLineWidth, visibleLine.getWidth(null));
568
}
569
570
if (allWidthsComputed && rendStartLineNumber === 1 && rendEndLineNumber === this._context.viewModel.getLineCount()) {
571
// we know the max line width for all the lines
572
this._maxLineWidth = 0;
573
}
574
575
this._ensureMaxLineWidth(localMaxLineWidth);
576
577
return allWidthsComputed;
578
}
579
580
private _checkMonospaceFontAssumptions(): void {
581
// Problems with monospace assumptions are more apparent for longer lines,
582
// as small rounding errors start to sum up, so we will select the longest
583
// line for a closer inspection
584
let longestLineNumber = -1;
585
let longestWidth = -1;
586
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
587
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
588
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
589
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
590
if (visibleLine.needsMonospaceFontCheck()) {
591
const lineWidth = visibleLine.getWidth(null);
592
if (lineWidth > longestWidth) {
593
longestWidth = lineWidth;
594
longestLineNumber = lineNumber;
595
}
596
}
597
}
598
599
if (longestLineNumber === -1) {
600
return;
601
}
602
603
if (!this._visibleLines.getVisibleLine(longestLineNumber).monospaceAssumptionsAreValid()) {
604
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
605
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
606
visibleLine.onMonospaceAssumptionsInvalidated();
607
}
608
}
609
}
610
611
public prepareRender(): void {
612
throw new Error('Not supported');
613
}
614
615
public render(): void {
616
throw new Error('Not supported');
617
}
618
619
public renderText(viewportData: ViewportData): void {
620
// (1) render lines - ensures lines are in the DOM
621
this._visibleLines.renderLines(viewportData);
622
this._lastRenderedData.setCurrentVisibleRange(viewportData.visibleRange);
623
this.domNode.setWidth(this._context.viewLayout.getScrollWidth());
624
this.domNode.setHeight(Math.min(this._context.viewLayout.getScrollHeight(), 1000000));
625
626
// (2) compute horizontal scroll position:
627
// - this must happen after the lines are in the DOM since it might need a line that rendered just now
628
// - it might change `scrollWidth` and `scrollLeft`
629
if (this._horizontalRevealRequest) {
630
631
const horizontalRevealRequest = this._horizontalRevealRequest;
632
633
// Check that we have the line that contains the horizontal range in the viewport
634
if (viewportData.startLineNumber <= horizontalRevealRequest.minLineNumber && horizontalRevealRequest.maxLineNumber <= viewportData.endLineNumber) {
635
636
this._horizontalRevealRequest = null;
637
638
// allow `visibleRangesForRange2` to work
639
this.onDidRender();
640
641
// compute new scroll position
642
const newScrollLeft = this._computeScrollLeftToReveal(horizontalRevealRequest);
643
644
if (newScrollLeft) {
645
if (!this._isViewportWrapping && !newScrollLeft.hasRTL) {
646
// ensure `scrollWidth` is large enough
647
this._ensureMaxLineWidth(newScrollLeft.maxHorizontalOffset);
648
}
649
// set `scrollLeft`
650
this._context.viewModel.viewLayout.setScrollPosition({
651
scrollLeft: newScrollLeft.scrollLeft
652
}, horizontalRevealRequest.scrollType);
653
}
654
}
655
}
656
657
// Update max line width (not so important, it is just so the horizontal scrollbar doesn't get too small)
658
if (!this._updateLineWidthsFast()) {
659
// Computing the width of some lines would be slow => delay it
660
this._asyncUpdateLineWidths.schedule();
661
} else {
662
this._asyncUpdateLineWidths.cancel();
663
}
664
665
if (platform.isLinux && !this._asyncCheckMonospaceFontAssumptions.isScheduled()) {
666
const rendStartLineNumber = this._visibleLines.getStartLineNumber();
667
const rendEndLineNumber = this._visibleLines.getEndLineNumber();
668
for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) {
669
const visibleLine = this._visibleLines.getVisibleLine(lineNumber);
670
if (visibleLine.needsMonospaceFontCheck()) {
671
this._asyncCheckMonospaceFontAssumptions.schedule();
672
break;
673
}
674
}
675
}
676
677
// (3) handle scrolling
678
this._linesContent.setLayerHinting(this._canUseLayerHinting);
679
this._linesContent.setContain('strict');
680
const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta;
681
this._linesContent.setTop(-adjustedScrollTop);
682
this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft());
683
}
684
685
// --- width
686
687
private _ensureMaxLineWidth(lineWidth: number): void {
688
const iLineWidth = Math.ceil(lineWidth);
689
if (this._maxLineWidth < iLineWidth) {
690
this._maxLineWidth = iLineWidth;
691
this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLineWidth);
692
}
693
}
694
695
private _computeScrollTopToRevealRange(viewport: Viewport, source: string | null | undefined, minimalReveal: boolean, range: Range | null, selections: Selection[] | null, verticalType: viewEvents.VerticalRevealType): number {
696
const viewportStartY = viewport.top;
697
const viewportHeight = viewport.height;
698
const viewportEndY = viewportStartY + viewportHeight;
699
let boxIsSingleRange: boolean;
700
let boxStartY: number;
701
let boxEndY: number;
702
703
if (selections && selections.length > 0) {
704
let minLineNumber = selections[0].startLineNumber;
705
let maxLineNumber = selections[0].endLineNumber;
706
for (let i = 1, len = selections.length; i < len; i++) {
707
const selection = selections[i];
708
minLineNumber = Math.min(minLineNumber, selection.startLineNumber);
709
maxLineNumber = Math.max(maxLineNumber, selection.endLineNumber);
710
}
711
boxIsSingleRange = false;
712
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(minLineNumber);
713
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(maxLineNumber) + this._lineHeight;
714
} else if (range) {
715
boxIsSingleRange = true;
716
boxStartY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.startLineNumber);
717
boxEndY = this._context.viewLayout.getVerticalOffsetForLineNumber(range.endLineNumber) + this._lineHeight;
718
} else {
719
return -1;
720
}
721
722
const shouldIgnoreScrollOff = (source === 'mouse' || minimalReveal) && this._cursorSurroundingLinesStyle === 'default';
723
724
let paddingTop: number = 0;
725
let paddingBottom: number = 0;
726
727
if (!shouldIgnoreScrollOff) {
728
const maxLinesInViewport = (viewportHeight / this._lineHeight);
729
const surroundingLines = Math.max(this._cursorSurroundingLines, this._stickyScrollEnabled ? this._maxNumberStickyLines : 0);
730
const context = Math.min(maxLinesInViewport / 2, surroundingLines);
731
paddingTop = context * this._lineHeight;
732
paddingBottom = Math.max(0, (context - 1)) * this._lineHeight;
733
} else {
734
if (!minimalReveal) {
735
// Reveal one more line above (this case is hit when dragging)
736
paddingTop = this._lineHeight;
737
}
738
}
739
if (!minimalReveal) {
740
if (verticalType === viewEvents.VerticalRevealType.Simple || verticalType === viewEvents.VerticalRevealType.Bottom) {
741
// Reveal one line more when the last line would be covered by the scrollbar - arrow down case or revealing a line explicitly at bottom
742
paddingBottom += this._lineHeight;
743
}
744
}
745
746
boxStartY -= paddingTop;
747
boxEndY += paddingBottom;
748
let newScrollTop: number;
749
750
if (boxEndY - boxStartY > viewportHeight) {
751
// the box is larger than the viewport ... scroll to its top
752
if (!boxIsSingleRange) {
753
// do not reveal multiple cursors if there are more than fit the viewport
754
return -1;
755
}
756
newScrollTop = boxStartY;
757
} else if (verticalType === viewEvents.VerticalRevealType.NearTop || verticalType === viewEvents.VerticalRevealType.NearTopIfOutsideViewport) {
758
if (verticalType === viewEvents.VerticalRevealType.NearTopIfOutsideViewport && viewportStartY <= boxStartY && boxEndY <= viewportEndY) {
759
// Box is already in the viewport... do nothing
760
newScrollTop = viewportStartY;
761
} else {
762
// We want a gap that is 20% of the viewport, but with a minimum of 5 lines
763
const desiredGapAbove = Math.max(5 * this._lineHeight, viewportHeight * 0.2);
764
// Try to scroll just above the box with the desired gap
765
const desiredScrollTop = boxStartY - desiredGapAbove;
766
// But ensure that the box is not pushed out of viewport
767
const minScrollTop = boxEndY - viewportHeight;
768
newScrollTop = Math.max(minScrollTop, desiredScrollTop);
769
}
770
} else if (verticalType === viewEvents.VerticalRevealType.Center || verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport) {
771
if (verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport && viewportStartY <= boxStartY && boxEndY <= viewportEndY) {
772
// Box is already in the viewport... do nothing
773
newScrollTop = viewportStartY;
774
} else {
775
// Box is outside the viewport... center it
776
const boxMiddleY = (boxStartY + boxEndY) / 2;
777
newScrollTop = Math.max(0, boxMiddleY - viewportHeight / 2);
778
}
779
} else {
780
newScrollTop = this._computeMinimumScrolling(viewportStartY, viewportEndY, boxStartY, boxEndY, verticalType === viewEvents.VerticalRevealType.Top, verticalType === viewEvents.VerticalRevealType.Bottom);
781
}
782
783
return newScrollTop;
784
}
785
786
private _computeScrollLeftToReveal(horizontalRevealRequest: HorizontalRevealRequest): { scrollLeft: number; maxHorizontalOffset: number; hasRTL: boolean } | null {
787
788
const viewport = this._context.viewLayout.getCurrentViewport();
789
const layoutInfo = this._context.configuration.options.get(EditorOption.layoutInfo);
790
const viewportStartX = viewport.left;
791
const viewportEndX = viewportStartX + viewport.width - layoutInfo.verticalScrollbarWidth;
792
793
let boxStartX = Constants.MAX_SAFE_SMALL_INTEGER;
794
let boxEndX = 0;
795
let hasRTL = false;
796
if (horizontalRevealRequest.type === 'range') {
797
hasRTL = this._lineIsRenderedRTL(horizontalRevealRequest.lineNumber);
798
const visibleRanges = this._visibleRangesForLineRange(horizontalRevealRequest.lineNumber, horizontalRevealRequest.startColumn, horizontalRevealRequest.endColumn);
799
if (!visibleRanges) {
800
return null;
801
}
802
for (const visibleRange of visibleRanges.ranges) {
803
boxStartX = Math.min(boxStartX, Math.round(visibleRange.left));
804
boxEndX = Math.max(boxEndX, Math.round(visibleRange.left + visibleRange.width));
805
}
806
} else {
807
for (const selection of horizontalRevealRequest.selections) {
808
if (selection.startLineNumber !== selection.endLineNumber) {
809
return null;
810
}
811
const visibleRanges = this._visibleRangesForLineRange(selection.startLineNumber, selection.startColumn, selection.endColumn);
812
hasRTL ||= this._lineIsRenderedRTL(selection.startLineNumber);
813
if (!visibleRanges) {
814
return null;
815
}
816
for (const visibleRange of visibleRanges.ranges) {
817
boxStartX = Math.min(boxStartX, Math.round(visibleRange.left));
818
boxEndX = Math.max(boxEndX, Math.round(visibleRange.left + visibleRange.width));
819
}
820
}
821
}
822
823
if (!horizontalRevealRequest.minimalReveal) {
824
boxStartX = Math.max(0, boxStartX - ViewLines.HORIZONTAL_EXTRA_PX);
825
boxEndX += this._revealHorizontalRightPadding;
826
}
827
828
if (horizontalRevealRequest.type === 'selections' && boxEndX - boxStartX > viewport.width) {
829
return null;
830
}
831
832
const newScrollLeft = this._computeMinimumScrolling(viewportStartX, viewportEndX, boxStartX, boxEndX);
833
return {
834
scrollLeft: newScrollLeft,
835
maxHorizontalOffset: boxEndX,
836
hasRTL
837
};
838
}
839
840
private _computeMinimumScrolling(viewportStart: number, viewportEnd: number, boxStart: number, boxEnd: number, revealAtStart?: boolean, revealAtEnd?: boolean): number {
841
viewportStart = viewportStart | 0;
842
viewportEnd = viewportEnd | 0;
843
boxStart = boxStart | 0;
844
boxEnd = boxEnd | 0;
845
revealAtStart = !!revealAtStart;
846
revealAtEnd = !!revealAtEnd;
847
848
const viewportLength = viewportEnd - viewportStart;
849
const boxLength = boxEnd - boxStart;
850
851
if (boxLength < viewportLength) {
852
// The box would fit in the viewport
853
854
if (revealAtStart) {
855
return boxStart;
856
}
857
858
if (revealAtEnd) {
859
return Math.max(0, boxEnd - viewportLength);
860
}
861
862
if (boxStart < viewportStart) {
863
// The box is above the viewport
864
return boxStart;
865
} else if (boxEnd > viewportEnd) {
866
// The box is below the viewport
867
return Math.max(0, boxEnd - viewportLength);
868
}
869
} else {
870
// The box would not fit in the viewport
871
// Reveal the beginning of the box
872
return boxStart;
873
}
874
875
return viewportStart;
876
}
877
}
878
879