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

7
import { DocumentSymbolProviderRegistry, DocumentSymbolProvider, DocumentSymbol } from 'vs/editor/common/modes';
J
Johannes Rieken 已提交
8
import { ITextModel } from 'vs/editor/common/model';
J
Johannes Rieken 已提交
9
import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters';
J
Johannes Rieken 已提交
10
import { IPosition } from 'vs/editor/common/core/position';
11
import { Range, IRange } from 'vs/editor/common/core/range';
12
import { first, size, forEach } from 'vs/base/common/collections';
13
import { isFalsyOrEmpty, binarySearch, coalesce } from 'vs/base/common/arrays';
14
import { commonPrefixLength } from 'vs/base/common/strings';
15
import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers';
J
Johannes Rieken 已提交
16
import { onUnexpectedExternalError } from 'vs/base/common/errors';
17
import { LRUCache } from 'vs/base/common/map';
18
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
J
Johannes Rieken 已提交
19

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

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

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

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

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

J
Johannes Rieken 已提交
46
		return id;
47
	}
48

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

	static size(element: TreeElement): number {
		let res = 1;
		for (const key in element.children) {
			res += TreeElement.size(element.children[key]);
		}
		return res;
	}
76 77
}

J
Johannes Rieken 已提交
78
export class OutlineElement extends TreeElement {
J
Johannes Rieken 已提交
79

J
Johannes Rieken 已提交
80 81
	children: { [id: string]: OutlineElement; } = Object.create(null);
	score: FuzzyScore = [0, []];
82
	marker: { count: number, topSev: MarkerSeverity };
J
Johannes Rieken 已提交
83

J
Johannes Rieken 已提交
84 85
	constructor(
		readonly id: string,
86
		public parent: OutlineModel | OutlineGroup | OutlineElement,
87
		readonly symbol: DocumentSymbol
J
Johannes Rieken 已提交
88
	) {
J
Johannes Rieken 已提交
89
		super();
J
Johannes Rieken 已提交
90
	}
91 92 93 94 95 96

	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 已提交
97 98
}

J
Johannes Rieken 已提交
99
export class OutlineGroup extends TreeElement {
J
Johannes Rieken 已提交
100

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

J
Johannes Rieken 已提交
103
	constructor(
J
Johannes Rieken 已提交
104
		readonly id: string,
105
		public parent: OutlineModel,
J
Johannes Rieken 已提交
106 107
		readonly provider: DocumentSymbolProvider,
		readonly providerIndex: number,
J
Johannes Rieken 已提交
108
	) {
J
Johannes Rieken 已提交
109
		super();
J
Johannes Rieken 已提交
110
	}
J
Johannes Rieken 已提交
111

112 113 114 115 116 117
	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 已提交
118 119 120 121
	updateMatches(pattern: string, topMatch: OutlineElement): OutlineElement {
		for (const key in this.children) {
			topMatch = this._updateMatches(pattern, this.children[key], topMatch);
		}
122 123 124
		return topMatch;
	}

J
Johannes Rieken 已提交
125
	private _updateMatches(pattern: string, item: OutlineElement, topMatch: OutlineElement): OutlineElement {
126
		item.score = fuzzyScore(pattern, item.symbol.name, undefined, true);
J
Johannes Rieken 已提交
127
		if (item.score && (!topMatch || item.score[0] > topMatch.score[0])) {
128 129
			topMatch = item;
		}
J
Johannes Rieken 已提交
130 131 132 133
		for (const key in item.children) {
			let child = item.children[key];
			topMatch = this._updateMatches(pattern, child, topMatch);
			if (!item.score && child.score) {
134
				// don't filter parents with unfiltered children
J
Johannes Rieken 已提交
135
				item.score = [0, []];
136
			}
J
Johannes Rieken 已提交
137
		}
138
		return topMatch;
J
Johannes Rieken 已提交
139
	}
J
Johannes Rieken 已提交
140

J
Johannes Rieken 已提交
141 142 143 144 145 146 147
	getItemEnclosingPosition(position: IPosition): OutlineElement {
		return this._getItemEnclosingPosition(position, this.children);
	}

	private _getItemEnclosingPosition(position: IPosition, children: { [id: string]: OutlineElement }): OutlineElement {
		for (let key in children) {
			let item = children[key];
148
			if (!Range.containsPosition(item.symbol.range, position)) {
J
Johannes Rieken 已提交
149
				continue;
J
Johannes Rieken 已提交
150
			}
J
Johannes Rieken 已提交
151
			return this._getItemEnclosingPosition(position, item.children) || item;
J
Johannes Rieken 已提交
152 153 154
		}
		return undefined;
	}
155 156 157 158 159 160 161

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

162 163 164 165 166
	private _updateMarker(markers: IMarker[], item: OutlineElement): void {

		item.marker = undefined;

		// find the proper start index to check for item/marker overlap.
167
		let idx = binarySearch<IRange>(markers, item.symbol.range, Range.compareRangesUsingStarts);
168 169 170
		let start: number;
		if (idx < 0) {
			start = ~idx;
171
			if (start > 0 && Range.areIntersecting(markers[start - 1], item.symbol.range)) {
172 173 174 175 176 177 178 179 180
				start -= 1;
			}
		} else {
			start = idx;
		}

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

181
		for (; start < markers.length && Range.areIntersecting(item.symbol.range, markers[start]); start++) {
182 183
			// remove markers intersecting with this outline element
			// and store them in a 'private' array.
184
			let marker = markers[start];
185
			myMarkers.push(marker);
186
			markers[start] = undefined;
187 188
			if (!myTopSev || marker.severity > myTopSev) {
				myTopSev = marker.severity;
189 190 191
			}
		}

192 193 194 195
		// 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'
196 197 198
		for (const key in item.children) {
			this._updateMarker(myMarkers, item.children[key]);
		}
199

200
		if (myTopSev) {
201 202 203 204 205
			item.marker = {
				count: myMarkers.length,
				topSev: myTopSev
			};
		}
206 207

		coalesce(markers, true);
208
	}
J
Johannes Rieken 已提交
209
}
J
Johannes Rieken 已提交
210

