Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.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, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
7
import { IActiveCodeEditor } from '../../../browser/editorBrowser.js';
8
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
9
import { OutlineElement, OutlineGroup, OutlineModel } from '../../documentSymbols/browser/outlineModel.js';
10
import { CancellationToken } from '../../../../base/common/cancellation.js';
11
import { CancelablePromise, createCancelablePromise, Delayer } from '../../../../base/common/async.js';
12
import { FoldingController, RangesLimitReporter } from '../../folding/browser/folding.js';
13
import { SyntaxRangeProvider } from '../../folding/browser/syntaxRangeProvider.js';
14
import { IndentRangeProvider } from '../../folding/browser/indentRangeProvider.js';
15
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
16
import { FoldingRegions } from '../../folding/browser/foldingRanges.js';
17
import { onUnexpectedError } from '../../../../base/common/errors.js';
18
import { StickyElement, StickyModel, StickyRange } from './stickyScrollElement.js';
19
import { Iterable } from '../../../../base/common/iterator.js';
20
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
21
import { EditorOption } from '../../../common/config/editorOptions.js';
22
23
enum ModelProvider {
24
OUTLINE_MODEL = 'outlineModel',
25
FOLDING_PROVIDER_MODEL = 'foldingProviderModel',
26
INDENTATION_MODEL = 'indentationModel'
27
}
28
29
enum Status {
30
VALID,
31
INVALID,
32
CANCELED
33
}
34
35
export interface IStickyModelProvider extends IDisposable {
36
37
/**
38
* Method which updates the sticky model
39
* @param token cancellation token
40
* @returns the sticky model
41
*/
42
update(token: CancellationToken): Promise<StickyModel | null>;
43
}
44
45
export class StickyModelProvider extends Disposable implements IStickyModelProvider {
46
47
private _modelProviders: IStickyModelCandidateProvider<any>[] = [];
48
private _modelPromise: CancelablePromise<any | null> | null = null;
49
private _updateScheduler: Delayer<StickyModel | null> = this._register(new Delayer<StickyModel | null>(300));
50
private readonly _updateOperation: DisposableStore = this._register(new DisposableStore());
51
52
constructor(
53
private readonly _editor: IActiveCodeEditor,
54
onProviderUpdate: () => void,
55
@IInstantiationService _languageConfigurationService: ILanguageConfigurationService,
56
@ILanguageFeaturesService _languageFeaturesService: ILanguageFeaturesService,
57
) {
58
super();
59
60
switch (this._editor.getOption(EditorOption.stickyScroll).defaultModel) {
61
case ModelProvider.OUTLINE_MODEL:
62
this._modelProviders.push(new StickyModelFromCandidateOutlineProvider(this._editor, _languageFeaturesService));
63
// fall through
64
case ModelProvider.FOLDING_PROVIDER_MODEL:
65
this._modelProviders.push(new StickyModelFromCandidateSyntaxFoldingProvider(this._editor, onProviderUpdate, _languageFeaturesService));
66
// fall through
67
case ModelProvider.INDENTATION_MODEL:
68
this._modelProviders.push(new StickyModelFromCandidateIndentationFoldingProvider(this._editor, _languageConfigurationService));
69
break;
70
}
71
}
72
73
public override dispose(): void {
74
this._modelProviders.forEach(provider => provider.dispose());
75
this._updateOperation.clear();
76
this._cancelModelPromise();
77
super.dispose();
78
}
79
80
private _cancelModelPromise(): void {
81
if (this._modelPromise) {
82
this._modelPromise.cancel();
83
this._modelPromise = null;
84
}
85
}
86
87
public async update(token: CancellationToken): Promise<StickyModel | null> {
88
89
this._updateOperation.clear();
90
this._updateOperation.add({
91
dispose: () => {
92
this._cancelModelPromise();
93
this._updateScheduler.cancel();
94
}
95
});
96
this._cancelModelPromise();
97
98
return await this._updateScheduler.trigger(async () => {
99
100
for (const modelProvider of this._modelProviders) {
101
const { statusPromise, modelPromise } = modelProvider.computeStickyModel(token);
102
this._modelPromise = modelPromise;
103
const status = await statusPromise;
104
if (this._modelPromise !== modelPromise) {
105
return null;
106
}
107
switch (status) {
108
case Status.CANCELED:
109
this._updateOperation.clear();
110
return null;
111
case Status.VALID:
112
return modelProvider.stickyModel;
113
}
114
}
115
return null;
116
}).catch((error) => {
117
onUnexpectedError(error);
118
return null;
119
});
120
}
121
}
122
123
interface IStickyModelCandidateProvider<T> extends IDisposable {
124
get stickyModel(): StickyModel | null;
125
126
/**
127
* Method which computes the sticky model and returns a status to signal whether the sticky model has been successfully found
128
* @param token cancellation token
129
* @returns a promise of a status indicating whether the sticky model has been successfully found as well as the model promise
130
*/
131
computeStickyModel(token: CancellationToken): { statusPromise: Promise<Status> | Status; modelPromise: CancelablePromise<T | null> | null };
132
}
133
134
abstract class StickyModelCandidateProvider<T> extends Disposable implements IStickyModelCandidateProvider<T> {
135
136
protected _stickyModel: StickyModel | null = null;
137
138
constructor(protected readonly _editor: IActiveCodeEditor) {
139
super();
140
}
141
142
get stickyModel(): StickyModel | null {
143
return this._stickyModel;
144
}
145
146
private _invalid(): Status {
147
this._stickyModel = null;
148
return Status.INVALID;
149
}
150
151
public computeStickyModel(token: CancellationToken): { statusPromise: Promise<Status> | Status; modelPromise: CancelablePromise<T | null> | null } {
152
if (token.isCancellationRequested || !this.isProviderValid()) {
153
return { statusPromise: this._invalid(), modelPromise: null };
154
}
155
const providerModelPromise = createCancelablePromise(token => this.createModelFromProvider(token));
156
157
return {
158
statusPromise: providerModelPromise.then(providerModel => {
159
if (!this.isModelValid(providerModel)) {
160
return this._invalid();
161
162
}
163
if (token.isCancellationRequested) {
164
return Status.CANCELED;
165
}
166
this._stickyModel = this.createStickyModel(token, providerModel);
167
return Status.VALID;
168
}).then(undefined, (err) => {
169
onUnexpectedError(err);
170
return Status.CANCELED;
171
}),
172
modelPromise: providerModelPromise
173
};
174
}
175
176
/**
177
* Method which checks whether the model returned by the provider is valid and can be used to compute a sticky model.
178
* This method by default returns true.
179
* @param model model returned by the provider
180
* @returns boolean indicating whether the model is valid
181
*/
182
protected isModelValid(model: T): boolean {
183
return true;
184
}
185
186
/**
187
* Method which checks whether the provider is valid before applying it to find the provider model.
188
* This method by default returns true.
189
* @returns boolean indicating whether the provider is valid
190
*/
191
protected isProviderValid(): boolean {
192
return true;
193
}
194
195
/**
196
* Abstract method which creates the model from the provider and returns the provider model
197
* @param token cancellation token
198
* @returns the model returned by the provider
199
*/
200
protected abstract createModelFromProvider(token: CancellationToken): Promise<T>;
201
202
/**
203
* Abstract method which computes the sticky model from the model returned by the provider and returns the sticky model
204
* @param token cancellation token
205
* @param model model returned by the provider
206
* @returns the sticky model
207
*/
208
protected abstract createStickyModel(token: CancellationToken, model: T): StickyModel;
209
}
210
211
class StickyModelFromCandidateOutlineProvider extends StickyModelCandidateProvider<OutlineModel> {
212
213
constructor(_editor: IActiveCodeEditor, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService) {
214
super(_editor);
215
}
216
217
protected createModelFromProvider(token: CancellationToken): Promise<OutlineModel> {
218
return OutlineModel.create(this._languageFeaturesService.documentSymbolProvider, this._editor.getModel(), token);
219
}
220
221
protected createStickyModel(token: CancellationToken, model: OutlineModel): StickyModel {
222
const { stickyOutlineElement, providerID } = this._stickyModelFromOutlineModel(model, this._stickyModel?.outlineProviderId);
223
const textModel = this._editor.getModel();
224
return new StickyModel(textModel.uri, textModel.getVersionId(), stickyOutlineElement, providerID);
225
}
226
227
protected override isModelValid(model: OutlineModel): boolean {
228
return model && model.children.size > 0;
229
}
230
231
private _stickyModelFromOutlineModel(outlineModel: OutlineModel, preferredProvider: string | undefined): { stickyOutlineElement: StickyElement; providerID: string | undefined } {
232
233
let outlineElements: Map<string, OutlineElement>;
234
// When several possible outline providers
235
if (Iterable.first(outlineModel.children.values()) instanceof OutlineGroup) {
236
const provider = Iterable.find(outlineModel.children.values(), outlineGroupOfModel => outlineGroupOfModel.id === preferredProvider);
237
if (provider) {
238
outlineElements = provider.children;
239
} else {
240
let tempID = '';
241
let maxTotalSumOfRanges = -1;
242
let optimalOutlineGroup = undefined;
243
for (const [_key, outlineGroup] of outlineModel.children.entries()) {
244
const totalSumRanges = this._findSumOfRangesOfGroup(outlineGroup);
245
if (totalSumRanges > maxTotalSumOfRanges) {
246
optimalOutlineGroup = outlineGroup;
247
maxTotalSumOfRanges = totalSumRanges;
248
tempID = outlineGroup.id;
249
}
250
}
251
preferredProvider = tempID;
252
outlineElements = optimalOutlineGroup!.children;
253
}
254
} else {
255
outlineElements = outlineModel.children as Map<string, OutlineElement>;
256
}
257
const stickyChildren: StickyElement[] = [];
258
const outlineElementsArray = Array.from(outlineElements.values()).sort((element1, element2) => {
259
const range1: StickyRange = new StickyRange(element1.symbol.range.startLineNumber, element1.symbol.range.endLineNumber);
260
const range2: StickyRange = new StickyRange(element2.symbol.range.startLineNumber, element2.symbol.range.endLineNumber);
261
return this._comparator(range1, range2);
262
});
263
for (const outlineElement of outlineElementsArray) {
264
stickyChildren.push(this._stickyModelFromOutlineElement(outlineElement, outlineElement.symbol.selectionRange.startLineNumber));
265
}
266
const stickyOutlineElement = new StickyElement(undefined, stickyChildren, undefined);
267
268
return {
269
stickyOutlineElement: stickyOutlineElement,
270
providerID: preferredProvider
271
};
272
}
273
274
private _stickyModelFromOutlineElement(outlineElement: OutlineElement, previousStartLine: number): StickyElement {
275
const children: StickyElement[] = [];
276
for (const child of outlineElement.children.values()) {
277
if (child.symbol.selectionRange.startLineNumber !== child.symbol.range.endLineNumber) {
278
if (child.symbol.selectionRange.startLineNumber !== previousStartLine) {
279
children.push(this._stickyModelFromOutlineElement(child, child.symbol.selectionRange.startLineNumber));
280
} else {
281
for (const subchild of child.children.values()) {
282
children.push(this._stickyModelFromOutlineElement(subchild, child.symbol.selectionRange.startLineNumber));
283
}
284
}
285
}
286
}
287
children.sort((child1, child2) => this._comparator(child1.range!, child2.range!));
288
const range = new StickyRange(outlineElement.symbol.selectionRange.startLineNumber, outlineElement.symbol.range.endLineNumber);
289
return new StickyElement(range, children, undefined);
290
}
291
292
private _comparator(range1: StickyRange, range2: StickyRange): number {
293
if (range1.startLineNumber !== range2.startLineNumber) {
294
return range1.startLineNumber - range2.startLineNumber;
295
} else {
296
return range2.endLineNumber - range1.endLineNumber;
297
}
298
}
299
300
private _findSumOfRangesOfGroup(outline: OutlineGroup | OutlineElement): number {
301
let res = 0;
302
for (const child of outline.children.values()) {
303
res += this._findSumOfRangesOfGroup(child);
304
}
305
if (outline instanceof OutlineElement) {
306
return res + outline.symbol.range.endLineNumber - outline.symbol.selectionRange.startLineNumber;
307
} else {
308
return res;
309
}
310
}
311
}
312
313
abstract class StickyModelFromCandidateFoldingProvider extends StickyModelCandidateProvider<FoldingRegions | null> {
314
315
protected _foldingLimitReporter: RangesLimitReporter;
316
317
constructor(editor: IActiveCodeEditor) {
318
super(editor);
319
this._foldingLimitReporter = this._register(new RangesLimitReporter(editor));
320
}
321
322
protected createStickyModel(token: CancellationToken, model: FoldingRegions): StickyModel {
323
const foldingElement = this._fromFoldingRegions(model);
324
const textModel = this._editor.getModel();
325
return new StickyModel(textModel.uri, textModel.getVersionId(), foldingElement, undefined);
326
}
327
328
protected override isModelValid(model: FoldingRegions): boolean {
329
return model !== null;
330
}
331
332
333
private _fromFoldingRegions(foldingRegions: FoldingRegions): StickyElement {
334
const length = foldingRegions.length;
335
const orderedStickyElements: StickyElement[] = [];
336
337
// The root sticky outline element
338
const stickyOutlineElement = new StickyElement(
339
undefined,
340
[],
341
undefined
342
);
343
344
for (let i = 0; i < length; i++) {
345
// Finding the parent index of the current range
346
const parentIndex = foldingRegions.getParentIndex(i);
347
348
let parentNode;
349
if (parentIndex !== -1) {
350
// Access the reference of the parent node
351
parentNode = orderedStickyElements[parentIndex];
352
} else {
353
// In that case the parent node is the root node
354
parentNode = stickyOutlineElement;
355
}
356
357
const child = new StickyElement(
358
new StickyRange(foldingRegions.getStartLineNumber(i), foldingRegions.getEndLineNumber(i) + 1),
359
[],
360
parentNode
361
);
362
parentNode.children.push(child);
363
orderedStickyElements.push(child);
364
}
365
return stickyOutlineElement;
366
}
367
}
368
369
class StickyModelFromCandidateIndentationFoldingProvider extends StickyModelFromCandidateFoldingProvider {
370
371
private readonly provider: IndentRangeProvider;
372
373
constructor(
374
editor: IActiveCodeEditor,
375
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService) {
376
super(editor);
377
378
this.provider = this._register(new IndentRangeProvider(editor.getModel(), this._languageConfigurationService, this._foldingLimitReporter));
379
}
380
381
protected override async createModelFromProvider(token: CancellationToken): Promise<FoldingRegions> {
382
return this.provider.compute(token);
383
}
384
}
385
386
class StickyModelFromCandidateSyntaxFoldingProvider extends StickyModelFromCandidateFoldingProvider {
387
388
private readonly provider: MutableDisposable<SyntaxRangeProvider> = this._register(new MutableDisposable<SyntaxRangeProvider>());
389
390
constructor(
391
editor: IActiveCodeEditor,
392
onProviderUpdate: () => void,
393
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService
394
) {
395
super(editor);
396
this._register(this._languageFeaturesService.foldingRangeProvider.onDidChange(() => {
397
this._updateProvider(editor, onProviderUpdate);
398
}));
399
this._updateProvider(editor, onProviderUpdate);
400
}
401
402
private _updateProvider(editor: IActiveCodeEditor, onProviderUpdate: () => void): void {
403
const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, editor.getModel());
404
if (selectedProviders.length === 0) {
405
return;
406
}
407
this.provider.value = new SyntaxRangeProvider(editor.getModel(), selectedProviders, onProviderUpdate, this._foldingLimitReporter, undefined);
408
}
409
410
protected override isProviderValid(): boolean {
411
return this.provider !== undefined;
412
}
413
414
protected override async createModelFromProvider(token: CancellationToken): Promise<FoldingRegions | null> {
415
return this.provider.value?.compute(token) ?? null;
416
}
417
}
418
419