decorationsService.ts 13.6 KB
Newer Older
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 { URI } from 'vs/base/common/uri';
M
Matt Bierner 已提交
7
import { Event, Emitter, debounceEvent, anyEvent } from 'vs/base/common/event';
J
Johannes Rieken 已提交
8
import { IDecorationsService, IDecoration, IResourceDecorationChangeEvent, IDecorationsProvider, IDecorationData } from './decorations';
9
import { TernarySearchTree } from 'vs/base/common/map';
10
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
11 12
import { isThenable } from 'vs/base/common/async';
import { LinkedList } from 'vs/base/common/linkedList';
13
import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
14
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
15
import { IdGenerator } from 'vs/base/common/idGenerator';
J
Joao Moreno 已提交
16
import { Iterator } from 'vs/base/common/iterator';
17
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
18
import { localize } from 'vs/nls';
J
Johannes Rieken 已提交
19
import { isPromiseCanceledError } from 'vs/base/common/errors';
20
import { CancellationTokenSource } from 'vs/base/common/cancellation';
21

J
Johannes Rieken 已提交
22
class DecorationRule {
23

J
Johannes Rieken 已提交
24
	static keyOf(data: IDecorationData | IDecorationData[]): string {
J
Johannes Rieken 已提交
25 26 27
		if (Array.isArray(data)) {
			return data.map(DecorationRule.keyOf).join(',');
		} else {
28 29
			const { color, letter } = data;
			return `${color}/${letter}`;
J
Johannes Rieken 已提交
30
		}
J
Johannes Rieken 已提交
31 32 33 34
	}

	private static readonly _classNames = new IdGenerator('monaco-decorations-style-');

J
Johannes Rieken 已提交
35
	readonly data: IDecorationData | IDecorationData[];
J
Johannes Rieken 已提交
36 37 38
	readonly itemColorClassName: string;
	readonly itemBadgeClassName: string;
	readonly bubbleBadgeClassName: string;
J
Johannes Rieken 已提交
39

J
Johannes Rieken 已提交
40
	constructor(data: IDecorationData | IDecorationData[]) {
J
Johannes Rieken 已提交
41
		this.data = data;
J
Johannes Rieken 已提交
42 43 44
		this.itemColorClassName = DecorationRule._classNames.nextId();
		this.itemBadgeClassName = DecorationRule._classNames.nextId();
		this.bubbleBadgeClassName = DecorationRule._classNames.nextId();
J
Johannes Rieken 已提交
45 46 47
	}

	appendCSSRules(element: HTMLStyleElement, theme: ITheme): void {
J
Johannes Rieken 已提交
48 49 50 51 52 53 54
		if (!Array.isArray(this.data)) {
			this._appendForOne(this.data, element, theme);
		} else {
			this._appendForMany(this.data, element, theme);
		}
	}

J
Johannes Rieken 已提交
55
	private _appendForOne(data: IDecorationData, element: HTMLStyleElement, theme: ITheme): void {
56
		const { color, letter } = data;
J
Johannes Rieken 已提交
57
		// label
M
Matt Bierner 已提交
58
		createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(theme, color)};`, element);
59
		// letter
60
		if (letter) {
M
Matt Bierner 已提交
61
			createCSSRule(`.${this.itemBadgeClassName}::after`, `content: "${letter}"; color: ${getColor(theme, color)};`, element);
62
		}
J
Johannes Rieken 已提交
63 64
	}

J
Johannes Rieken 已提交
65
	private _appendForMany(data: IDecorationData[], element: HTMLStyleElement, theme: ITheme): void {
J
Johannes Rieken 已提交
66
		// label
67
		const { color } = data[0];
M
Matt Bierner 已提交
68
		createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(theme, color)};`, element);
J
Johannes Rieken 已提交
69 70

		// badge
71
		const letters = data.filter(d => !isFalsyOrWhitespace(d.letter)).map(d => d.letter);
72
		if (letters.length) {
M
Matt Bierner 已提交
73
			createCSSRule(`.${this.itemBadgeClassName}::after`, `content: "${letters.join(', ')}"; color: ${getColor(theme, color)};`, element);
74
		}
J
Johannes Rieken 已提交
75 76

		// bubble badge
J
Johannes Rieken 已提交
77 78
		createCSSRule(
			`.${this.bubbleBadgeClassName}::after`,
M
Matt Bierner 已提交
79
			`content: "\uf052"; color: ${getColor(theme, color)}; font-family: octicons; font-size: 14px; padding-right: 14px; opacity: 0.4;`,
J
Johannes Rieken 已提交
80 81
			element
		);
J
Johannes Rieken 已提交
82 83
	}

