Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/decorationProvider.ts
5240 views
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
6
import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n, SourceControlHistoryItemRef } from 'vscode';
7
import * as path from 'path';
8
import { Repository, GitResourceGroup } from './repository';
9
import { Model } from './model';
10
import { debounce } from './decorators';
11
import { filterEvent, dispose, anyEvent, PromiseSource, combinedDisposable, runAndSubscribeEvent } from './util';
12
import { Change, GitErrorCodes, Status } from './api/git';
13
14
function equalSourceControlHistoryItemRefs(ref1?: SourceControlHistoryItemRef, ref2?: SourceControlHistoryItemRef): boolean {
15
if (ref1 === ref2) {
16
return true;
17
}
18
19
return ref1?.id === ref2?.id &&
20
ref1?.name === ref2?.name &&
21
ref1?.revision === ref2?.revision;
22
}
23
24
class GitIgnoreDecorationProvider implements FileDecorationProvider {
25
26
private static Decoration: FileDecoration = { color: new ThemeColor('gitDecoration.ignoredResourceForeground') };
27
28
private readonly _onDidChangeDecorations = new EventEmitter<undefined | Uri | Uri[]>();
29
readonly onDidChangeFileDecorations: Event<undefined | Uri | Uri[]> = this._onDidChangeDecorations.event;
30
31
private queue = new Map<string, { repository: Repository; queue: Map<string, PromiseSource<FileDecoration | undefined>> }>();
32
private disposables: Disposable[] = [];
33
34
constructor(private model: Model) {
35
const onDidChangeRepository = anyEvent<unknown>(
36
filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)),
37
model.onDidOpenRepository,
38
model.onDidCloseRepository
39
);
40
this.disposables.push(onDidChangeRepository(() => this._onDidChangeDecorations.fire(undefined)));
41
this.disposables.push(window.registerFileDecorationProvider(this));
42
}
43
44
async provideFileDecoration(uri: Uri): Promise<FileDecoration | undefined> {
45
const repository = this.model.getRepository(uri);
46
47
if (!repository) {
48
return;
49
}
50
51
let queueItem = this.queue.get(repository.root);
52
53
if (!queueItem) {
54
queueItem = { repository, queue: new Map<string, PromiseSource<FileDecoration | undefined>>() };
55
this.queue.set(repository.root, queueItem);
56
}
57
58
let promiseSource = queueItem.queue.get(uri.fsPath);
59
60
if (!promiseSource) {
61
promiseSource = new PromiseSource();
62
queueItem!.queue.set(uri.fsPath, promiseSource);
63
this.checkIgnoreSoon();
64
}
65
66
return await promiseSource.promise;
67
}
68
69
@debounce(500)
70
private checkIgnoreSoon(): void {
71
const queue = new Map(this.queue.entries());
72
this.queue.clear();
73
74
for (const [, item] of queue) {
75
const paths = [...item.queue.keys()];
76
77
item.repository.checkIgnore(paths).then(ignoreSet => {
78
for (const [path, promiseSource] of item.queue.entries()) {
79
promiseSource.resolve(ignoreSet.has(path) ? GitIgnoreDecorationProvider.Decoration : undefined);
80
}
81
}, err => {
82
if (err.gitErrorCode !== GitErrorCodes.IsInSubmodule) {
83
console.error(err);
84
}
85
86
for (const [, promiseSource] of item.queue.entries()) {
87
promiseSource.reject(err);
88
}
89
});
90
}
91
}
92
93
dispose(): void {
94
this.disposables.forEach(d => d.dispose());
95
this.queue.clear();
96
}
97
}
98
99
class GitDecorationProvider implements FileDecorationProvider {
100
101
private static SubmoduleDecorationData: FileDecoration = {
102
tooltip: 'Submodule',
103
badge: 'S',
104
color: new ThemeColor('gitDecoration.submoduleResourceForeground')
105
};
106
107
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
108
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
109
110
private disposables: Disposable[] = [];
111
private decorations = new Map<string, FileDecoration>();
112
113
constructor(private repository: Repository) {
114
this.disposables.push(
115
window.registerFileDecorationProvider(this),
116
runAndSubscribeEvent(repository.onDidRunGitStatus, () => this.onDidRunGitStatus())
117
);
118
}
119
120
private onDidRunGitStatus(): void {
121
const newDecorations = new Map<string, FileDecoration>();
122
123
this.collectDecorationData(this.repository.indexGroup, newDecorations);
124
this.collectDecorationData(this.repository.untrackedGroup, newDecorations);
125
this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);
126
this.collectDecorationData(this.repository.mergeGroup, newDecorations);
127
this.collectSubmoduleDecorationData(newDecorations);
128
129
const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));
130
this.decorations = newDecorations;
131
this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
132
}
133
134
private collectDecorationData(group: GitResourceGroup, bucket: Map<string, FileDecoration>): void {
135
for (const r of group.resourceStates) {
136
const decoration = r.resourceDecoration;
137
138
if (decoration) {
139
// not deleted and has a decoration
140
bucket.set(r.original.toString(), decoration);
141
142
if (r.type === Status.DELETED && r.rightUri) {
143
bucket.set(r.rightUri.toString(), decoration);
144
}
145
146
if (r.type === Status.INDEX_RENAMED || r.type === Status.INTENT_TO_RENAME) {
147
bucket.set(r.resourceUri.toString(), decoration);
148
}
149
}
150
}
151
}
152
153
private collectSubmoduleDecorationData(bucket: Map<string, FileDecoration>): void {
154
for (const submodule of this.repository.submodules) {
155
bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData);
156
}
157
}
158
159
provideFileDecoration(uri: Uri): FileDecoration | undefined {
160
return this.decorations.get(uri.toString());
161
}
162
163
dispose(): void {
164
this.disposables.forEach(d => d.dispose());
165
}
166
}
167
168
class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider {
169
170
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
171
readonly onDidChangeFileDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
172
173
private _currentHistoryItemRef: SourceControlHistoryItemRef | undefined;
174
private _currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined;
175
176
private _decorations = new Map<string, FileDecoration>();
177
private readonly disposables: Disposable[] = [];
178
179
constructor(private readonly repository: Repository) {
180
this.disposables.push(
181
window.registerFileDecorationProvider(this),
182
runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemRefs, () => this.onDidChangeCurrentHistoryItemRefs())
183
);
184
}
185
186
private async onDidChangeCurrentHistoryItemRefs(): Promise<void> {
187
const historyProvider = this.repository.historyProvider;
188
const currentHistoryItemRef = historyProvider.currentHistoryItemRef;
189
const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef;
190
191
if (equalSourceControlHistoryItemRefs(this._currentHistoryItemRef, currentHistoryItemRef) &&
192
equalSourceControlHistoryItemRefs(this._currentHistoryItemRemoteRef, currentHistoryItemRemoteRef)) {
193
return;
194
}
195
196
const decorations = new Map<string, FileDecoration>();
197
await this.collectIncomingChangesFileDecorations(decorations);
198
const uris = new Set([...this._decorations.keys()].concat([...decorations.keys()]));
199
200
this._decorations = decorations;
201
this._currentHistoryItemRef = currentHistoryItemRef;
202
this._currentHistoryItemRemoteRef = currentHistoryItemRemoteRef;
203
204
this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
205
}
206
207
private async collectIncomingChangesFileDecorations(bucket: Map<string, FileDecoration>): Promise<void> {
208
for (const change of await this.getIncomingChanges()) {
209
switch (change.status) {
210
case Status.INDEX_ADDED:
211
bucket.set(change.uri.toString(), {
212
badge: '↓A',
213
tooltip: l10n.t('Incoming Changes (added)'),
214
});
215
break;
216
case Status.DELETED:
217
bucket.set(change.uri.toString(), {
218
badge: '↓D',
219
tooltip: l10n.t('Incoming Changes (deleted)'),
220
});
221
break;
222
case Status.INDEX_RENAMED:
223
bucket.set(change.originalUri.toString(), {
224
badge: '↓R',
225
tooltip: l10n.t('Incoming Changes (renamed)'),
226
});
227
break;
228
case Status.MODIFIED:
229
bucket.set(change.uri.toString(), {
230
badge: '↓M',
231
tooltip: l10n.t('Incoming Changes (modified)'),
232
});
233
break;
234
default: {
235
bucket.set(change.uri.toString(), {
236
badge: '↓~',
237
tooltip: l10n.t('Incoming Changes'),
238
});
239
break;
240
}
241
}
242
}
243
}
244
245
private async getIncomingChanges(): Promise<Change[]> {
246
try {
247
const historyProvider = this.repository.historyProvider;
248
const currentHistoryItemRef = historyProvider.currentHistoryItemRef;
249
const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef;
250
251
if (!currentHistoryItemRef || !currentHistoryItemRemoteRef) {
252
return [];
253
}
254
255
const ancestor = await historyProvider.resolveHistoryItemRefsCommonAncestor([currentHistoryItemRef.id, currentHistoryItemRemoteRef.id]);
256
if (!ancestor) {
257
return [];
258
}
259
260
const changes = await this.repository.diffBetweenWithStats(ancestor, currentHistoryItemRemoteRef.id);
261
return changes;
262
} catch (err) {
263
return [];
264
}
265
}
266
267
provideFileDecoration(uri: Uri): FileDecoration | undefined {
268
return this._decorations.get(uri.toString());
269
}
270
271
dispose(): void {
272
dispose(this.disposables);
273
}
274
}
275
276
export class GitDecorations {
277
278
private enabled = false;
279
private disposables: Disposable[] = [];
280
private modelDisposables: Disposable[] = [];
281
private providers = new Map<Repository, Disposable>();
282
283
constructor(private model: Model) {
284
this.disposables.push(new GitIgnoreDecorationProvider(model));
285
286
const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.decorations.enabled'));
287
onEnablementChange(this.update, this, this.disposables);
288
this.update();
289
}
290
291
private update(): void {
292
const config = workspace.getConfiguration('git');
293
const enabled = config.get<boolean>('decorations.enabled') === true;
294
if (this.enabled === enabled) {
295
return;
296
}
297
298
if (enabled) {
299
this.enable();
300
} else {
301
this.disable();
302
}
303
304
this.enabled = enabled;
305
}
306
307
private enable(): void {
308
this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelDisposables);
309
this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelDisposables);
310
this.model.repositories.forEach(this.onDidOpenRepository, this);
311
}
312
313
private disable(): void {
314
this.modelDisposables = dispose(this.modelDisposables);
315
this.providers.forEach(value => value.dispose());
316
this.providers.clear();
317
}
318
319
private onDidOpenRepository(repository: Repository): void {
320
const providers = combinedDisposable([
321
new GitDecorationProvider(repository),
322
new GitIncomingChangesFileDecorationProvider(repository)
323
]);
324
325
this.providers.set(repository, providers);
326
}
327
328
private onDidCloseRepository(repository: Repository): void {
329
const provider = this.providers.get(repository);
330
331
if (provider) {
332
provider.dispose();
333
this.providers.delete(repository);
334
}
335
}
336
337
dispose(): void {
338
this.disable();
339
this.disposables = dispose(this.disposables);
340
}
341
}
342
343