outlineModel.ts 12.9 KB
Newer Older
J
Johannes Rieken 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { binarySearch, coalesceInPlace, equals } from 'vs/base/common/arrays';
7 8 9
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { onUnexpectedExternalError } from 'vs/base/common/errors';
import { LRUCache } from 'vs/base/common/map';
10
import { commonPrefixLength } from 'vs/base/common/strings';
11 12 13 14
import { IPosition } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes';
J
Johannes Rieken 已提交
15
import { MarkerSeverity } from 'vs/platform/markers/common/markers';
16
import { Iterable } from 'vs/base/common/iterator';
J
Johannes Rieken 已提交
17

J
Johannes Rieken 已提交
18
export abstract class TreeElement {
19

J
Johannes Rieken 已提交
20
	abstract id: string;
21
	abstract children: Map<string, TreeElement>;
22
	abstract parent: TreeElement | undefined;
J
Johannes Rieken 已提交
23

24 25
	abstract adopt(newParent: TreeElement): TreeElement;

26
	remove(): void {
27
		if (this.parent) {
28
			this.parent.children.delete(this.id);
29
		}
30 31
	}

32
	static findId(candidate: DocumentSymbol | string, container: TreeElement): string {
J
Johannes Rieken 已提交
33 34
		// complex id-computation which contains the origin/extension,
		// the parent path, and some dedupe logic when names collide
35 36 37 38 39
		let candidateId: string;
		if (typeof candidate === 'string') {
			candidateId = `${container.id}/${candidate}`;
		} else {
			candidateId = `${container.id}/${candidate.name}`;
40
			if (container.children.get(candidateId) !== undefined) {
41
				candidateId = `${container.id}/${candidate.name}_${candidate.range.startLineNumber}_${candidate.range.startColumn}`;
42 43 44 45
			}
		}

		let id = candidateId;
46
		for (let i = 0; container.children.get(id) !== undefined; i++) {
47
			id = `${candidateId}_${i}`;
J
Johannes Rieken 已提交
48
		}
49

J
Johannes Rieken 已提交
50
		return id;
51
	}
52

53
	static getElementById(id: string, element: TreeElement): TreeElement | undefined {
54 55 56 57 58
		if (!id) {
			return undefined;
		}
		let len = commonPrefixLength(id, element.id);
		if (len === id.length) {
J
Johannes Rieken 已提交
59 60
			return element;
		}
61 62 63
		if (len < element.id.length) {
			return undefined;
		}
64 65
		for (const [, child] of element.children) {
			let candidate = TreeElement.getElementById(id, child);
J
Johannes Rieken 已提交
66 67 68
			if (candidate) {
				return candidate;
			}
J
Johannes Rieken 已提交
69
		}
J
Johannes Rieken 已提交
70
		return undefined;
J
Johannes Rieken 已提交
71
	}
72 73 74

	static size(element: TreeElement): number {
		let res = 1;
75 76
		for (const [, child] of element.children) {
			res += TreeElement.size(child);
77 78 79
		}
		return res;
	}
80 81

	static empty(element: TreeElement): boolean {
82
		return element.children.size === 0;
83
	}
84 85
}

J
Johannes Rieken 已提交
86 87 88 89 90 91 92 93
export interface IOutlineMarker {
	startLineNumber: number;
	startColumn: number;
	endLineNumber: number;
	endColumn: number;
	severity: MarkerSeverity;
}

J
Johannes Rieken 已提交
94
export class OutlineElement extends TreeElement {
J
Johannes Rieken 已提交
95

96
	children = new Map<string, OutlineElement>();
97
	marker: { count: number, topSev: MarkerSeverity } | undefined;
J
Johannes Rieken 已提交
98

J
Johannes Rieken 已提交
99 100
	constructor(
		readonly id: string,
101
		public parent: TreeElement | undefined,
102
		readonly symbol: DocumentSymbol
J
Johannes Rieken 已提交
103
	) {
J
Johannes Rieken 已提交
104
		super();
J
Johannes Rieken 已提交
105
	}
106

107
	adopt(parent: TreeElement): OutlineElement {
108
		let res = new OutlineElement(this.id, parent, this.symbol);
109 110 111
		for (const [key, value] of this.children) {
			res.children.set(key, value.adopt(res));
		}
112 113
		return res;
	}
J
Johannes Rieken 已提交
114 115
}

