outlineModel.ts 12.7 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 } from 'vs/base/common/arrays';
7 8 9
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { first, forEach, size } from 'vs/base/common/collections';
import { onUnexpectedExternalError } from 'vs/base/common/errors';
J
Johannes Rieken 已提交
10
import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters';
11
import { LRUCache } from 'vs/base/common/map';
12
import { commonPrefixLength } from 'vs/base/common/strings';
13 14 15 16
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';
17
import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers';
J
Johannes Rieken 已提交
18

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

J
Johannes Rieken 已提交
21 22
	abstract id: string;
	abstract children: { [id: string]: TreeElement };
23
	abstract parent: TreeElement;
J
Johannes Rieken 已提交
24

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

27 28 29 30
	remove(): void {
		delete this.parent.children[this.id];
	}

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

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

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

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

	static size(element: TreeElement): number {
		let res = 1;
		for (const key in element.children) {
			res += TreeElement.size(element.children[key]);
		}
		return res;
	}
79 80 81 82 83 84 85

	static empty(element: TreeElement): boolean {
		for (const _key in element.children) {
			return false;
		}
		return true;
	}
86 87
}

J
Johannes Rieken 已提交
88
export class OutlineElement extends TreeElement {
J
Johannes Rieken 已提交
89

J
Johannes Rieken 已提交
90
	children: { [id: string]: OutlineElement; } = Object.create(null);
91
	score: FuzzyScore = FuzzyScore.Default;
92
	marker: { count: number, topSev: MarkerSeverity };
J
Johannes Rieken 已提交
93

J
Johannes Rieken 已提交
94 95
	constructor(
		readonly id: string,
96
		public parent: OutlineModel | OutlineGroup | OutlineElement,
97
		readonly symbol: DocumentSymbol
J
Johannes Rieken 已提交
98
	) {
J
Johannes Rieken 已提交
99
		super();
J
Johannes Rieken 已提交
100
	}
101 102 103 104 105 106

	adopt(parent: OutlineModel | OutlineGroup | OutlineElement): OutlineElement {
		let res = new OutlineElement(this.id, parent, this.symbol);
		forEach(this.children, entry => res.children[entry.key] = entry.value.adopt(res));
		return res;
	}
J
Johannes Rieken 已提交
107 108
}

J
Johannes Rieken 已提交
109
export class OutlineGroup extends TreeElement {
J
Johannes Rieken 已提交
110

J
Johannes Rieken 已提交
111
	children: { [id: string]: OutlineElement; } = Object.create(null);
J
Johannes Rieken 已提交
112

J
Johannes Rieken 已提交
113
	constructor(
J
Johannes Rieken 已提交
114
		readonly id: string,
115
		public parent: OutlineModel,
J
Johannes Rieken 已提交
116 117
		readonly provider: DocumentSymbolProvider,
		readonly providerIndex: number,
J
Johannes Rieken 已提交
118
	) {
J
Johannes Rieken 已提交
119
		super();
J
Johannes Rieken 已提交
120
	}
J
Johannes Rieken 已提交
121

122 123 124 125 126 127
	adopt(parent: OutlineModel): OutlineGroup {
		let res = new OutlineGroup(this.id, parent, this.provider, this.providerIndex);
		forEach(this.children, entry => res.children[entry.key] = entry.value.adopt(res));
		return res;
	}

J
Johannes Rieken 已提交
128 129 130 131
	updateMatches(pattern: string, topMatch: OutlineElement): OutlineElement {
		for (const key in this.children) {
			topMatch = this._updateMatches(pattern, this.children[key], topMatch);
		}
132 133 134
		return topMatch;
	}

J
Johannes Rieken 已提交
135
	private _updateMatches(pattern: string, item: OutlineElement, topMatch: OutlineElement): OutlineElement {
J
Johannes Rieken 已提交
136 137 138

		item.score = pattern
			? fuzzyScore(pattern, pattern.toLowerCase(), 0, item.symbol.name, item.symbol.name.toLowerCase(), 0, true)
139
			: FuzzyScore.Default;
J
Johannes Rieken 已提交
140

J
Johannes Rieken 已提交
141
		if (item.score && (!topMatch || item.score[0] > topMatch.score[0])) {
142 143
			topMatch = item;
		}
J
Johannes Rieken 已提交
144 145 146 147
		for (const key in item.children) {
			let child = item.children[key];
			topMatch = this._updateMatches(pattern, child, topMatch);
			if (!item.score && child.score) {
148
				// don't filter parents with unfiltered children
149
				item.score = FuzzyScore.Default;
150
			}
J
Johannes Rieken 已提交
151
		}
152
		return topMatch;
J
Johannes Rieken 已提交
153
	}
J
Johannes Rieken 已提交
154

J
Johannes Rieken 已提交
155
	getItemEnclosingPosition(position: IPosition): OutlineElement {
J
Johannes Rieken 已提交
156
		return position ? this._getItemEnclosingPosition(position, this.children) : undefined;
J
Johannes Rieken 已提交
157 158 159 160 161
	}

