Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/viewLayout/viewLayout.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 { Event, Emitter } from '../../../base/common/event.js';
7
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
8
import { IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility, INewScrollPosition } from '../../../base/common/scrollable.js';
9
import { ConfigurationChangedEvent, EditorOption } from '../config/editorOptions.js';
10
import { ScrollType } from '../editorCommon.js';
11
import { IEditorConfiguration } from '../config/editorConfiguration.js';
12
import { LinesLayout } from './linesLayout.js';
13
import { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewLayout, IViewWhitespaceViewportData, IWhitespaceChangeAccessor, Viewport } from '../viewModel.js';
14
import { ContentSizeChangedEvent } from '../viewModelEventDispatcher.js';
15
import { ICustomLineHeightData } from './lineHeights.js';
16
17
const SMOOTH_SCROLLING_TIME = 125;
18
19
class EditorScrollDimensions {
20
21
public readonly width: number;
22
public readonly contentWidth: number;
23
public readonly scrollWidth: number;
24
25
public readonly height: number;
26
public readonly contentHeight: number;
27
public readonly scrollHeight: number;
28
29
constructor(
30
width: number,
31
contentWidth: number,
32
height: number,
33
contentHeight: number,
34
) {
35
width = width | 0;
36
contentWidth = contentWidth | 0;
37
height = height | 0;
38
contentHeight = contentHeight | 0;
39
40
if (width < 0) {
41
width = 0;
42
}
43
if (contentWidth < 0) {
44
contentWidth = 0;
45
}
46
47
if (height < 0) {
48
height = 0;
49
}
50
if (contentHeight < 0) {
51
contentHeight = 0;
52
}
53
54
this.width = width;
55
this.contentWidth = contentWidth;
56
this.scrollWidth = Math.max(width, contentWidth);
57
58
this.height = height;
59
this.contentHeight = contentHeight;
60
this.scrollHeight = Math.max(height, contentHeight);
61
}
62
63
public equals(other: EditorScrollDimensions): boolean {
64
return (
65
this.width === other.width
66
&& this.contentWidth === other.contentWidth
67
&& this.height === other.height
68
&& this.contentHeight === other.contentHeight
69
);
70
}
71
}
72
73
class EditorScrollable extends Disposable {
74
75
private readonly _scrollable: Scrollable;
76
private _dimensions: EditorScrollDimensions;
77
78
public readonly onDidScroll: Event<ScrollEvent>;
79
80
private readonly _onDidContentSizeChange = this._register(new Emitter<ContentSizeChangedEvent>());
81
public readonly onDidContentSizeChange: Event<ContentSizeChangedEvent> = this._onDidContentSizeChange.event;
82
83
constructor(smoothScrollDuration: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) {
84
super();
85
this._dimensions = new EditorScrollDimensions(0, 0, 0, 0);
86
this._scrollable = this._register(new Scrollable({
87
forceIntegerValues: true,
88
smoothScrollDuration,
89
scheduleAtNextAnimationFrame
90
}));
91
this.onDidScroll = this._scrollable.onScroll;
92
}
93
94
public getScrollable(): Scrollable {
95
return this._scrollable;
96
}
97
98
public setSmoothScrollDuration(smoothScrollDuration: number): void {
99
this._scrollable.setSmoothScrollDuration(smoothScrollDuration);
100
}
101
102
public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition {
103
return this._scrollable.validateScrollPosition(scrollPosition);
104
}
105
106
public getScrollDimensions(): EditorScrollDimensions {
107
return this._dimensions;
108
}
109
110
public setScrollDimensions(dimensions: EditorScrollDimensions): void {
111
if (this._dimensions.equals(dimensions)) {
112
return;
113
}
114
115
const oldDimensions = this._dimensions;
116
this._dimensions = dimensions;
117
118
this._scrollable.setScrollDimensions({
119
width: dimensions.width,
120
scrollWidth: dimensions.scrollWidth,
121
height: dimensions.height,
122
scrollHeight: dimensions.scrollHeight
123
}, true);
124
125
const contentWidthChanged = (oldDimensions.contentWidth !== dimensions.contentWidth);
126
const contentHeightChanged = (oldDimensions.contentHeight !== dimensions.contentHeight);
127
if (contentWidthChanged || contentHeightChanged) {
128
this._onDidContentSizeChange.fire(new ContentSizeChangedEvent(
129
oldDimensions.contentWidth, oldDimensions.contentHeight,
130
dimensions.contentWidth, dimensions.contentHeight
131
));
132
}
133
}
134
135
public getFutureScrollPosition(): IScrollPosition {
136
return this._scrollable.getFutureScrollPosition();
137
}
138
139
public getCurrentScrollPosition(): IScrollPosition {
140
return this._scrollable.getCurrentScrollPosition();
141
}
142
143
public setScrollPositionNow(update: INewScrollPosition): void {
144
this._scrollable.setScrollPositionNow(update);
145
}
146
147
public setScrollPositionSmooth(update: INewScrollPosition): void {
148
this._scrollable.setScrollPositionSmooth(update);
149
}
150
151
public hasPendingScrollAnimation(): boolean {
152
return this._scrollable.hasPendingScrollAnimation();
153
}
154
}
155
156
export class ViewLayout extends Disposable implements IViewLayout {
157
158
private readonly _configuration: IEditorConfiguration;
159
private readonly _linesLayout: LinesLayout;
160
private _maxLineWidth: number;
161
private _overlayWidgetsMinWidth: number;
162
163
private readonly _scrollable: EditorScrollable;
164
public readonly onDidScroll: Event<ScrollEvent>;
165
public readonly onDidContentSizeChange: Event<ContentSizeChangedEvent>;
166
167
constructor(configuration: IEditorConfiguration, lineCount: number, customLineHeightData: ICustomLineHeightData[], scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) {
168
super();
169
170
this._configuration = configuration;
171
const options = this._configuration.options;
172
const layoutInfo = options.get(EditorOption.layoutInfo);
173
const padding = options.get(EditorOption.padding);
174
175
this._linesLayout = new LinesLayout(lineCount, options.get(EditorOption.lineHeight), padding.top, padding.bottom, customLineHeightData);
176
this._maxLineWidth = 0;
177
this._overlayWidgetsMinWidth = 0;
178
179
this._scrollable = this._register(new EditorScrollable(0, scheduleAtNextAnimationFrame));
180
this._configureSmoothScrollDuration();
181
182
this._scrollable.setScrollDimensions(new EditorScrollDimensions(
183
layoutInfo.contentWidth,
184
0,
185
layoutInfo.height,
186
0
187
));
188
this.onDidScroll = this._scrollable.onDidScroll;
189
this.onDidContentSizeChange = this._scrollable.onDidContentSizeChange;
190
191
this._updateHeight();
192
}
193
194
public override dispose(): void {
195
super.dispose();
196
}
197
198
public getScrollable(): Scrollable {
199
return this._scrollable.getScrollable();
200
}
201
202
public onHeightMaybeChanged(): void {
203
this._updateHeight();
204
}
205
206
private _configureSmoothScrollDuration(): void {
207
this._scrollable.setSmoothScrollDuration(this._configuration.options.get(EditorOption.smoothScrolling) ? SMOOTH_SCROLLING_TIME : 0);
208
}
209
210
// ---- begin view event handlers
211
212
public onConfigurationChanged(e: ConfigurationChangedEvent): void {
213
const options = this._configuration.options;
214
if (e.hasChanged(EditorOption.lineHeight)) {
215
this._linesLayout.setDefaultLineHeight(options.get(EditorOption.lineHeight));
216
}
217
if (e.hasChanged(EditorOption.padding)) {
218
const padding = options.get(EditorOption.padding);
219
this._linesLayout.setPadding(padding.top, padding.bottom);
220
}
221
if (e.hasChanged(EditorOption.layoutInfo)) {
222
const layoutInfo = options.get(EditorOption.layoutInfo);
223
const width = layoutInfo.contentWidth;
224
const height = layoutInfo.height;
225
const scrollDimensions = this._scrollable.getScrollDimensions();
226
const contentWidth = scrollDimensions.contentWidth;
227
this._scrollable.setScrollDimensions(new EditorScrollDimensions(
228
width,
229
scrollDimensions.contentWidth,
230
height,
231
this._getContentHeight(width, height, contentWidth)
232
));
233
} else {
234
this._updateHeight();
235
}
236
if (e.hasChanged(EditorOption.smoothScrolling)) {
237
this._configureSmoothScrollDuration();
238
}
239
}
240
public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void {
241
this._linesLayout.onFlushed(lineCount, customLineHeightData);
242
}
243
public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void {
244
this._linesLayout.onLinesDeleted(fromLineNumber, toLineNumber);
245
}
246
public onLinesInserted(fromLineNumber: number, toLineNumber: number): void {
247
this._linesLayout.onLinesInserted(fromLineNumber, toLineNumber);
248
}
249
250
// ---- end view event handlers
251
252
private _getHorizontalScrollbarHeight(width: number, scrollWidth: number): number {
253
const options = this._configuration.options;
254
const scrollbar = options.get(EditorOption.scrollbar);
255
if (scrollbar.horizontal === ScrollbarVisibility.Hidden) {
256
// horizontal scrollbar not visible
257
return 0;
258
}
259
if (width >= scrollWidth) {
260
// horizontal scrollbar not visible
261
return 0;
262
}
263
return scrollbar.horizontalScrollbarSize;
264
}
265
266
private _getContentHeight(width: number, height: number, contentWidth: number): number {
267
const options = this._configuration.options;
268
269
let result = this._linesLayout.getLinesTotalHeight();
270
if (options.get(EditorOption.scrollBeyondLastLine)) {
271
result += Math.max(0, height - options.get(EditorOption.lineHeight) - options.get(EditorOption.padding).bottom);
272
} else if (!options.get(EditorOption.scrollbar).ignoreHorizontalScrollbarInContentHeight) {
273
result += this._getHorizontalScrollbarHeight(width, contentWidth);
274
}
275
276
return result;
277
}
278
279
private _updateHeight(): void {
280
const scrollDimensions = this._scrollable.getScrollDimensions();
281
const width = scrollDimensions.width;
282
const height = scrollDimensions.height;
283
const contentWidth = scrollDimensions.contentWidth;
284
this._scrollable.setScrollDimensions(new EditorScrollDimensions(
285
width,
286
scrollDimensions.contentWidth,
287
height,
288
this._getContentHeight(width, height, contentWidth)
289
));
290
}
291
292
// ---- Layouting logic
293
294
public getCurrentViewport(): Viewport {
295
const scrollDimensions = this._scrollable.getScrollDimensions();
296
const currentScrollPosition = this._scrollable.getCurrentScrollPosition();
297
return new Viewport(
298
currentScrollPosition.scrollTop,
299
currentScrollPosition.scrollLeft,
300
scrollDimensions.width,
301
scrollDimensions.height
302
);
303
}
304
305
public getFutureViewport(): Viewport {
306
const scrollDimensions = this._scrollable.getScrollDimensions();
307
const currentScrollPosition = this._scrollable.getFutureScrollPosition();
308
return new Viewport(
309
currentScrollPosition.scrollTop,
310
currentScrollPosition.scrollLeft,
311
scrollDimensions.width,
312
scrollDimensions.height
313
);
314
}
315
316
private _computeContentWidth(): number {
317
const options = this._configuration.options;
318
const maxLineWidth = this._maxLineWidth;
319
const wrappingInfo = options.get(EditorOption.wrappingInfo);
320
const fontInfo = options.get(EditorOption.fontInfo);
321
const layoutInfo = options.get(EditorOption.layoutInfo);
322
if (wrappingInfo.isViewportWrapping) {
323
const minimap = options.get(EditorOption.minimap);
324
if (maxLineWidth > layoutInfo.contentWidth + fontInfo.typicalHalfwidthCharacterWidth) {
325
// This is a case where viewport wrapping is on, but the line extends above the viewport
326
if (minimap.enabled && minimap.side === 'right') {
327
// We need to accomodate the scrollbar width
328
return maxLineWidth + layoutInfo.verticalScrollbarWidth;
329
}
330
}
331
return maxLineWidth;
332
} else {
333
const extraHorizontalSpace = options.get(EditorOption.scrollBeyondLastColumn) * fontInfo.typicalHalfwidthCharacterWidth;
334
const whitespaceMinWidth = this._linesLayout.getWhitespaceMinWidth();
335
return Math.max(maxLineWidth + extraHorizontalSpace + layoutInfo.verticalScrollbarWidth, whitespaceMinWidth, this._overlayWidgetsMinWidth);
336
}
337
}
338
339
public setMaxLineWidth(maxLineWidth: number): void {
340
this._maxLineWidth = maxLineWidth;
341
this._updateContentWidth();
342
}
343
344
public setOverlayWidgetsMinWidth(maxMinWidth: number): void {
345
this._overlayWidgetsMinWidth = maxMinWidth;
346
this._updateContentWidth();
347
}
348
349
private _updateContentWidth(): void {
350
const scrollDimensions = this._scrollable.getScrollDimensions();
351
this._scrollable.setScrollDimensions(new EditorScrollDimensions(
352
scrollDimensions.width,
353
this._computeContentWidth(),
354
scrollDimensions.height,
355
scrollDimensions.contentHeight
356
));
357
358
// The height might depend on the fact that there is a horizontal scrollbar or not
359
this._updateHeight();
360
}
361
362
// ---- view state
363
364
public saveState(): { scrollTop: number; scrollTopWithoutViewZones: number; scrollLeft: number } {
365
const currentScrollPosition = this._scrollable.getFutureScrollPosition();
366
const scrollTop = currentScrollPosition.scrollTop;
367
const firstLineNumberInViewport = this._linesLayout.getLineNumberAtOrAfterVerticalOffset(scrollTop);
368
const whitespaceAboveFirstLine = this._linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(firstLineNumberInViewport);
369
return {
370
scrollTop: scrollTop,
371
scrollTopWithoutViewZones: scrollTop - whitespaceAboveFirstLine,
372
scrollLeft: currentScrollPosition.scrollLeft
373
};
374
}
375
376
// ----
377
public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): boolean {
378
const hadAChange = this._linesLayout.changeWhitespace(callback);
379
if (hadAChange) {
380
this.onHeightMaybeChanged();
381
}
382
return hadAChange;
383
}
384
385
public changeSpecialLineHeights(callback: (accessor: ILineHeightChangeAccessor) => void): boolean {
386
const hadAChange = this._linesLayout.changeLineHeights(callback);
387
if (hadAChange) {
388
this.onHeightMaybeChanged();
389
}
390
return hadAChange;
391
}
392
393
public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones: boolean = false): number {
394
return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber, includeViewZones);
395
}
396
public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones: boolean = false): number {
397
return this._linesLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones);
398
}
399
public getLineHeightForLineNumber(lineNumber: number): number {
400
return this._linesLayout.getLineHeightForLineNumber(lineNumber);
401
}
402
public isAfterLines(verticalOffset: number): boolean {
403
return this._linesLayout.isAfterLines(verticalOffset);
404
}
405
public isInTopPadding(verticalOffset: number): boolean {
406
return this._linesLayout.isInTopPadding(verticalOffset);
407
}
408
public isInBottomPadding(verticalOffset: number): boolean {
409
return this._linesLayout.isInBottomPadding(verticalOffset);
410
}
411
412
public getLineNumberAtVerticalOffset(verticalOffset: number): number {
413
return this._linesLayout.getLineNumberAtOrAfterVerticalOffset(verticalOffset);
414
}
415
416
public getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null {
417
return this._linesLayout.getWhitespaceAtVerticalOffset(verticalOffset);
418
}
419
public getLinesViewportData(): IPartialViewLinesViewportData {
420
const visibleBox = this.getCurrentViewport();
421
return this._linesLayout.getLinesViewportData(visibleBox.top, visibleBox.top + visibleBox.height);
422
}
423
public getLinesViewportDataAtScrollTop(scrollTop: number): IPartialViewLinesViewportData {
424
// do some minimal validations on scrollTop
425
const scrollDimensions = this._scrollable.getScrollDimensions();
426
if (scrollTop + scrollDimensions.height > scrollDimensions.scrollHeight) {
427
scrollTop = scrollDimensions.scrollHeight - scrollDimensions.height;
428
}
429
if (scrollTop < 0) {
430
scrollTop = 0;
431
}
432
return this._linesLayout.getLinesViewportData(scrollTop, scrollTop + scrollDimensions.height);
433
}
434
public getWhitespaceViewportData(): IViewWhitespaceViewportData[] {
435
const visibleBox = this.getCurrentViewport();
436
return this._linesLayout.getWhitespaceViewportData(visibleBox.top, visibleBox.top + visibleBox.height);
437
}
438
public getWhitespaces(): IEditorWhitespace[] {
439
return this._linesLayout.getWhitespaces();
440
}
441
442
// ----
443
444
public getContentWidth(): number {
445
const scrollDimensions = this._scrollable.getScrollDimensions();
446
return scrollDimensions.contentWidth;
447
}
448
public getScrollWidth(): number {
449
const scrollDimensions = this._scrollable.getScrollDimensions();
450
return scrollDimensions.scrollWidth;
451
}
452
public getContentHeight(): number {
453
const scrollDimensions = this._scrollable.getScrollDimensions();
454
return scrollDimensions.contentHeight;
455
}
456
public getScrollHeight(): number {
457
const scrollDimensions = this._scrollable.getScrollDimensions();
458
return scrollDimensions.scrollHeight;
459
}
460
461
public getCurrentScrollLeft(): number {
462
const currentScrollPosition = this._scrollable.getCurrentScrollPosition();
463
return currentScrollPosition.scrollLeft;
464
}
465
public getCurrentScrollTop(): number {
466
const currentScrollPosition = this._scrollable.getCurrentScrollPosition();
467
return currentScrollPosition.scrollTop;
468
}
469
470
public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition {
471
return this._scrollable.validateScrollPosition(scrollPosition);
472
}
473
474
public setScrollPosition(position: INewScrollPosition, type: ScrollType): void {
475
if (type === ScrollType.Immediate) {
476
this._scrollable.setScrollPositionNow(position);
477
} else {
478
this._scrollable.setScrollPositionSmooth(position);
479
}
480
}
481
482
public hasPendingScrollAnimation(): boolean {
483
return this._scrollable.hasPendingScrollAnimation();
484
}
485
486
public deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void {
487
const currentScrollPosition = this._scrollable.getCurrentScrollPosition();
488
this._scrollable.setScrollPositionNow({
489
scrollLeft: currentScrollPosition.scrollLeft + deltaScrollLeft,
490
scrollTop: currentScrollPosition.scrollTop + deltaScrollTop
491
});
492
}
493
}
494
495