J
Johannes Rieken 已提交
116
export class OutlineGroup extends TreeElement {
J
Johannes Rieken 已提交
117

118
	children = new Map<string, OutlineElement>();
J
Johannes Rieken 已提交
119

J
Johannes Rieken 已提交
120
	constructor(
J
Johannes Rieken 已提交
121
		readonly id: string,
122
		public parent: TreeElement | undefined,
J
Johannes Rieken 已提交
123 124
		readonly provider: DocumentSymbolProvider,
		readonly providerIndex: number,
J
Johannes Rieken 已提交
125
	) {
J
Johannes Rieken 已提交
126
		super();
J
Johannes Rieken 已提交
127
	}
J
Johannes Rieken 已提交
128

129
	adopt(parent: TreeElement): OutlineGroup {
130
		let res = new OutlineGroup(this.id, parent, this.provider, this.providerIndex);
131 132 133
		for (const [key, value] of this.children) {
			res.children.set(key, value.adopt(res));
		}
134 135 136
		return res;
	}

137
	getItemEnclosingPosition(position: IPosition): OutlineElement | undefined {
J
Johannes Rieken 已提交
138
		return position ? this._getItemEnclosingPosition(position, this.children) : undefined;
J
Johannes Rieken 已提交
139 140
	}

141 142
	private _getItemEnclosingPosition(position: IPosition, children: Map<string, OutlineElement>): OutlineElement | undefined {
		for (const [, item] of children) {
J
Johannes Rieken 已提交
143
			if (!item.symbol.range || !Range.containsPosition(item.symbol.range, position)) {
J
Johannes Rieken 已提交
144
				continue;
J
Johannes Rieken 已提交
145
			}
J
Johannes Rieken 已提交
146
			return this._getItemEnclosingPosition(position, item.children) || item;
J
Johannes Rieken 已提交
147 148 149
		}
		return undefined;
	}
150

J
Johannes Rieken 已提交
151
	updateMarker(marker: IOutlineMarker[]): void {
152 153
		for (const [, child] of this.children) {
			this._updateMarker(marker, child);
154 155 156
		}
	}

J
Johannes Rieken 已提交
157
	private _updateMarker(markers: IOutlineMarker[], item: OutlineElement): void {
158 159 160
		item.marker = undefined;

		// find the proper start index to check for item/marker overlap.
161
		let idx = binarySearch<IRange>(markers, item.symbol.range, Range.compareRangesUsingStarts);
162 163 164
		let start: number;
		if (idx < 0) {
			start = ~idx;
165
			if (start > 0 && Range.areIntersecting(markers[start - 1], item.symbol.range)) {
166 167 168 169 170 171
				start -= 1;
			}
		} else {
			start = idx;
		}

J
Johannes Rieken 已提交
172
		let myMarkers: IOutlineMarker[] = [];
R
Rudi Chen 已提交
173
		let myTopSev: MarkerSeverity | undefined;
174

175
		for (; start < markers.length && Range.areIntersecting(item.symbol.range, markers[start]); start++) {
176 177
			// remove markers intersecting with this outline element
			// and store them in a 'private' array.
178
			let marker = markers[start];
179
			myMarkers.push(marker);
J
Johannes Rieken 已提交
180
			(markers as Array<IOutlineMarker | undefined>)[start] = undefined;
181 182
			if (!myTopSev || marker.severity > myTopSev) {
				myTopSev = marker.severity;
183 184 185
			}
		}

186 187 188 189
		// Recurse into children and let them match markers that have matched
		// this outline element. This might remove markers from this element and
		// therefore we remember that we have had markers. That allows us to render
		// the dot, saying 'this element has children with markers'
190 191
		for (const [, child] of item.children) {
			this._updateMarker(myMarkers, child);
192
		}
193

194
		if (myTopSev) {
195 196 197 198 199
			item.marker = {
				count: myMarkers.length,
				topSev: myTopSev
			};
		}
200

201
		coalesceInPlace(markers);
202
	}
J
Johannes Rieken 已提交
203
}
J
Johannes Rieken 已提交
204

205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
class MovingAverage {

	private _n = 1;
	private _val = 0;

	update(value: number): this {
		this._val = this._val + (value - this._val) / this._n;
		this._n += 1;
		return this;
	}

	get value(): number {
		return this._val;
	}
}

