Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/documentSymbols/browser/outlineModel.ts
5263 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 { binarySearch, coalesceInPlace, equals } from '../../../../base/common/arrays.js';
7
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
8
import { onUnexpectedExternalError } from '../../../../base/common/errors.js';
9
import { Iterable } from '../../../../base/common/iterator.js';
10
import { LRUCache } from '../../../../base/common/map.js';
11
import { commonPrefixLength } from '../../../../base/common/strings.js';
12
import { URI } from '../../../../base/common/uri.js';
13
import { IPosition, Position } from '../../../common/core/position.js';
14
import { IRange, Range } from '../../../common/core/range.js';
15
import { ITextModel } from '../../../common/model.js';
16
import { DocumentSymbol, DocumentSymbolProvider } from '../../../common/languages.js';
17
import { MarkerSeverity } from '../../../../platform/markers/common/markers.js';
18
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';
19
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
20
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
21
import { IModelService } from '../../../common/services/model.js';
22
import { DisposableStore } from '../../../../base/common/lifecycle.js';
23
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
24
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
25
26
export abstract class TreeElement {
27
28
abstract id: string;
29
abstract children: Map<string, TreeElement>;
30
abstract parent: TreeElement | undefined;
31
32
remove(): void {
33
this.parent?.children.delete(this.id);
34
}
35
36
static findId(candidate: DocumentSymbol | string, container: TreeElement): string {
37
// complex id-computation which contains the origin/extension,
38
// the parent path, and some dedupe logic when names collide
39
let candidateId: string;
40
if (typeof candidate === 'string') {
41
candidateId = `${container.id}/${candidate}`;
42
} else {
43
candidateId = `${container.id}/${candidate.name}`;
44
if (container.children.get(candidateId) !== undefined) {
45
candidateId = `${container.id}/${candidate.name}_${candidate.range.startLineNumber}_${candidate.range.startColumn}`;
46
}
47
}
48
49
let id = candidateId;
50
for (let i = 0; container.children.get(id) !== undefined; i++) {
51
id = `${candidateId}_${i}`;
52
}
53
54
return id;
55
}
56
57
static getElementById(id: string, element: TreeElement): TreeElement | undefined {
58
if (!id) {
59
return undefined;
60
}
61
const len = commonPrefixLength(id, element.id);
62
if (len === id.length) {
63
return element;
64
}
65
if (len < element.id.length) {
66
return undefined;
67
}
68
for (const [, child] of element.children) {
69
// eslint-disable-next-line no-restricted-syntax
70
const candidate = TreeElement.getElementById(id, child);
71
if (candidate) {
72
return candidate;
73
}
74
}
75
return undefined;
76
}
77
78
static size(element: TreeElement): number {
79
let res = 1;
80
for (const [, child] of element.children) {
81
res += TreeElement.size(child);
82
}
83
return res;
84
}
85
86
static empty(element: TreeElement): boolean {
87
return element.children.size === 0;
88
}
89
}
90
91
export interface IOutlineMarker {
92
startLineNumber: number;
93
startColumn: number;
94
endLineNumber: number;
95
endColumn: number;
96
severity: MarkerSeverity;
97
}
98
99
export class OutlineElement extends TreeElement {
100
101
children = new Map<string, OutlineElement>();
102
marker: { count: number; topSev: MarkerSeverity } | undefined;
103
104
constructor(
105
readonly id: string,
106
public parent: TreeElement | undefined,
107
readonly symbol: DocumentSymbol
108
) {
109
super();
110
}
111
}
112
113
export class OutlineGroup extends TreeElement {
114
115
children = new Map<string, OutlineElement>();
116
117
constructor(
118
readonly id: string,
119
public parent: TreeElement | undefined,
120
readonly label: string,
121
readonly order: number,
122
) {
123
super();
124
}
125
126
getItemEnclosingPosition(position: IPosition): OutlineElement | undefined {
127
return position ? this._getItemEnclosingPosition(position, this.children) : undefined;
128
}
129
130
private _getItemEnclosingPosition(position: IPosition, children: Map<string, OutlineElement>): OutlineElement | undefined {
131
for (const [, item] of children) {
132
if (!item.symbol.range || !Range.containsPosition(item.symbol.range, position)) {
133
continue;
134
}
135
return this._getItemEnclosingPosition(position, item.children) || item;
136
}
137
return undefined;
138
}
139
140
updateMarker(marker: IOutlineMarker[]): void {
141
for (const [, child] of this.children) {
142
this._updateMarker(marker, child);
143
}
144
}
145
146
private _updateMarker(markers: IOutlineMarker[], item: OutlineElement): void {
147
item.marker = undefined;
148
149
// find the proper start index to check for item/marker overlap.
150
const idx = binarySearch<IRange>(markers, item.symbol.range, Range.compareRangesUsingStarts);
151
let start: number;
152
if (idx < 0) {
153
start = ~idx;
154
if (start > 0 && Range.areIntersecting(markers[start - 1], item.symbol.range)) {
155
start -= 1;
156
}
157
} else {
158
start = idx;
159
}
160
161
const myMarkers: IOutlineMarker[] = [];
162
let myTopSev: MarkerSeverity | undefined;
163
164
for (; start < markers.length && Range.areIntersecting(item.symbol.range, markers[start]); start++) {
165
// remove markers intersecting with this outline element
166
// and store them in a 'private' array.
167
const marker = markers[start];
168
myMarkers.push(marker);
169
(markers as Array<IOutlineMarker | undefined>)[start] = undefined;
170
if (!myTopSev || marker.severity > myTopSev) {
171
myTopSev = marker.severity;
172
}
173
}
174
175
// Recurse into children and let them match markers that have matched
176
// this outline element. This might remove markers from this element and
177
// therefore we remember that we have had markers. That allows us to render
178
// the dot, saying 'this element has children with markers'
179
for (const [, child] of item.children) {
180
this._updateMarker(myMarkers, child);
181
}
182
183
if (myTopSev) {
184
item.marker = {
185
count: myMarkers.length,
186
topSev: myTopSev
187
};
188
}
189
190
coalesceInPlace(markers);
191
}
192
}
193
194
export class OutlineModel extends TreeElement {
195
196
static create(registry: LanguageFeatureRegistry<DocumentSymbolProvider>, textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
197
198
const cts = new CancellationTokenSource(token);
199
const result = new OutlineModel(textModel.uri);
200
const provider = registry.ordered(textModel);
201
const promises = provider.map((provider, index) => {
202
203
const id = TreeElement.findId(`provider_${index}`, result);
204
const group = new OutlineGroup(id, result, provider.displayName ?? 'Unknown Outline Provider', index);
205
206
207
return Promise.resolve(provider.provideDocumentSymbols(textModel, cts.token)).then(result => {
208
for (const info of result || []) {
209
OutlineModel._makeOutlineElement(info, group);
210
}
211
return group;
212
}, err => {
213
onUnexpectedExternalError(err);
214
return group;
215
}).then(group => {
216
if (!TreeElement.empty(group)) {
217
result._groups.set(id, group);
218
} else {
219
group.remove();
220
}
221
});
222
});
223
224
const listener = registry.onDidChange(() => {
225
const newProvider = registry.ordered(textModel);
226
if (!equals(newProvider, provider)) {
227
cts.cancel();
228
}
229
});
230
231
return Promise.all(promises).then(() => {
232
if (cts.token.isCancellationRequested && !token.isCancellationRequested) {
233
return OutlineModel.create(registry, textModel, token);
234
} else {
235
return result._compact();
236
}
237
}).finally(() => {
238
cts.dispose();
239
listener.dispose();
240
cts.dispose();
241
});
242
}
243
244
private static _makeOutlineElement(info: DocumentSymbol, container: OutlineGroup | OutlineElement): void {
245
const id = TreeElement.findId(info, container);
246
const res = new OutlineElement(id, container, info);
247
if (info.children) {
248
for (const childInfo of info.children) {
249
OutlineModel._makeOutlineElement(childInfo, res);
250
}
251
}
252
container.children.set(res.id, res);
253
}
254
255
static get(element: TreeElement | undefined): OutlineModel | undefined {
256
while (element) {
257
if (element instanceof OutlineModel) {
258
return element;
259
}
260
element = element.parent;
261
}
262
return undefined;
263
}
264
265
readonly id = 'root';
266
readonly parent = undefined;
267
268
protected _groups = new Map<string, OutlineGroup>();
269
children = new Map<string, OutlineGroup | OutlineElement>();
270
271
protected constructor(readonly uri: URI) {
272
super();
273
274
this.id = 'root';
275
this.parent = undefined;
276
}
277
278
private _compact(): this {
279
let count = 0;
280
for (const [key, group] of this._groups) {
281
if (group.children.size === 0) { // empty
282
this._groups.delete(key);
283
} else {
284
count += 1;
285
}
286
}
287
if (count !== 1) {
288
//
289
this.children = this._groups;
290
} else {
291
// adopt all elements of the first group
292
const group = Iterable.first(this._groups.values())!;
293
for (const [, child] of group.children) {
294
child.parent = this;
295
this.children.set(child.id, child);
296
}
297
}
298
return this;
299
}
300
301
merge(other: OutlineModel): boolean {
302
if (this.uri.toString() !== other.uri.toString()) {
303
return false;
304
}
305
if (this._groups.size !== other._groups.size) {
306
return false;
307
}
308
this._groups = other._groups;
309
this.children = other.children;
310
return true;
311
}
312
313
getItemEnclosingPosition(position: IPosition, context?: OutlineElement): OutlineElement | undefined {
314
315
let preferredGroup: OutlineGroup | undefined;
316
if (context) {
317
let candidate = context.parent;
318
while (candidate && !preferredGroup) {
319
if (candidate instanceof OutlineGroup) {
320
preferredGroup = candidate;
321
}
322
candidate = candidate.parent;
323
}
324
}
325
326
let result: OutlineElement | undefined = undefined;
327
for (const [, group] of this._groups) {
328
result = group.getItemEnclosingPosition(position);
329
if (result && (!preferredGroup || preferredGroup === group)) {
330
break;
331
}
332
}
333
return result;
334
}
335
336
getItemById(id: string): TreeElement | undefined {
337
// eslint-disable-next-line no-restricted-syntax
338
return TreeElement.getElementById(id, this);
339
}
340
341
updateMarker(marker: IOutlineMarker[]): void {
342
// sort markers by start range so that we can use
343
// outline element starts for quicker look up
344
marker.sort(Range.compareRangesUsingStarts);
345
346
for (const [, group] of this._groups) {
347
group.updateMarker(marker.slice(0));
348
}
349
}
350
351
getTopLevelSymbols(): DocumentSymbol[] {
352
const roots: DocumentSymbol[] = [];
353
for (const child of this.children.values()) {
354
if (child instanceof OutlineElement) {
355
roots.push(child.symbol);
356
} else {
357
roots.push(...Iterable.map(child.children.values(), child => child.symbol));
358
}
359
}
360
return roots.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
361
}
362
363
asListOfDocumentSymbols(): DocumentSymbol[] {
364
const roots = this.getTopLevelSymbols();
365
const bucket: DocumentSymbol[] = [];
366
OutlineModel._flattenDocumentSymbols(bucket, roots, '');
367
return bucket.sort((a, b) =>
368
Position.compare(Range.getStartPosition(a.range), Range.getStartPosition(b.range)) || Position.compare(Range.getEndPosition(b.range), Range.getEndPosition(a.range))
369
);
370
}
371
372
private static _flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void {
373
for (const entry of entries) {
374
bucket.push({
375
kind: entry.kind,
376
tags: entry.tags,
377
name: entry.name,
378
detail: entry.detail,
379
containerName: entry.containerName || overrideContainerLabel,
380
range: entry.range,
381
selectionRange: entry.selectionRange,
382
children: undefined, // we flatten it...
383
});
384
385
// Recurse over children
386
if (entry.children) {
387
OutlineModel._flattenDocumentSymbols(bucket, entry.children, entry.name);
388
}
389
}
390
}
391
}
392
393
394
export const IOutlineModelService = createDecorator<IOutlineModelService>('IOutlineModelService');
395
396
export interface IOutlineModelService {
397
_serviceBrand: undefined;
398
getOrCreate(model: ITextModel, token: CancellationToken): Promise<OutlineModel>;
399
getDebounceValue(textModel: ITextModel): number;
400
getCachedModels(): Iterable<OutlineModel>;
401
}
402
403
interface CacheEntry {
404
versionId: number;
405
provider: DocumentSymbolProvider[];
406
407
promiseCnt: number;
408
source: CancellationTokenSource;
409
promise: Promise<OutlineModel>;
410
model: OutlineModel | undefined;
411
}
412
413
export class OutlineModelService implements IOutlineModelService {
414
415
declare _serviceBrand: undefined;
416
417
private readonly _disposables = new DisposableStore();
418
private readonly _debounceInformation: IFeatureDebounceInformation;
419
private readonly _cache = new LRUCache<string, CacheEntry>(15, 0.7);
420
421
constructor(
422
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
423
@ILanguageFeatureDebounceService debounces: ILanguageFeatureDebounceService,
424
@IModelService modelService: IModelService
425
) {
426
this._debounceInformation = debounces.for(_languageFeaturesService.documentSymbolProvider, 'DocumentSymbols', { min: 350 });
427
428
// don't cache outline models longer than their text model
429
this._disposables.add(modelService.onModelRemoved(textModel => {
430
this._cache.delete(textModel.id);
431
}));
432
}
433
434
dispose(): void {
435
this._disposables.dispose();
436
}
437
438
async getOrCreate(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
439
440
const registry = this._languageFeaturesService.documentSymbolProvider;
441
const provider = registry.ordered(textModel);
442
443
let data = this._cache.get(textModel.id);
444
if (!data || data.versionId !== textModel.getVersionId() || !equals(data.provider, provider)) {
445
const source = new CancellationTokenSource();
446
data = {
447
versionId: textModel.getVersionId(),
448
provider,
449
promiseCnt: 0,
450
source,
451
promise: OutlineModel.create(registry, textModel, source.token),
452
model: undefined,
453
};
454
this._cache.set(textModel.id, data);
455
456
const now = Date.now();
457
data.promise.then(outlineModel => {
458
data!.model = outlineModel;
459
this._debounceInformation.update(textModel, Date.now() - now);
460
}).catch(_err => {
461
this._cache.delete(textModel.id);
462
});
463
}
464
465
if (data.model) {
466
// resolved -> return data
467
return data.model;
468
}
469
470
// increase usage counter
471
data.promiseCnt += 1;
472
473
const listener = token.onCancellationRequested(() => {
474
// last -> cancel provider request, remove cached promise
475
if (--data.promiseCnt === 0) {
476
data.source.cancel();
477
this._cache.delete(textModel.id);
478
}
479
});
480
481
try {
482
return await data.promise;
483
} finally {
484
listener.dispose();
485
}
486
}
487
488
getDebounceValue(textModel: ITextModel): number {
489
return this._debounceInformation.get(textModel);
490
}
491
492
getCachedModels(): Iterable<OutlineModel> {
493
return Iterable.filter<OutlineModel | undefined, OutlineModel>(Iterable.map(this._cache.values(), entry => entry.model), model => model !== undefined);
494
}
495
}
496
497
registerSingleton(IOutlineModelService, OutlineModelService, InstantiationType.Delayed);
498
499