J
Johannes Rieken 已提交
84
	removeCSSRules(element: HTMLStyleElement): void {
J
Johannes Rieken 已提交
85 86 87
		removeCSSRulesContainingSelector(this.itemColorClassName, element);
		removeCSSRulesContainingSelector(this.itemBadgeClassName, element);
		removeCSSRulesContainingSelector(this.bubbleBadgeClassName, element);
J
Johannes Rieken 已提交
88
	}
J
Johannes Rieken 已提交
89 90

	isUnused(): boolean {
J
Johannes Rieken 已提交
91 92 93
		return !document.querySelector(`.${this.itemColorClassName}`)
			&& !document.querySelector(`.${this.itemBadgeClassName}`)
			&& !document.querySelector(`.${this.bubbleBadgeClassName}`);
J
Johannes Rieken 已提交
94
	}
J
Johannes Rieken 已提交
95 96 97
}

class DecorationStyles {
98 99 100

	private readonly _disposables: IDisposable[];
	private readonly _styleElement = createStyleSheet();
101
	private readonly _decorationRules = new Map<string, DecorationRule>();
102 103 104 105

	constructor(
		private _themeService: IThemeService,
	) {
J
Johannes Rieken 已提交
106
		this._disposables = [this._themeService.onThemeChange(this._onThemeChange, this)];
107 108 109 110
	}

	dispose(): void {
		dispose(this._disposables);
M
Matt Bierner 已提交
111 112 113 114 115

		const parent = this._styleElement.parentElement;
		if (parent) {
			parent.removeChild(this._styleElement);
		}
116 117
	}

118 119 120
	asDecoration(data: IDecorationData[], onlyChildren: boolean): IDecoration {

		// sort by weight
121
		data.sort((a, b) => (b.weight || 0) - (a.weight || 0));
122

J
Johannes Rieken 已提交
123
		let key = DecorationRule.keyOf(data);
124
		let rule = this._decorationRules.get(key);
125

J
Johannes Rieken 已提交
126 127 128
		if (!rule) {
			// new css rule
			rule = new DecorationRule(data);
129
			this._decorationRules.set(key, rule);
J
Johannes Rieken 已提交
130
			rule.appendCSSRules(this._styleElement, this._themeService.getTheme());
131 132
		}

J
Johannes Rieken 已提交
133 134 135 136 137 138 139
		let labelClassName = rule.itemColorClassName;
		let badgeClassName = rule.itemBadgeClassName;
		let tooltip = data.filter(d => !isFalsyOrWhitespace(d.tooltip)).map(d => d.tooltip).join('');

		if (onlyChildren) {
			// show items from its children only
			badgeClassName = rule.bubbleBadgeClassName;
I
Itamar Kestenbaum 已提交
140
			tooltip = localize('bubbleTitle', "Contains emphasized items");
J
Johannes Rieken 已提交
141 142
		}

143
		return {
J
Johannes Rieken 已提交
144 145 146
			labelClassName,
			badgeClassName,
			tooltip,
147
			update: (replace) => {
148
				let newData = data.slice();
149 150 151 152
				for (let i = 0; i < newData.length; i++) {
					if (newData[i].source === replace.source) {
						// replace
						newData[i] = replace;
153 154 155 156 157
					}
				}
				return this.asDecoration(newData, onlyChildren);
			}
		};
158 159 160
	}

	private _onThemeChange(): void {
161
		this._decorationRules.forEach(rule => {
J
Johannes Rieken 已提交
162 163
			rule.removeCSSRules(this._styleElement);
			rule.appendCSSRules(this._styleElement, this._themeService.getTheme());
164 165
		});
	}
166

J
Joao Moreno 已提交
167
	cleanUp(iter: Iterator<DecorationProviderWrapper>): void {
168 169
		// remove every rule for which no more
		// decoration (data) is kept. this isn't cheap
J
Johannes Rieken 已提交
170 171 172
		let usedDecorations = new Set<string>();
		for (let e = iter.next(); !e.done; e = iter.next()) {
			e.value.data.forEach((value, key) => {
173
				if (value && !(value instanceof DecorationDataRequest)) {
J
Johannes Rieken 已提交
174 175 176 177 178 179
					usedDecorations.add(DecorationRule.keyOf(value));
				}
			});
		}
		this._decorationRules.forEach((value, index) => {
			const { data } = value;
J
Johannes Rieken 已提交
180
			if (value.isUnused()) {
M
Matt Bierner 已提交
181
				let remove: boolean = false;
J
Johannes Rieken 已提交
182 183 184 185 186 187 188 189 190
				if (Array.isArray(data)) {
					remove = data.some(data => !usedDecorations.has(DecorationRule.keyOf(data)));
				} else if (!usedDecorations.has(DecorationRule.keyOf(data))) {
					remove = true;
				}
				if (remove) {
					value.removeCSSRules(this._styleElement);
					this._decorationRules.delete(index);
				}
J
Johannes Rieken 已提交
191 192
			}
		});
193
	}
194 195
}