J
Johannes Rieken 已提交
221 222
export class OutlineModel extends TreeElement {

223
	private static readonly _requestDurations = new LRUCache<string, MovingAverage>(50, 0.7);
224
	private static readonly _requests = new LRUCache<string, { promiseCnt: number, source: CancellationTokenSource, promise: Promise<any>, model: OutlineModel | undefined }>(9, 0.75);
J
Johannes Rieken 已提交
225 226 227 228 229
	private static readonly _keys = new class {

		private _counter = 1;
		private _data = new WeakMap<DocumentSymbolProvider, number>();

230 231
		for(textModel: ITextModel, version: boolean): string {
			return `${textModel.id}/${version ? textModel.getVersionId() : ''}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`;
J
Johannes Rieken 已提交
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
		}

		private _hash(providers: DocumentSymbolProvider[]): string {
			let result = '';
			for (const provider of providers) {
				let n = this._data.get(provider);
				if (typeof n === 'undefined') {
					n = this._counter++;
					this._data.set(provider, n);
				}
				result += n;
			}
			return result;
		}
	};

248

249
	static create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
250

251
		let key = this._keys.for(textModel, true);
252 253
		let data = OutlineModel._requests.get(key);

254
		if (!data) {
255
			let source = new CancellationTokenSource();
256 257
			data = {
				promiseCnt: 0,
258 259
				source,
				promise: OutlineModel._create(textModel, source.token),
260 261
				model: undefined,
			};
262
			OutlineModel._requests.set(key, data);
263 264 265 266 267 268 269 270 271 272 273 274

			// keep moving average of request durations
			const now = Date.now();
			data.promise.then(() => {
				let key = this._keys.for(textModel, false);
				let avg = this._requestDurations.get(key);
				if (!avg) {
					avg = new MovingAverage();
					this._requestDurations.set(key, avg);
				}
				avg.update(Date.now() - now);
			});
275 276
		}

277
		if (data!.model) {
278
			// resolved -> return data
279
			return Promise.resolve(data.model!);
280 281
		}

282
		// increase usage counter
283
		data!.promiseCnt += 1;
284

285 286
		token.onCancellationRequested(() => {
			// last -> cancel provider request, remove cached promise
287 288
			if (--data!.promiseCnt === 0) {
				data!.source.cancel();
289 290 291 292 293
				OutlineModel._requests.delete(key);
			}
		});

		return new Promise((resolve, reject) => {
294 295
			data!.promise.then(model => {
				data!.model = model;
296
				resolve(model);
297 298
			}, err => {
				OutlineModel._requests.delete(key);
299 300
				reject(err);
			});
301 302 303
		});
	}

304 305 306 307 308 309 310 311 312 313 314 315
	static getRequestDelay(textModel: ITextModel | null): number {
		if (!textModel) {
			return 350;
		}
		const avg = this._requestDurations.get(this._keys.for(textModel, false));
		if (!avg) {
			return 350;
		}
		return Math.max(350, Math.floor(1.3 * avg.value));
	}

	private static _create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
J
Johannes Rieken 已提交
316

317
		const cts = new CancellationTokenSource(token);
318 319 320
		const result = new OutlineModel(textModel);
		const provider = DocumentSymbolProviderRegistry.ordered(textModel);
		const promises = provider.map((provider, index) => {
321

J
Johannes Rieken 已提交
322 323
			let id = TreeElement.findId(`provider_${index}`, result);
			let group = new OutlineGroup(id, result, provider, index);
324

325
			return Promise.resolve(provider.provideDocumentSymbols(result.textModel, cts.token)).then(result => {
326 327
				for (const info of result || []) {
					OutlineModel._makeOutlineElement(info, group);
J
Johannes Rieken 已提交
328 329 330
				}
				return group;
			}, err => {
J
Johannes Rieken 已提交
331
				onUnexpectedExternalError(err);
J
Johannes Rieken 已提交
332 333
				return group;
			}).then(group => {
334
				if (!TreeElement.empty(group)) {
335
					result._groups.set(id, group);
336 337 338
				} else {
					group.remove();
				}
J
Johannes Rieken 已提交
339 340 341
			});
		});

342 343 344
		const listener = DocumentSymbolProviderRegistry.onDidChange(() => {
			const newProvider = DocumentSymbolProviderRegistry.ordered(textModel);
			if (!equals(newProvider, provider)) {
345
				cts.cancel();
346 347 348 349
			}
		});

		return Promise.all(promises).then(() => {
350
			if (cts.token.isCancellationRequested && !token.isCancellationRequested) {
351 352 353 354 355 356 357
				return OutlineModel._create(textModel, token);
			} else {
				return result._compact();
			}
		}).finally(() => {
			listener.dispose();
		});
