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