	private _getItemEnclosingPosition(position: IPosition, children: { [id: string]: OutlineElement }): OutlineElement {
		for (let key in children) {
			let item = children[key];
J
Johannes Rieken 已提交
162
			if (!item.symbol.range || !Range.containsPosition(item.symbol.range, position)) {
J
Johannes Rieken 已提交
163
				continue;
J
Johannes Rieken 已提交
164
			}
J
Johannes Rieken 已提交
165
			return this._getItemEnclosingPosition(position, item.children) || item;
J
Johannes Rieken 已提交
166 167 168
		}
		return undefined;
	}
169 170 171 172 173 174 175

	updateMarker(marker: IMarker[]): void {
		for (const key in this.children) {
			this._updateMarker(marker, this.children[key]);
		}
	}

176 177 178 179 180
	private _updateMarker(markers: IMarker[], item: OutlineElement): void {

		item.marker = undefined;

		// find the proper start index to check for item/marker overlap.
181
		let idx = binarySearch<IRange>(markers, item.symbol.range, Range.compareRangesUsingStarts);
182 183 184
		let start: number;
		if (idx < 0) {
			start = ~idx;
185
			if (start > 0 && Range.areIntersecting(markers[start - 1], item.symbol.range)) {
186 187 188 189 190 191 192 193 194
				start -= 1;
			}
		} else {
			start = idx;
		}

		let myMarkers: IMarker[] = [];
		let myTopSev: MarkerSeverity;

195
		for (; start < markers.length && Range.areIntersecting(item.symbol.range, markers[start]); start++) {
196 197
			// remove markers intersecting with this outline element
			// and store them in a 'private' array.
198
			let marker = markers[start];
199
			myMarkers.push(marker);
200
			markers[start] = undefined;
201 202
			if (!myTopSev || marker.severity > myTopSev) {
				myTopSev = marker.severity;
203 204 205
			}
		}

206 207 208 209
		// 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'
210 211 212
		for (const key in item.children) {
			this._updateMarker(myMarkers, item.children[key]);
		}
213

214
		if (myTopSev) {
215 216 217 218 219
			item.marker = {
				count: myMarkers.length,
				topSev: myTopSev
			};
		}
220

221
		coalesceInPlace(markers);
222
	}
J
Johannes Rieken 已提交
223
}
J
Johannes Rieken 已提交
224

J
Johannes Rieken 已提交
225 226
export class OutlineModel extends TreeElement {

227
	private static readonly _requests = new LRUCache<string, { promiseCnt: number, source: CancellationTokenSource, promise: Promise<any>, model: OutlineModel }>(9, .75);
J
Johannes Rieken 已提交
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
	private static readonly _keys = new class {

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

		for(textModel: ITextModel): string {
			return `${textModel.id}/${textModel.getVersionId()}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`;
		}

		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;
		}
	};

251

252
	static create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
253

J
Johannes Rieken 已提交
254
		let key = this._keys.for(textModel);
255 256
		let data = OutlineModel._requests.get(key);

257
		if (!data) {
258
			let source = new CancellationTokenSource();
259 260
			data = {
				promiseCnt: 0,
261 262
				source,
				promise: OutlineModel._create(textModel, source.token),
263 264
				model: undefined,
			};
265 266 267
			OutlineModel._requests.set(key, data);
		}

268 269
		if (data.model) {
			// resolved -> return data
270
			return Promise.resolve(data.model);
271 272
		}

273
		// increase usage counter
274
		data.promiseCnt += 1;
275

276 277 278 279 280 281 282 283 284
		token.onCancellationRequested(() => {
			// last -> cancel provider request, remove cached promise
			if (--data.promiseCnt === 0) {
				data.source.cancel();
				OutlineModel._requests.delete(key);
			}
		});

		return new Promise((resolve, reject) => {
285 286 287
			data.promise.then(model => {
				data.model = model;
				resolve(model);
288 289
			}, err => {
				OutlineModel._requests.delete(key);
290 291
				reject(err);
			});
292 293 294
		});
	}

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

J
Johannes Rieken 已提交
297 298
		let result = new OutlineModel(textModel);
		let promises = DocumentSymbolProviderRegistry.ordered(textModel).map((provider, index) => {
299

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

303
			return Promise.resolve(provider.provideDocumentSymbols(result.textModel, token)).then(result => {
304 305
				for (const info of result || []) {
					OutlineModel._makeOutlineElement(info, group);
J
Johannes Rieken 已提交
306 307 308
				}
				return group;
			}, err => {
J
Johannes Rieken 已提交
309
				onUnexpectedExternalError(err);
J
Johannes Rieken 已提交
310 311
				return group;
			}).then(group => {
312 313 314 315 316
				if (!TreeElement.empty(group)) {
					result._groups[id] = group;
				} else {
					group.remove();
				}
J
Johannes Rieken 已提交
317 318 319
			});
		});