J
Johannes Rieken 已提交
211 212
export class OutlineModel extends TreeElement {

213
	private static readonly _requests = new LRUCache<string, { promiseCnt: number, source: CancellationTokenSource, promise: Promise<any>, model: OutlineModel }>(9, .75);
J
Johannes Rieken 已提交
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
	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;
		}
	};

237

238
	static create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
239

J
Johannes Rieken 已提交
240
		let key = this._keys.for(textModel);
241 242
		let data = OutlineModel._requests.get(key);

243
		if (!data) {
244
			let source = new CancellationTokenSource();
245 246
			data = {
				promiseCnt: 0,
247 248
				source,
				promise: OutlineModel._create(textModel, source.token),
249 250
				model: undefined,
			};
251 252 253
			OutlineModel._requests.set(key, data);
		}

254 255
		if (data.model) {
			// resolved -> return data
256
			return Promise.resolve(data.model);
257 258
		}

259
		// increase usage counter
260
		data.promiseCnt += 1;
261

262 263 264 265 266 267 268 269 270
		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) => {
271 272 273
			data.promise.then(model => {
				data.model = model;
				resolve(model);
274 275
			}, err => {
				OutlineModel._requests.delete(key);
276 277
				reject(err);
			});
278 279 280
		});
	}

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

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

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

289
			return Promise.resolve(provider.provideDocumentSymbols(result.textModel, token)).then(result => {
J
Johannes Rieken 已提交
290 291 292 293
				if (!isFalsyOrEmpty(result)) {
					for (const info of result) {
						OutlineModel._makeOutlineElement(info, group);
					}
J
Johannes Rieken 已提交
294 295 296
				}
				return group;
			}, err => {
J
Johannes Rieken 已提交
297
				onUnexpectedExternalError(err);
J
Johannes Rieken 已提交
298 299
				return group;
			}).then(group => {
J
Johannes Rieken 已提交
300
				result._groups[id] = group;
J
Johannes Rieken 已提交
301 302 303
			});
		});

304
		return Promise.all(promises).then(() => result._compact());
305 306
	}

307
	private static _makeOutlineElement(info: DocumentSymbol, container: OutlineGroup | OutlineElement): void {
308
		let id = TreeElement.findId(info, container);
J
Johannes Rieken 已提交
309 310 311 312 313
		let res = new OutlineElement(id, container, info);
		if (info.children) {
			for (const childInfo of info.children) {
				OutlineModel._makeOutlineElement(childInfo, res);
			}
314
		}
J
Johannes Rieken 已提交
315
		container.children[res.id] = res;
316 317
	}

318 319 320 321 322 323 324 325 326 327
	static get(element: TreeElement): OutlineModel {
		while (element) {
			if (element instanceof OutlineModel) {
				return element;
			}
			element = element.parent;
		}
		return undefined;
	}

J
Johannes Rieken 已提交
328 329 330
	readonly id = 'root';
	readonly parent = undefined;

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

334
	protected constructor(readonly textModel: ITextModel) {
J
Johannes Rieken 已提交
335 336 337
		super();
	}

338 339 340 341 342
	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 已提交
343

344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
	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 已提交
367 368
	}

369
	merge(other: OutlineModel): boolean {
J
Johannes Rieken 已提交
370 371 372 373 374 375 376 377 378 379 380
		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;
	}

J
Johannes Rieken 已提交
381 382
	updateMatches(pattern: string): OutlineElement {
		let topMatch: OutlineElement;
383 384
		for (const key in this._groups) {
			topMatch = this._groups[key].updateMatches(pattern, topMatch);
J
Johannes Rieken 已提交
385 386
		}
		return topMatch;
387 388
	}

389 390 391 392 393 394 395 396 397 398 399 400 401 402
	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;
			}
		}

		let result: OutlineElement = undefined;
403
		for (const key in this._groups) {
404 405 406 407
			const group = this._groups[key];
			result = group.getItemEnclosingPosition(position);
			if (result && (!preferredGroup || preferredGroup === group)) {
				break;
J
Johannes Rieken 已提交
408 409
			}
		}
410
		return result;
411 412
	}

J
Johannes Rieken 已提交
413 414
	getItemById(id: string): TreeElement {
		return TreeElement.getElementById(id, this);
415
	}
416 417 418 419 420 421 422

	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) {
423
			this._groups[key].updateMarker(marker.slice(0));
424 425
		}
	}
426
}