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