Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts
5274 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 { IDisposable, Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
7
import { ICodeEditor, MouseTargetType } from '../../../browser/editorBrowser.js';
8
import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';
9
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
10
import { EditorOption, RenderLineNumbersType, ConfigurationChangedEvent } from '../../../common/config/editorOptions.js';
11
import { StickyScrollWidget, StickyScrollWidgetState } from './stickyScrollWidget.js';
12
import { IStickyLineCandidateProvider, StickyLineCandidateProvider } from './stickyScrollProvider.js';
13
import { IModelTokensChangedEvent } from '../../../common/textModelEvents.js';
14
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
15
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
16
import { MenuId } from '../../../../platform/actions/common/actions.js';
17
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
18
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
19
import { ClickLinkGesture, ClickLinkMouseEvent } from '../../gotoSymbol/browser/link/clickLinkGesture.js';
20
import { IRange, Range } from '../../../common/core/range.js';
21
import { getDefinitionsAtPosition } from '../../gotoSymbol/browser/goToSymbol.js';
22
import { goToDefinitionWithLocation } from '../../inlayHints/browser/inlayHintsLocations.js';
23
import { IPosition, Position } from '../../../common/core/position.js';
24
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
25
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
26
import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';
27
import * as dom from '../../../../base/browser/dom.js';
28
import { StickyRange } from './stickyScrollElement.js';
29
import { IMouseEvent, StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
30
import { FoldingController } from '../../folding/browser/folding.js';
31
import { FoldingModel, toggleCollapseState } from '../../folding/browser/foldingModel.js';
32
import { Emitter, Event } from '../../../../base/common/event.js';
33
import { mainWindow } from '../../../../base/browser/window.js';
34
35
export interface IStickyScrollController {
36
get stickyScrollCandidateProvider(): IStickyLineCandidateProvider;
37
get stickyScrollWidgetState(): StickyScrollWidgetState;
38
readonly stickyScrollWidgetHeight: number;
39
isFocused(): boolean;
40
focus(): void;
41
focusNext(): void;
42
focusPrevious(): void;
43
goToFocused(): void;
44
findScrollWidgetState(): StickyScrollWidgetState;
45
dispose(): void;
46
selectEditor(): void;
47
readonly onDidChangeStickyScrollHeight: Event<{ height: number }>;
48
}
49
50
export class StickyScrollController extends Disposable implements IEditorContribution, IStickyScrollController {
51
52
static readonly ID = 'store.contrib.stickyScrollController';
53
54
private readonly _stickyScrollWidget: StickyScrollWidget;
55
private readonly _stickyLineCandidateProvider: IStickyLineCandidateProvider;
56
private readonly _sessionStore: DisposableStore = new DisposableStore();
57
58
private _widgetState: StickyScrollWidgetState;
59
private _foldingModel: FoldingModel | undefined;
60
private _maxStickyLines: number = Number.MAX_SAFE_INTEGER;
61
62
private _stickyRangeProjectedOnEditor: IRange | undefined;
63
private _candidateDefinitionsLength: number = -1;
64
65
private _stickyScrollFocusedContextKey: IContextKey<boolean>;
66
private _stickyScrollVisibleContextKey: IContextKey<boolean>;
67
68
private _focusDisposableStore: DisposableStore | undefined;
69
private _focusedStickyElementIndex: number = -1;
70
private _enabled = false;
71
private _focused = false;
72
private _positionRevealed = false;
73
private _onMouseDown = false;
74
private _endLineNumbers: number[] = [];
75
private _showEndForLine: number | undefined;
76
private _minRebuildFromLine: number | undefined;
77
private _mouseTarget: EventTarget | null = null;
78
private _cursorPositionListener: IDisposable | undefined;
79
private _positionLineNumber: number | undefined;
80
81
private readonly _onDidChangeStickyScrollHeight = this._register(new Emitter<{ height: number }>());
82
public readonly onDidChangeStickyScrollHeight = this._onDidChangeStickyScrollHeight.event;
83
84
constructor(
85
private readonly _editor: ICodeEditor,
86
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
87
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
88
@IInstantiationService private readonly _instaService: IInstantiationService,
89
@ILanguageConfigurationService _languageConfigurationService: ILanguageConfigurationService,
90
@ILanguageFeatureDebounceService _languageFeatureDebounceService: ILanguageFeatureDebounceService,
91
@IContextKeyService private readonly _contextKeyService: IContextKeyService
92
) {
93
super();
94
this._stickyScrollWidget = new StickyScrollWidget(this._editor);
95
this._stickyLineCandidateProvider = new StickyLineCandidateProvider(this._editor, _languageFeaturesService, _languageConfigurationService);
96
this._register(this._stickyScrollWidget);
97
this._register(this._stickyLineCandidateProvider);
98
99
this._widgetState = StickyScrollWidgetState.Empty;
100
const stickyScrollDomNode = this._stickyScrollWidget.getDomNode();
101
this._register(this._editor.onDidChangeLineHeight((e) => {
102
e.changes.forEach((change) => {
103
const lineNumber = change.lineNumber;
104
if (this._widgetState.startLineNumbers.includes(lineNumber)) {
105
this._renderStickyScroll(lineNumber);
106
}
107
});
108
}));
109
this._register(this._editor.onDidChangeFont((e) => {
110
e.changes.forEach((change) => {
111
const lineNumber = change.lineNumber;
112
if (this._widgetState.startLineNumbers.includes(lineNumber)) {
113
this._renderStickyScroll(lineNumber);
114
}
115
});
116
}));
117
this._register(this._editor.onDidChangeConfiguration(e => {
118
this._readConfigurationChange(e);
119
}));
120
this._register(dom.addDisposableListener(stickyScrollDomNode, dom.EventType.CONTEXT_MENU, async (event: MouseEvent) => {
121
this._onContextMenu(dom.getWindow(stickyScrollDomNode), event);
122
}));
123
this._stickyScrollFocusedContextKey = EditorContextKeys.stickyScrollFocused.bindTo(this._contextKeyService);
124
this._stickyScrollVisibleContextKey = EditorContextKeys.stickyScrollVisible.bindTo(this._contextKeyService);
125
const focusTracker = this._register(dom.trackFocus(stickyScrollDomNode));
126
this._register(focusTracker.onDidBlur(_ => {
127
// Suppose that the blurring is caused by scrolling, then keep the focus on the sticky scroll
128
// This is determined by the fact that the height of the widget has become zero and there has been no position revealing
129
if (this._positionRevealed === false && stickyScrollDomNode.clientHeight === 0) {
130
this._focusedStickyElementIndex = -1;
131
this.focus();
132
133
}
134
// In all other casees, dispose the focus on the sticky scroll
135
else {
136
this._disposeFocusStickyScrollStore();
137
}
138
}));
139
this._register(focusTracker.onDidFocus(_ => {
140
this.focus();
141
}));
142
this._registerMouseListeners();
143
// Suppose that mouse down on the sticky scroll, then do not focus on the sticky scroll because this will be followed by the revealing of a position
144
this._register(dom.addDisposableListener(stickyScrollDomNode, dom.EventType.MOUSE_DOWN, (e) => {
145
this._onMouseDown = true;
146
}));
147
this._register(this._stickyScrollWidget.onDidChangeStickyScrollHeight((e) => {
148
this._onDidChangeStickyScrollHeight.fire(e);
149
}));
150
this._onDidResize();
151
this._readConfiguration();
152
}
153
154
get stickyScrollCandidateProvider(): IStickyLineCandidateProvider {
155
return this._stickyLineCandidateProvider;
156
}
157
158
get stickyScrollWidgetState(): StickyScrollWidgetState {
159
return this._widgetState;
160
}
161
162
get stickyScrollWidgetHeight(): number {
163
return this._stickyScrollWidget.height;
164
}
165
166
public static get(editor: ICodeEditor): IStickyScrollController | null {
167
return editor.getContribution<StickyScrollController>(StickyScrollController.ID);
168
}
169
170
private _disposeFocusStickyScrollStore() {
171
this._stickyScrollFocusedContextKey.set(false);
172
this._focusDisposableStore?.dispose();
173
this._focused = false;
174
this._positionRevealed = false;
175
this._onMouseDown = false;
176
}
177
178
public isFocused(): boolean {
179
return this._focused;
180
}
181
182
public focus(): void {
183
// If the mouse is down, do not focus on the sticky scroll
184
if (this._onMouseDown) {
185
this._onMouseDown = false;
186
this._editor.focus();
187
return;
188
}
189
const focusState = this._stickyScrollFocusedContextKey.get();
190
if (focusState === true) {
191
return;
192
}
193
this._focused = true;
194
this._focusDisposableStore = new DisposableStore();
195
this._stickyScrollFocusedContextKey.set(true);
196
this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumbers.length - 1;
197
this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex);
198
}
199
200
public focusNext(): void {
201
if (this._focusedStickyElementIndex < this._stickyScrollWidget.lineNumberCount - 1) {
202
this._focusNav(true);
203
}
204
}
205
206
public focusPrevious(): void {
207
if (this._focusedStickyElementIndex > 0) {
208
this._focusNav(false);
209
}
210
}
211
212
public selectEditor(): void {
213
this._editor.focus();
214
}
215
216
// True is next, false is previous
217
private _focusNav(direction: boolean): void {
218
this._focusedStickyElementIndex = direction ? this._focusedStickyElementIndex + 1 : this._focusedStickyElementIndex - 1;
219
this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex);
220
}
221
222
public goToFocused(): void {
223
const lineNumbers = this._stickyScrollWidget.lineNumbers;
224
this._disposeFocusStickyScrollStore();
225
this._revealPosition({ lineNumber: lineNumbers[this._focusedStickyElementIndex], column: 1 });
226
}
227
228
private _revealPosition(position: IPosition): void {
229
this._reveaInEditor(position, () => this._editor.revealPosition(position));
230
}
231
232
private _revealLineInCenterIfOutsideViewport(position: IPosition): void {
233
this._reveaInEditor(position, () => this._editor.revealLineInCenterIfOutsideViewport(position.lineNumber, ScrollType.Smooth));
234
}
235
236
private _reveaInEditor(position: IPosition, revealFunction: () => void): void {
237
if (this._focused) {
238
this._disposeFocusStickyScrollStore();
239
}
240
this._positionRevealed = true;
241
revealFunction();
242
this._editor.setSelection(Range.fromPositions(position));
243
this._editor.focus();
244
}
245
246
private _registerMouseListeners(): void {
247
248
const sessionStore = this._register(new DisposableStore());
249
const gesture = this._register(new ClickLinkGesture(this._editor, {
250
extractLineNumberFromMouseEvent: (e) => {
251
const position = this._stickyScrollWidget.getEditorPositionFromNode(e.target.element);
252
return position ? position.lineNumber : 0;
253
}
254
}));
255
256
const getMouseEventTarget = (mouseEvent: ClickLinkMouseEvent): { range: Range; textElement: HTMLElement } | null => {
257
if (!this._editor.hasModel()) {
258
return null;
259
}
260
if (mouseEvent.target.type !== MouseTargetType.OVERLAY_WIDGET || mouseEvent.target.detail !== this._stickyScrollWidget.getId()) {
261
// not hovering over our widget
262
return null;
263
}
264
const mouseTargetElement = mouseEvent.target.element;
265
if (!mouseTargetElement || mouseTargetElement.innerText !== mouseTargetElement.innerHTML) {
266
// not on a span element rendering text
267
return null;
268
}
269
const position = this._stickyScrollWidget.getEditorPositionFromNode(mouseTargetElement);
270
if (!position) {
271
// not hovering a sticky scroll line
272
return null;
273
}
274
return {
275
range: new Range(position.lineNumber, position.column, position.lineNumber, position.column + mouseTargetElement.innerText.length),
276
textElement: mouseTargetElement
277
};
278
};
279
280
const stickyScrollWidgetDomNode = this._stickyScrollWidget.getDomNode();
281
this._register(dom.addStandardDisposableListener(stickyScrollWidgetDomNode, dom.EventType.CLICK, (mouseEvent: IMouseEvent) => {
282
if (mouseEvent.ctrlKey || mouseEvent.altKey || mouseEvent.metaKey) {
283
// modifier pressed
284
return;
285
}
286
if (!mouseEvent.leftButton) {
287
// not left click
288
return;
289
}
290
if (mouseEvent.shiftKey) {
291
// shift click
292
const lineIndex = this._stickyScrollWidget.getLineIndexFromChildDomNode(mouseEvent.target);
293
if (lineIndex === null) {
294
return;
295
}
296
const position = new Position(this._endLineNumbers[lineIndex], 1);
297
this._revealLineInCenterIfOutsideViewport(position);
298
return;
299
}
300
const isInFoldingIconDomNode = this._stickyScrollWidget.isInFoldingIconDomNode(mouseEvent.target);
301
if (isInFoldingIconDomNode) {
302
// clicked on folding icon
303
const lineNumber = this._stickyScrollWidget.getLineNumberFromChildDomNode(mouseEvent.target);
304
this._toggleFoldingRegionForLine(lineNumber);
305
return;
306
}
307
const isInStickyLine = this._stickyScrollWidget.isInStickyLine(mouseEvent.target);
308
if (!isInStickyLine) {
309
return;
310
}
311
// normal click
312
let position = this._stickyScrollWidget.getEditorPositionFromNode(mouseEvent.target);
313
if (!position) {
314
const lineNumber = this._stickyScrollWidget.getLineNumberFromChildDomNode(mouseEvent.target);
315
if (lineNumber === null) {
316
// not hovering a sticky scroll line
317
return;
318
}
319
position = new Position(lineNumber, 1);
320
}
321
this._revealPosition(position);
322
}));
323
this._register(dom.addDisposableListener(mainWindow, dom.EventType.MOUSE_MOVE, mouseEvent => {
324
this._mouseTarget = mouseEvent.target;
325
this._onMouseMoveOrKeyDown(mouseEvent);
326
}));
327
this._register(dom.addDisposableListener(mainWindow, dom.EventType.KEY_DOWN, mouseEvent => {
328
this._onMouseMoveOrKeyDown(mouseEvent);
329
}));
330
this._register(dom.addDisposableListener(mainWindow, dom.EventType.KEY_UP, () => {
331
if (this._showEndForLine !== undefined) {
332
this._showEndForLine = undefined;
333
this._renderStickyScroll();
334
}
335
}));
336
337
this._register(gesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, _keyboardEvent]) => {
338
const mouseTarget = getMouseEventTarget(mouseEvent);
339
if (!mouseTarget || !mouseEvent.hasTriggerModifier || !this._editor.hasModel()) {
340
sessionStore.clear();
341
return;
342
}
343
const { range, textElement } = mouseTarget;
344
345
if (!range.equalsRange(this._stickyRangeProjectedOnEditor)) {
346
this._stickyRangeProjectedOnEditor = range;
347
sessionStore.clear();
348
} else if (textElement.style.textDecoration === 'underline') {
349
return;
350
}
351
352
const cancellationToken = new CancellationTokenSource();
353
sessionStore.add(toDisposable(() => cancellationToken.dispose(true)));
354
355
let currentHTMLChild: HTMLElement;
356
357
getDefinitionsAtPosition(this._languageFeaturesService.definitionProvider, this._editor.getModel(), new Position(range.startLineNumber, range.startColumn + 1), false, cancellationToken.token).then((candidateDefinitions => {
358
if (cancellationToken.token.isCancellationRequested) {
359
return;
360
}
361
if (candidateDefinitions.length !== 0) {
362
this._candidateDefinitionsLength = candidateDefinitions.length;
363
const childHTML: HTMLElement = textElement;
364
if (currentHTMLChild !== childHTML) {
365
sessionStore.clear();
366
currentHTMLChild = childHTML;
367
currentHTMLChild.style.textDecoration = 'underline';
368
sessionStore.add(toDisposable(() => {
369
currentHTMLChild.style.textDecoration = 'none';
370
}));
371
} else if (!currentHTMLChild) {
372
currentHTMLChild = childHTML;
373
currentHTMLChild.style.textDecoration = 'underline';
374
sessionStore.add(toDisposable(() => {
375
currentHTMLChild.style.textDecoration = 'none';
376
}));
377
}
378
} else {
379
sessionStore.clear();
380
}
381
}));
382
}));
383
this._register(gesture.onCancel(() => {
384
sessionStore.clear();
385
}));
386
this._register(gesture.onExecute(async e => {
387
if (e.target.type !== MouseTargetType.OVERLAY_WIDGET || e.target.detail !== this._stickyScrollWidget.getId()) {
388
// not hovering over our widget
389
return;
390
}
391
const position = this._stickyScrollWidget.getEditorPositionFromNode(e.target.element);
392
if (!position) {
393
// not hovering a sticky scroll line
394
return;
395
}
396
if (!this._editor.hasModel() || !this._stickyRangeProjectedOnEditor) {
397
return;
398
}
399
if (this._candidateDefinitionsLength > 1) {
400
if (this._focused) {
401
this._disposeFocusStickyScrollStore();
402
}
403
this._revealPosition({ lineNumber: position.lineNumber, column: 1 });
404
}
405
this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor, { uri: this._editor.getModel().uri, range: this._stickyRangeProjectedOnEditor });
406
}));
407
}
408
409
private _onContextMenu(targetWindow: Window, e: MouseEvent) {
410
const event = new StandardMouseEvent(targetWindow, e);
411
412
this._contextMenuService.showContextMenu({
413
menuId: MenuId.StickyScrollContext,
414
getAnchor: () => event,
415
menuActionOptions: { renderShortTitle: true },
416
});
417
}
418
419
private _onMouseMoveOrKeyDown(mouseEvent: KeyboardEvent | MouseEvent): void {
420
if (!mouseEvent.shiftKey) {
421
return;
422
}
423
if (!this._mouseTarget || !dom.isHTMLElement(this._mouseTarget)) {
424
return;
425
}
426
const currentEndForLineIndex = this._stickyScrollWidget.getLineIndexFromChildDomNode(this._mouseTarget);
427
if (currentEndForLineIndex === null || this._showEndForLine === currentEndForLineIndex) {
428
return;
429
}
430
this._showEndForLine = currentEndForLineIndex;
431
this._renderStickyScroll();
432
}
433
434
private _toggleFoldingRegionForLine(line: number | null) {
435
if (!this._foldingModel || line === null) {
436
return;
437
}
438
const stickyLine = this._stickyScrollWidget.getRenderedStickyLine(line);
439
const foldingIcon = stickyLine?.foldingIcon;
440
if (!foldingIcon) {
441
return;
442
}
443
toggleCollapseState(this._foldingModel, 1, [line]);
444
foldingIcon.isCollapsed = !foldingIcon.isCollapsed;
445
const scrollTop = (foldingIcon.isCollapsed ?
446
this._editor.getTopForLineNumber(foldingIcon.foldingEndLine)
447
: this._editor.getTopForLineNumber(foldingIcon.foldingStartLine))
448
- this._editor.getOption(EditorOption.lineHeight) * stickyLine.index + 1;
449
this._editor.setScrollTop(scrollTop);
450
this._renderStickyScroll(line);
451
}
452
453
private _readConfiguration() {
454
const options = this._editor.getOption(EditorOption.stickyScroll);
455
if (options.enabled === false) {
456
this._editor.removeOverlayWidget(this._stickyScrollWidget);
457
this._resetState();
458
this._sessionStore.clear();
459
this._enabled = false;
460
return;
461
} else if (options.enabled && !this._enabled) {
462
// When sticky scroll was just enabled, add the listeners on the sticky scroll
463
this._editor.addOverlayWidget(this._stickyScrollWidget);
464
this._sessionStore.add(this._editor.onDidScrollChange((e) => {
465
if (e.scrollTopChanged) {
466
this._showEndForLine = undefined;
467
this._renderStickyScroll();
468
}
469
}));
470
this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize()));
471
this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e)));
472
this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => {
473
this._showEndForLine = undefined;
474
this._renderStickyScroll();
475
}));
476
this._enabled = true;
477
}
478
479
const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers);
480
if (lineNumberOption.renderType === RenderLineNumbersType.Relative) {
481
if (!this._cursorPositionListener) {
482
this._cursorPositionListener = this._editor.onDidChangeCursorPosition((e) => {
483
if (this._positionLineNumber === e.position.lineNumber) {
484
return;
485
}
486
this._positionLineNumber = e.position.lineNumber;
487
this._showEndForLine = undefined;
488
this._renderStickyScroll(0);
489
});
490
this._sessionStore.add(this._cursorPositionListener);
491
}
492
} else if (this._cursorPositionListener) {
493
this._sessionStore.delete(this._cursorPositionListener);
494
this._cursorPositionListener.dispose();
495
this._cursorPositionListener = undefined;
496
}
497
}
498
499
private _readConfigurationChange(event: ConfigurationChangedEvent) {
500
if (
501
event.hasChanged(EditorOption.stickyScroll)
502
|| event.hasChanged(EditorOption.minimap)
503
|| event.hasChanged(EditorOption.lineHeight)
504
|| event.hasChanged(EditorOption.showFoldingControls)
505
|| event.hasChanged(EditorOption.lineNumbers)
506
) {
507
this._readConfiguration();
508
}
509
510
if (event.hasChanged(EditorOption.lineNumbers) || event.hasChanged(EditorOption.folding) || event.hasChanged(EditorOption.showFoldingControls)) {
511
this._renderStickyScroll(0);
512
}
513
}
514
515
private _needsUpdate(event: IModelTokensChangedEvent) {
516
const stickyLineNumbers = this._stickyScrollWidget.getCurrentLines();
517
for (const stickyLineNumber of stickyLineNumbers) {
518
for (const range of event.ranges) {
519
if (stickyLineNumber >= range.fromLineNumber && stickyLineNumber <= range.toLineNumber) {
520
return true;
521
}
522
}
523
}
524
return false;
525
}
526
527
private _onTokensChange(event: IModelTokensChangedEvent) {
528
if (this._needsUpdate(event)) {
529
// Rebuilding the whole widget from line 0
530
this._renderStickyScroll(0);
531
}
532
}
533
534
private _onDidResize() {
535
const layoutInfo = this._editor.getLayoutInfo();
536
// Make sure sticky scroll doesn't take up more than 25% of the editor
537
const theoreticalLines = layoutInfo.height / this._editor.getOption(EditorOption.lineHeight);
538
this._maxStickyLines = Math.round(theoreticalLines * .25);
539
this._renderStickyScroll(0);
540
}
541
542
private async _renderStickyScroll(rebuildFromLine?: number): Promise<void> {
543
const model = this._editor.getModel();
544
if (!model || model.isTooLargeForTokenization()) {
545
this._resetState();
546
return;
547
}
548
const nextRebuildFromLine = this._updateAndGetMinRebuildFromLine(rebuildFromLine);
549
const stickyWidgetVersion = this._stickyLineCandidateProvider.getVersionId();
550
const shouldUpdateState = stickyWidgetVersion === undefined || stickyWidgetVersion === model.getVersionId();
551
if (shouldUpdateState) {
552
if (!this._focused) {
553
await this._updateState(nextRebuildFromLine);
554
} else {
555
// Suppose that previously the sticky scroll widget had height 0, then if there are visible lines, set the last line as focused
556
if (this._focusedStickyElementIndex === -1) {
557
await this._updateState(nextRebuildFromLine);
558
this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumberCount - 1;
559
if (this._focusedStickyElementIndex !== -1) {
560
this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex);
561
}
562
} else {
563
const focusedStickyElementLineNumber = this._stickyScrollWidget.lineNumbers[this._focusedStickyElementIndex];
564
await this._updateState(nextRebuildFromLine);
565
// Suppose that after setting the state, there are no sticky lines, set the focused index to -1
566
if (this._stickyScrollWidget.lineNumberCount === 0) {
567
this._focusedStickyElementIndex = -1;
568
} else {
569
const previousFocusedLineNumberExists = this._stickyScrollWidget.lineNumbers.includes(focusedStickyElementLineNumber);
570
571
// If the line number is still there, do not change anything
572
// If the line number is not there, set the new focused line to be the last line
573
if (!previousFocusedLineNumberExists) {
574
this._focusedStickyElementIndex = this._stickyScrollWidget.lineNumberCount - 1;
575
}
576
this._stickyScrollWidget.focusLineWithIndex(this._focusedStickyElementIndex);
577
}
578
}
579
}
580
}
581
}
582
583
private _updateAndGetMinRebuildFromLine(rebuildFromLine: number | undefined): number | undefined {
584
if (rebuildFromLine !== undefined) {
585
const minRebuildFromLineOrInfinity = this._minRebuildFromLine !== undefined ? this._minRebuildFromLine : Infinity;
586
this._minRebuildFromLine = Math.min(rebuildFromLine, minRebuildFromLineOrInfinity);
587
}
588
return this._minRebuildFromLine;
589
}
590
591
private async _updateState(rebuildFromLine?: number): Promise<void> {
592
this._minRebuildFromLine = undefined;
593
this._foldingModel = await FoldingController.get(this._editor)?.getFoldingModel() ?? undefined;
594
this._widgetState = this.findScrollWidgetState();
595
const stickyWidgetHasLines = this._widgetState.startLineNumbers.length > 0;
596
this._stickyScrollVisibleContextKey.set(stickyWidgetHasLines);
597
this._stickyScrollWidget.setState(this._widgetState, this._foldingModel, rebuildFromLine);
598
}
599
600
private async _resetState(): Promise<void> {
601
this._minRebuildFromLine = undefined;
602
this._foldingModel = undefined;
603
this._widgetState = StickyScrollWidgetState.Empty;
604
this._stickyScrollVisibleContextKey.set(false);
605
this._stickyScrollWidget.setState(undefined, undefined);
606
}
607
608
findScrollWidgetState(): StickyScrollWidgetState {
609
const maxNumberStickyLines = Math.min(this._maxStickyLines, this._editor.getOption(EditorOption.stickyScroll).maxLineCount);
610
const scrollTop: number = this._editor.getScrollTop();
611
let lastLineRelativePosition: number = 0;
612
const startLineNumbers: number[] = [];
613
const endLineNumbers: number[] = [];
614
const arrayVisibleRanges = this._editor.getVisibleRanges();
615
if (arrayVisibleRanges.length !== 0) {
616
const fullVisibleRange = new StickyRange(arrayVisibleRanges[0].startLineNumber, arrayVisibleRanges[arrayVisibleRanges.length - 1].endLineNumber);
617
const candidateRanges = this._stickyLineCandidateProvider.getCandidateStickyLinesIntersecting(fullVisibleRange);
618
for (const range of candidateRanges) {
619
const start = range.startLineNumber;
620
const end = range.endLineNumber;
621
const topOfElement = range.top;
622
const bottomOfElement = topOfElement + range.height;
623
const topOfBeginningLine = this._editor.getTopForLineNumber(start) - scrollTop;
624
const bottomOfEndLine = this._editor.getBottomForLineNumber(end) - scrollTop;
625
if (topOfElement > topOfBeginningLine && topOfElement <= bottomOfEndLine) {
626
startLineNumbers.push(start);
627
endLineNumbers.push(end + 1);
628
if (bottomOfElement > bottomOfEndLine) {
629
lastLineRelativePosition = bottomOfEndLine - bottomOfElement;
630
}
631
}
632
if (startLineNumbers.length === maxNumberStickyLines) {
633
break;
634
}
635
}
636
}
637
this._endLineNumbers = endLineNumbers;
638
return new StickyScrollWidgetState(startLineNumbers, endLineNumbers, lastLineRelativePosition, this._showEndForLine);
639
}
640
641
override dispose(): void {
642
super.dispose();
643
this._sessionStore.dispose();
644
}
645
}
646
647