358 359
	}

360
	private static _makeOutlineElement(info: DocumentSymbol, container: OutlineGroup | OutlineElement): void {
361
		let id = TreeElement.findId(info, container);
J
Johannes Rieken 已提交
362 363 364 365 366
		let res = new OutlineElement(id, container, info);
		if (info.children) {
			for (const childInfo of info.children) {
				OutlineModel._makeOutlineElement(childInfo, res);
			}
367
		}
368
		container.children.set(res.id, res);
369 370
	}

371
	static get(element: TreeElement | undefined): OutlineModel | undefined {
372 373 374 375 376 377 378 379 380
		while (element) {
			if (element instanceof OutlineModel) {
				return element;
			}
			element = element.parent;
		}
		return undefined;
	}

J
Johannes Rieken 已提交
381 382 383
	readonly id = 'root';
	readonly parent = undefined;

384 385
	protected _groups = new Map<string, OutlineGroup>();
	children = new Map<string, OutlineGroup | OutlineElement>();
J
Johannes Rieken 已提交
386

387
	protected constructor(readonly textModel: ITextModel) {
J
Johannes Rieken 已提交
388
		super();
P
Peng Lyu 已提交
389 390 391

		this.id = 'root';
		this.parent = undefined;
J
Johannes Rieken 已提交
392 393
	}

394 395
	adopt(): OutlineModel {
		let res = new OutlineModel(this.textModel);
396 397 398
		for (const [key, value] of this._groups) {
			res._groups.set(key, value.adopt(res));
		}
399 400
		return res._compact();
	}
J
Johannes Rieken 已提交
401

402
	private _compact(): this {
R
Rudi Chen 已提交
403
		let count = 0;
404 405 406
		for (const [key, group] of this._groups) {
			if (group.children.size === 0) { // empty
				this._groups.delete(key);
R
Rudi Chen 已提交
407 408
			} else {
				count += 1;
409 410
			}
		}
R
Rudi Chen 已提交
411 412 413 414
		if (count !== 1) {
			//
			this.children = this._groups;
		} else {
415
			// adopt all elements of the first group
416 417
			let group = Iterable.first(this._groups.values())!;
			for (let [, child] of group.children) {
418
				child.parent = this;
419
				this.children.set(child.id, child);
420 421 422
			}
		}
		return this;
J
Johannes Rieken 已提交
423 424
	}

425
	merge(other: OutlineModel): boolean {
J
Johannes Rieken 已提交
426 427 428
		if (this.textModel.uri.toString() !== other.textModel.uri.toString()) {
			return false;
		}
429
		if (this._groups.size !== other._groups.size) {
J
Johannes Rieken 已提交
430 431 432 433 434 435 436
			return false;
		}
		this._groups = other._groups;
		this.children = other.children;
		return true;
	}

437
	getItemEnclosingPosition(position: IPosition, context?: OutlineElement): OutlineElement | undefined {
438

439
		let preferredGroup: OutlineGroup | undefined;
440 441 442 443 444 445 446 447 448 449
		if (context) {
			let candidate = context.parent;
			while (candidate && !preferredGroup) {
				if (candidate instanceof OutlineGroup) {
					preferredGroup = candidate;
				}
				candidate = candidate.parent;
			}
		}

450
		let result: OutlineElement | undefined = undefined;
451
		for (const [, group] of this._groups) {
452 453 454
			result = group.getItemEnclosingPosition(position);
			if (result && (!preferredGroup || preferredGroup === group)) {
				break;
J
Johannes Rieken 已提交
455 456
			}
		}
457
		return result;
458 459
	}

460
	getItemById(id: string): TreeElement | undefined {
J
Johannes Rieken 已提交
461
		return TreeElement.getElementById(id, this);
462
	}
463

J
Johannes Rieken 已提交
464
	updateMarker(marker: IOutlineMarker[]): void {
465 466 467 468
		// sort markers by start range so that we can use
		// outline element starts for quicker look up
		marker.sort(Range.compareRangesUsingStarts);

469 470
		for (const [, group] of this._groups) {
			group.updateMarker(marker.slice(0));
471 472
		}
	}
473
}