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