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