Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.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 { ICodeEditor } from '../../../browser/editorBrowser.js';
8
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
9
import { CancellationToken, CancellationTokenSource, } from '../../../../base/common/cancellation.js';
10
import { EditorOption } from '../../../common/config/editorOptions.js';
11
import { RunOnceScheduler } from '../../../../base/common/async.js';
12
import { binarySearch } from '../../../../base/common/arrays.js';
13
import { Event, Emitter } from '../../../../base/common/event.js';
14
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
15
import { StickyModelProvider, IStickyModelProvider } from './stickyScrollModelProvider.js';
16
import { StickyElement, StickyModel, StickyRange } from './stickyScrollElement.js';
17
import { Position } from '../../../common/core/position.js';
18
19
export class StickyLineCandidate {
20
constructor(
21
public readonly startLineNumber: number,
22
public readonly endLineNumber: number,
23
public readonly top: number,
24
public readonly height: number,
25
) { }
26
}
27
28
export interface IStickyLineCandidateProvider {
29
/**
30
* Dispose resources used by the provider.
31
*/
32
dispose(): void;
33
34
/**
35
* Get the version ID of the sticky model.
36
*/
37
getVersionId(): number | undefined;
38
39
/**
40
* Update the sticky line candidates.
41
*/
42
update(): Promise<void>;
43
44
/**
45
* Get sticky line candidates intersecting a given range.
46
*/
47
getCandidateStickyLinesIntersecting(range: StickyRange): StickyLineCandidate[];
48
49
/**
50
* Event triggered when sticky scroll changes.
51
*/
52
onDidChangeStickyScroll: Event<void>;
53
}
54
55
export class StickyLineCandidateProvider extends Disposable implements IStickyLineCandidateProvider {
56
static readonly ID = 'store.contrib.stickyScrollController';
57
58
private readonly _onDidChangeStickyScroll = this._register(new Emitter<void>());
59
public readonly onDidChangeStickyScroll = this._onDidChangeStickyScroll.event;
60
61
private readonly _editor: ICodeEditor;
62
private readonly _updateSoon: RunOnceScheduler;
63
private readonly _sessionStore: DisposableStore;
64
65
private _model: StickyModel | null = null;
66
private _cts: CancellationTokenSource | null = null;
67
private _stickyModelProvider: IStickyModelProvider | null = null;
68
69
constructor(
70
editor: ICodeEditor,
71
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
72
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,
73
) {
74
super();
75
this._editor = editor;
76
this._sessionStore = this._register(new DisposableStore());
77
this._updateSoon = this._register(new RunOnceScheduler(() => this.update(), 50));
78
79
this._register(this._editor.onDidChangeConfiguration(e => {
80
if (e.hasChanged(EditorOption.stickyScroll)) {
81
this.readConfiguration();
82
}
83
}));
84
this.readConfiguration();
85
}
86
87
/**
88
* Read and apply the sticky scroll configuration.
89
*/
90
private readConfiguration() {
91
this._sessionStore.clear();
92
const options = this._editor.getOption(EditorOption.stickyScroll);
93
if (!options.enabled) {
94
return;
95
}
96
this._sessionStore.add(this._editor.onDidChangeModel(() => {
97
this._model = null;
98
this.updateStickyModelProvider();
99
this._onDidChangeStickyScroll.fire();
100
this.update();
101
}));
102
this._sessionStore.add(this._editor.onDidChangeHiddenAreas(() => this.update()));
103
this._sessionStore.add(this._editor.onDidChangeModelContent(() => this._updateSoon.schedule()));
104
this._sessionStore.add(this._languageFeaturesService.documentSymbolProvider.onDidChange(() => this.update()));
105
this._sessionStore.add(toDisposable(() => {
106
this._stickyModelProvider?.dispose();
107
this._stickyModelProvider = null;
108
}));
109
this.updateStickyModelProvider();
110
this.update();
111
}
112
113
/**
114
* Get the version ID of the sticky model.
115
*/
116
public getVersionId(): number | undefined {
117
return this._model?.version;
118
}
119
120
/**
121
* Update the sticky model provider.
122
*/
123
private updateStickyModelProvider() {
124
this._stickyModelProvider?.dispose();
125
this._stickyModelProvider = null;
126
if (this._editor.hasModel()) {
127
this._stickyModelProvider = new StickyModelProvider(
128
this._editor,
129
() => this._updateSoon.schedule(),
130
this._languageConfigurationService,
131
this._languageFeaturesService
132
);
133
}
134
}
135
136
/**
137
* Update the sticky line candidates.
138
*/
139
public async update(): Promise<void> {
140
this._cts?.dispose(true);
141
this._cts = new CancellationTokenSource();
142
await this.updateStickyModel(this._cts.token);
143
this._onDidChangeStickyScroll.fire();
144
}
145
146
/**
147
* Update the sticky model based on the current editor state.
148
*/
149
private async updateStickyModel(token: CancellationToken): Promise<void> {
150
if (!this._editor.hasModel() || !this._stickyModelProvider || this._editor.getModel().isTooLargeForTokenization()) {
151
this._model = null;
152
return;
153
}
154
const model = await this._stickyModelProvider.update(token);
155
if (!token.isCancellationRequested) {
156
this._model = model;
157
}
158
}
159
160
/**
161
* Get sticky line candidates intersecting a given range.
162
*/
163
public getCandidateStickyLinesIntersecting(range: StickyRange): StickyLineCandidate[] {
164
if (!this._model?.element) {
165
return [];
166
}
167
const stickyLineCandidates: StickyLineCandidate[] = [];
168
this.getCandidateStickyLinesIntersectingFromStickyModel(range, this._model.element, stickyLineCandidates, 0, 0, -1);
169
return this.filterHiddenRanges(stickyLineCandidates);
170
}
171
172
/**
173
* Get sticky line candidates intersecting a given range from the sticky model.
174
*/
175
private getCandidateStickyLinesIntersectingFromStickyModel(
176
range: StickyRange,
177
outlineModel: StickyElement,
178
result: StickyLineCandidate[],
179
depth: number,
180
top: number,
181
lastStartLineNumber: number
182
): void {
183
if (outlineModel.children.length === 0) {
184
return;
185
}
186
let lastLine = lastStartLineNumber;
187
const childrenStartLines: number[] = [];
188
189
for (let i = 0; i < outlineModel.children.length; i++) {
190
const child = outlineModel.children[i];
191
if (child.range) {
192
childrenStartLines.push(child.range.startLineNumber);
193
}
194
}
195
const lowerBound = this.updateIndex(binarySearch(childrenStartLines, range.startLineNumber, (a: number, b: number) => { return a - b; }));
196
const upperBound = this.updateIndex(binarySearch(childrenStartLines, range.endLineNumber, (a: number, b: number) => { return a - b; }));
197
198
for (let i = lowerBound; i <= upperBound; i++) {
199
const child = outlineModel.children[i];
200
if (!child || !child.range) {
201
continue;
202
}
203
const { startLineNumber, endLineNumber } = child.range;
204
if (range.startLineNumber <= endLineNumber + 1 && startLineNumber - 1 <= range.endLineNumber && startLineNumber !== lastLine) {
205
lastLine = startLineNumber;
206
const lineHeight = this._editor.getLineHeightForPosition(new Position(startLineNumber, 1));
207
result.push(new StickyLineCandidate(startLineNumber, endLineNumber - 1, top, lineHeight));
208
this.getCandidateStickyLinesIntersectingFromStickyModel(range, child, result, depth + 1, top + lineHeight, startLineNumber);
209
}
210
}
211
}
212
213
/**
214
* Filter out sticky line candidates that are within hidden ranges.
215
*/
216
private filterHiddenRanges(stickyLineCandidates: StickyLineCandidate[]): StickyLineCandidate[] {
217
const hiddenRanges = this._editor._getViewModel()?.getHiddenAreas();
218
if (!hiddenRanges) {
219
return stickyLineCandidates;
220
}
221
return stickyLineCandidates.filter(candidate => {
222
return !hiddenRanges.some(hiddenRange =>
223
candidate.startLineNumber >= hiddenRange.startLineNumber &&
224
candidate.endLineNumber <= hiddenRange.endLineNumber + 1
225
);
226
});
227
}
228
229
/**
230
* Update the binary search index.
231
*/
232
private updateIndex(index: number): number {
233
if (index === -1) {
234
return 0;
235
} else if (index < 0) {
236
return -index - 2;
237
}
238
return index;
239
}
240
}
241
242