196 197 198 199 200 201 202 203
class FileDecorationChangeEvent implements IResourceDecorationChangeEvent {

	private readonly _data = TernarySearchTree.forPaths<boolean>();

	affectsResource(uri: URI): boolean {
		return this._data.get(uri.toString()) || this._data.findSuperstr(uri.toString()) !== undefined;
	}

204
	static debouncer(last: FileDecorationChangeEvent, current: URI | URI[]) {
205 206 207
		if (!last) {
			last = new FileDecorationChangeEvent();
		}
208 209 210 211 212 213 214 215 216 217
		if (Array.isArray(current)) {
			// many
			for (const uri of current) {
				last._data.set(uri.toString(), true);
			}
		} else {
			// one
			last._data.set(current.toString(), true);
		}

218 219 220 221
		return last;
	}
}

222 223 224 225 226 227 228
class DecorationDataRequest {
	constructor(
		readonly source: CancellationTokenSource,
		readonly thenable: Thenable<void>,
	) { }
}

229 230
class DecorationProviderWrapper {

231
	readonly data = TernarySearchTree.forPaths<DecorationDataRequest | IDecorationData | null>();
232
	private readonly _dispoable: IDisposable;
233 234 235

	constructor(
		private readonly _provider: IDecorationsProvider,
236 237
		private readonly _uriEmitter: Emitter<URI | URI[]>,
		private readonly _flushEmitter: Emitter<IResourceDecorationChangeEvent>
238
	) {
239 240 241 242 243 244 245 246
		this._dispoable = this._provider.onDidChange(uris => {
			if (!uris) {
				// flush event -> drop all data, can affect everything
				this.data.clear();
				this._flushEmitter.fire({ affectsResource() { return true; } });

			} else {
				// selective changes -> drop for resource, fetch again, send event
J
Johannes Rieken 已提交
247 248 249
				// perf: the map stores thenables, decorations, or `null`-markers.
				// we make us of that and ignore all uris in which we have never
				// been interested.
250
				for (const uri of uris) {
J
Johannes Rieken 已提交
251
					this._fetchData(uri);
252 253 254
				}
			}
		});
255 256 257
	}

	dispose(): void {
258
		this._dispoable.dispose();
259
		this.data.clear();
260 261
	}

262
	knowsAbout(uri: URI): boolean {
263
		return Boolean(this.data.get(uri.toString())) || Boolean(this.data.findSuperstr(uri.toString()));
264 265
	}

J
Johannes Rieken 已提交
266
	getOrRetrieve(uri: URI, includeChildren: boolean, callback: (data: IDecorationData, isChild: boolean) => void): void {
267
		const key = uri.toString();
268
		let item = this.data.get(key);
269

J
Johannes Rieken 已提交
270 271
		if (item === undefined) {
			// unknown -> trigger request
272
			item = this._fetchData(uri);
273 274
		}

275
		if (item && !(item instanceof DecorationDataRequest)) {
J
Johannes Rieken 已提交
276
			// found something (which isn't pending anymore)
277
			callback(item, false);
278
		}
J
Johannes Rieken 已提交
279

280 281
		if (includeChildren) {
			// (resolved) children
282 283 284
			const iter = this.data.findSuperstr(key);
			if (iter) {
				for (let item = iter.next(); !item.done; item = iter.next()) {
285
					if (item.value && !(item.value instanceof DecorationDataRequest)) {
286
						callback(item.value, true);
287
					}
288
				}
289 290 291 292
			}
		}
	}

293
	private _fetchData(uri: URI): IDecorationData | undefined | null {
294

J
Johannes Rieken 已提交
295 296
		// check for pending request and cancel it
		const pendingRequest = this.data.get(uri.toString());
297 298
		if (pendingRequest instanceof DecorationDataRequest) {
			pendingRequest.source.cancel();
J
Johannes Rieken 已提交
299 300 301
			this.data.delete(uri.toString());
		}

302 303
		const source = new CancellationTokenSource();
		const dataOrThenable = this._provider.provideDecorations(uri, source.token);
304
		if (!isThenable(dataOrThenable)) {
305
			// sync -> we have a result now
306
			return this._keepItem(uri, dataOrThenable);
307

308 309
		} else {
			// async -> we have a result soon
310
			const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => {
J
Johannes Rieken 已提交
311 312 313
				if (this.data.get(uri.toString()) === request) {
					this._keepItem(uri, data);
				}
314
			}).catch(err => {
J
Johannes Rieken 已提交
315 316 317
				if (!isPromiseCanceledError(err) && this.data.get(uri.toString()) === request) {
					this.data.delete(uri.toString());
				}
318
			}));
319

320
			this.data.set(uri.toString(), request);
321 322
			return undefined;
		}
