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
6import { window, workspace, Uri, Disposable, Event, EventEmitter, Decoration, DecorationProvider, ThemeColor } from 'vscode';
7import * as path from 'path';
8import { Repository, GitResourceGroup } from './repository';
9import { Model } from './model';
10import { debounce } from './decorators';
11import { filterEvent, dispose, anyEvent, fireEvent } from './util';
12import { GitErrorCodes, Status } from './api/git';
13
14type Callback = { resolve: (status: boolean) => void, reject: (err: any) => void };
15
16class GitIgnoreDecorationProvider implements DecorationProvider {
17
18	readonly onDidChangeDecorations: Event<Uri[]>;
19	private queue = new Map<string, { repository: Repository; queue: Map<string, Callback>; }>();
20	private disposables: Disposable[] = [];
21
22	constructor(private model: Model) {
23		this.onDidChangeDecorations = fireEvent(anyEvent<any>(
24			filterEvent(workspace.onDidSaveTextDocument, e => e.fileName.endsWith('.gitignore')),
25			model.onDidOpenRepository,
26			model.onDidCloseRepository
27		));
28
29		this.disposables.push(window.registerDecorationProvider(this));
30	}
31
32	provideDecoration(uri: Uri): Promise<Decoration | undefined> {
33		const repository = this.model.getRepository(uri);
34
35		if (!repository) {
36			return Promise.resolve(undefined);
37		}
38
39		let queueItem = this.queue.get(repository.root);
40
41		if (!queueItem) {
42			queueItem = { repository, queue: new Map<string, Callback>() };
43			this.queue.set(repository.root, queueItem);
44		}
45
46		return new Promise<boolean>((resolve, reject) => {
47			queueItem!.queue.set(uri.fsPath, { resolve, reject });
48			this.checkIgnoreSoon();
49		}).then(ignored => {
50			if (ignored) {
51				return <Decoration>{
52					priority: 3,
53					color: new ThemeColor('gitDecoration.ignoredResourceForeground')
54				};
55			}
56			return undefined;
57		});
58	}
59
60	@debounce(500)
61	private checkIgnoreSoon(): void {
62		const queue = new Map(this.queue.entries());
63		this.queue.clear();
64
65		for (const [, item] of queue) {
66			const paths = [...item.queue.keys()];
67
68			item.repository.checkIgnore(paths).then(ignoreSet => {
69				for (const [key, value] of item.queue.entries()) {
70					value.resolve(ignoreSet.has(key));
71				}
72			}, err => {
73				if (err.gitErrorCode !== GitErrorCodes.IsInSubmodule) {
74					console.error(err);
75				}
76
77				for (const [, value] of item.queue.entries()) {
78					value.reject(err);
79				}
80			});
81		}
82	}
83
84	dispose(): void {
85		this.disposables.forEach(d => d.dispose());
86		this.queue.clear();
87	}
88}
89
90class GitDecorationProvider implements DecorationProvider {
91
92	private static SubmoduleDecorationData: Decoration = {
93		title: 'Submodule',
94		letter: 'S',
95		color: new ThemeColor('gitDecoration.submoduleResourceForeground')
96	};
97
98	private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
99	readonly onDidChangeDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
100
101	private disposables: Disposable[] = [];
102	private decorations = new Map<string, Decoration>();
103
104	constructor(private repository: Repository) {
105		this.disposables.push(
106			window.registerDecorationProvider(this),
107			repository.onDidRunGitStatus(this.onDidRunGitStatus, this)
108		);
109	}
110
111	private onDidRunGitStatus(): void {
112		let newDecorations = new Map<string, Decoration>();
113
114		this.collectSubmoduleDecorationData(newDecorations);
115		this.collectDecorationData(this.repository.indexGroup, newDecorations);
116		this.collectDecorationData(this.repository.untrackedGroup, newDecorations);
117		this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);
118		this.collectDecorationData(this.repository.mergeGroup, newDecorations);
119
120		const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));
121		this.decorations = newDecorations;
122		this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
123	}
124
125	private collectDecorationData(group: GitResourceGroup, bucket: Map<string, Decoration>): void {
126		for (const r of group.resourceStates) {
127			const decoration = r.resourceDecoration;
128
129			if (decoration) {
130				// not deleted and has a decoration
131				bucket.set(r.original.toString(), decoration);
132
133				if (r.type === Status.INDEX_RENAMED) {
134					bucket.set(r.resourceUri.toString(), decoration);
135				}
136			}
137		}
138	}
139
140	private collectSubmoduleDecorationData(bucket: Map<string, Decoration>): void {
141		for (const submodule of this.repository.submodules) {
142			bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData);
143		}
144	}
145
146	provideDecoration(uri: Uri): Decoration | undefined {
147		return this.decorations.get(uri.toString());
148	}
149
150	dispose(): void {
151		this.disposables.forEach(d => d.dispose());
152	}
153}
154
155
156export class GitDecorations {
157
158	private disposables: Disposable[] = [];
159	private modelDisposables: Disposable[] = [];
160	private providers = new Map<Repository, Disposable>();
161
162	constructor(private model: Model) {
163		this.disposables.push(new GitIgnoreDecorationProvider(model));
164
165		const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.decorations.enabled'));
166		onEnablementChange(this.update, this, this.disposables);
167		this.update();
168	}
169
170	private update(): void {
171		const enabled = workspace.getConfiguration('git').get('decorations.enabled');
172
173		if (enabled) {
174			this.enable();
175		} else {
176			this.disable();
177		}
178	}
179
180	private enable(): void {
181		this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelDisposables);
182		this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelDisposables);
183		this.model.repositories.forEach(this.onDidOpenRepository, this);
184	}
185
186	private disable(): void {
187		this.modelDisposables = dispose(this.modelDisposables);
188		this.providers.forEach(value => value.dispose());
189		this.providers.clear();
190	}
191
192	private onDidOpenRepository(repository: Repository): void {
193		const provider = new GitDecorationProvider(repository);
194		this.providers.set(repository, provider);
195	}
196
197	private onDidCloseRepository(repository: Repository): void {
198		const provider = this.providers.get(repository);
199
200		if (provider) {
201			provider.dispose();
202			this.providers.delete(repository);
203		}
204	}
205
206	dispose(): void {
207		this.disable();
208		this.disposables = dispose(this.disposables);
209	}
210}
211