320
		return Promise.all(promises).then(() => result._compact());
321 322
	}

323
	private static _makeOutlineElement(info: DocumentSymbol, container: OutlineGroup | OutlineElement): void {
324
		let id = TreeElement.findId(info, container);
J
Johannes Rieken 已提交
325 326 327 328 329
		let res = new OutlineElement(id, container, info);
		if (info.children) {
			for (const childInfo of info.children) {
				OutlineModel._makeOutlineElement(childInfo, res);
			}
330
		}
J
Johannes Rieken 已提交
331
		container.children[res.id] = res;
332 333
	}

334 335 336 337 338 339 340 341 342 343
	static get(element: TreeElement): OutlineModel {
		while (element) {
			if (element instanceof OutlineModel) {
				return element;
			}
			element = element.parent;
		}
		return undefined;
	}

J
Johannes Rieken 已提交
344 345 346
	readonly id = 'root';
	readonly parent = undefined;

347
	protected _groups: { [id: string]: OutlineGroup; } = Object.create(null);
J
Johannes Rieken 已提交
348 349
	children: { [id: string]: OutlineGroup | OutlineElement; } = Object.create(null);

350
	protected constructor(readonly textModel: ITextModel) {
J
Johannes Rieken 已提交
351 352 353
		super();
	}

354 355 356 357 358
	adopt(): OutlineModel {
		let res = new OutlineModel(this.textModel);
		forEach(this._groups, entry => res._groups[entry.key] = entry.value.adopt(res));
		return res._compact();
	}
J
Johannes Rieken 已提交
359

360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
	private _compact(): this {
		let count = 0;
		for (const key in this._groups) {
			let group = this._groups[key];
			if (first(group.children) === undefined) { // empty
				delete this._groups[key];
			} else {
				count += 1;
			}
		}
		if (count !== 1) {
			//
			this.children = this._groups;
		} else {
			// adopt all elements of the first group
			let group = first(this._groups);
			for (let key in group.children) {
				let child = group.children[key];
				child.parent = this;
				this.children[child.id] = child;
			}
		}
		return this;
J
Johannes Rieken 已提交
383 384
	}

385
	merge(other: OutlineModel): boolean {
J
Johannes Rieken 已提交
386 387 388 389 390 391 392 393 394 395 396
		if (this.textModel.uri.toString() !== other.textModel.uri.toString()) {
			return false;
		}
		if (size(this._groups) !== size(other._groups)) {
			return false;
		}
		this._groups = other._groups;
		this.children = other.children;
		return true;
	}

397 398
	private _matches: [string, OutlineElement];

J
Johannes Rieken 已提交
399
	updateMatches(pattern: string): OutlineElement {
400 401 402
		if (this._matches && this._matches[0] === pattern) {
			return this._matches[1];
		}
J
Johannes Rieken 已提交
403
		let topMatch: OutlineElement;
404 405
		for (const key in this._groups) {
			topMatch = this._groups[key].updateMatches(pattern, topMatch);
J
Johannes Rieken 已提交
406
		}
407
		this._matches = [pattern, topMatch];
J
Johannes Rieken 已提交
408
		return topMatch;
409 410
	}

411 412 413 414 415 416 417 418 419 420 421 422 423
	getItemEnclosingPosition(position: IPosition, context?: OutlineElement): OutlineElement {

		let preferredGroup: OutlineGroup;
		if (context) {
			let candidate = context.parent;
			while (candidate && !preferredGroup) {
				if (candidate instanceof OutlineGroup) {
					preferredGroup = candidate;
				}
				candidate = candidate.parent;
			}
		}

424
		let result: OutlineElement | undefined = undefined;
425
		for (const key in this._groups) {
426 427 428 429
			const group = this._groups[key];
			result = group.getItemEnclosingPosition(position);
			if (result && (!preferredGroup || preferredGroup === group)) {
				break;
J
Johannes Rieken 已提交
430 431
			}
		}
432
		return result;
433 434
	}

J
Johannes Rieken 已提交
435 436
	getItemById(id: string): TreeElement {
		return TreeElement.getElementById(id, this);
437
	}
438 439 440 441 442 443 444

	updateMarker(marker: IMarker[]): void {
		// sort markers by start range so that we can use
		// outline element starts for quicker look up
		marker.sort(Range.compareRangesUsingStarts);

		for (const key in this._groups) {
445
			this._groups[key].updateMarker(marker.slice(0));
446 447
		}
	}
448
}