323
	}
324

325 326 327
	private _keepItem(uri: URI, data: IDecorationData | null | undefined): IDecorationData | null {
		const deco = data ? data : null;
		const old = this.data.set(uri.toString(), deco);
328 329 330 331
		if (deco || old) {
			// only fire event when something changed
			this._uriEmitter.fire(uri);
		}
332
		return deco;
333
	}
334 335
}

J
Johannes Rieken 已提交
336
export class FileDecorationsService implements IDecorationsService {
337

338
	_serviceBrand: any;
339

340
	private readonly _data = new LinkedList<DecorationProviderWrapper>();
341
	private readonly _onDidChangeDecorationsDelayed = new Emitter<URI | URI[]>();
342
	private readonly _onDidChangeDecorations = new Emitter<IResourceDecorationChangeEvent>({ leakWarningThreshold: 500 });
J
Johannes Rieken 已提交
343
	private readonly _decorationStyles: DecorationStyles;
344
	private readonly _disposables: IDisposable[];
345

346
	readonly onDidChangeDecorations: Event<IResourceDecorationChangeEvent> = anyEvent(
347 348 349
		this._onDidChangeDecorations.event,
		debounceEvent<URI | URI[], FileDecorationChangeEvent>(
			this._onDidChangeDecorationsDelayed.event,
350 351
			FileDecorationChangeEvent.debouncer,
			undefined, undefined, 500
352
		)
353 354
	);

355 356
	constructor(
		@IThemeService themeService: IThemeService,
357
		cleanUpCount: number = 17
358
	) {
J
Johannes Rieken 已提交
359
		this._decorationStyles = new DecorationStyles(themeService);
360 361 362 363 364 365 366 367 368 369 370 371 372 373

		// every so many events we check if there are
		// css styles that we don't need anymore
		let count = 0;
		let reg = this.onDidChangeDecorations(() => {
			if (++count % cleanUpCount === 0) {
				this._decorationStyles.cleanUp(this._data.iterator());
			}
		});

		this._disposables = [
			reg,
			this._decorationStyles
		];
374 375 376
	}

	dispose(): void {
377
		dispose(this._disposables);
J
Johannes Rieken 已提交
378 379
		dispose(this._onDidChangeDecorations);
		dispose(this._onDidChangeDecorationsDelayed);
380 381
	}

J
Johannes Rieken 已提交
382
	registerDecorationsProvider(provider: IDecorationsProvider): IDisposable {
383

384 385
		const wrapper = new DecorationProviderWrapper(
			provider,
386 387
			this._onDidChangeDecorationsDelayed,
			this._onDidChangeDecorations
388
		);
389
		const remove = this._data.push(wrapper);
390 391 392 393 394 395

		this._onDidChangeDecorations.fire({
			// everything might have changed
			affectsResource() { return true; }
		});

396 397 398 399 400 401 402
		return toDisposable(() => {
			// fire event that says 'yes' for any resource
			// known to this provider. then dispose and remove it.
			remove();
			this._onDidChangeDecorations.fire({ affectsResource: uri => wrapper.knowsAbout(uri) });
			wrapper.dispose();
		});
403 404
	}

405
	getDecoration(uri: URI, includeChildren: boolean, overwrite?: IDecorationData): IDecoration | undefined {
J
Johannes Rieken 已提交
406
		let data: IDecorationData[] = [];
407
		let containsChildren: boolean = false;
408
		for (let iter = this._data.iterator(), next = iter.next(); !next.done; next = iter.next()) {
J
Johannes Rieken 已提交
409
			next.value.getOrRetrieve(uri, includeChildren, (deco, isChild) => {
410 411
				if (!isChild || deco.bubble) {
					data.push(deco);
412
					containsChildren = isChild || containsChildren;
413
				}
414 415
			});
		}
416

J
Johannes Rieken 已提交
417
		if (data.length === 0) {
418 419
			// nothing, maybe overwrite data
			if (overwrite) {
420
				return this._decorationStyles.asDecoration([overwrite], containsChildren);
421 422 423 424 425
			} else {
				return undefined;
			}
		} else {
			// result, maybe overwrite
426
			let result = this._decorationStyles.asDecoration(data, containsChildren);
427
			if (overwrite) {
428
				return result.update(overwrite);
429 430 431
			} else {
				return result;
			}
432
		}
433 434
	}
}
M
Matt Bierner 已提交
435 436 437 438 439 440 441 442 443 444
function getColor(theme: ITheme, color: string | undefined) {
	if (color) {
		const foundColor = theme.getColor(color);
		if (foundColor) {
			return foundColor;
		}
	}
	return 'inherit';
}