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