Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts
3296 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 { DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js';
7
import { FileOperation, IFileService, IWatchOptions } from '../../../platform/files/common/files.js';
8
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
9
import { ExtHostContext, ExtHostFileSystemEventServiceShape, MainContext, MainThreadFileSystemEventServiceShape } from '../common/extHost.protocol.js';
10
import { localize } from '../../../nls.js';
11
import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from '../../services/workingCopy/common/workingCopyFileService.js';
12
import { IBulkEditService } from '../../../editor/browser/services/bulkEditService.js';
13
import { IProgressService, ProgressLocation } from '../../../platform/progress/common/progress.js';
14
import { raceCancellation } from '../../../base/common/async.js';
15
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
16
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
17
import Severity from '../../../base/common/severity.js';
18
import { IStorageService, StorageScope, StorageTarget } from '../../../platform/storage/common/storage.js';
19
import { Action2, registerAction2 } from '../../../platform/actions/common/actions.js';
20
import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';
21
import { ILogService } from '../../../platform/log/common/log.js';
22
import { IEnvironmentService } from '../../../platform/environment/common/environment.js';
23
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
24
import { reviveWorkspaceEditDto } from './mainThreadBulkEdits.js';
25
import { UriComponents, URI } from '../../../base/common/uri.js';
26
27
@extHostNamedCustomer(MainContext.MainThreadFileSystemEventService)
28
export class MainThreadFileSystemEventService implements MainThreadFileSystemEventServiceShape {
29
30
static readonly MementoKeyAdditionalEdits = `file.particpants.additionalEdits`;
31
32
private readonly _proxy: ExtHostFileSystemEventServiceShape;
33
34
private readonly _listener = new DisposableStore();
35
private readonly _watches = new DisposableMap<number>();
36
37
constructor(
38
extHostContext: IExtHostContext,
39
@IFileService private readonly _fileService: IFileService,
40
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
41
@IBulkEditService bulkEditService: IBulkEditService,
42
@IProgressService progressService: IProgressService,
43
@IDialogService dialogService: IDialogService,
44
@IStorageService storageService: IStorageService,
45
@ILogService logService: ILogService,
46
@IEnvironmentService envService: IEnvironmentService,
47
@IUriIdentityService uriIdentService: IUriIdentityService,
48
@ILogService private readonly _logService: ILogService,
49
) {
50
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService);
51
52
this._listener.add(_fileService.onDidFilesChange(event => {
53
this._proxy.$onFileEvent({
54
created: event.rawAdded,
55
changed: event.rawUpdated,
56
deleted: event.rawDeleted
57
});
58
}));
59
60
const that = this;
61
const fileOperationParticipant = new class implements IWorkingCopyFileOperationParticipant {
62
async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, timeout: number, token: CancellationToken) {
63
if (undoInfo?.isUndoing) {
64
return;
65
}
66
67
const cts = new CancellationTokenSource(token);
68
const timer = setTimeout(() => cts.cancel(), timeout);
69
70
const data = await progressService.withProgress({
71
location: ProgressLocation.Notification,
72
title: this._progressLabel(operation),
73
cancellable: true,
74
delay: Math.min(timeout / 2, 3000)
75
}, () => {
76
// race extension host event delivery against timeout AND user-cancel
77
const onWillEvent = that._proxy.$onWillRunFileOperation(operation, files, timeout, cts.token);
78
return raceCancellation(onWillEvent, cts.token);
79
}, () => {
80
// user-cancel
81
cts.cancel();
82
83
}).finally(() => {
84
cts.dispose();
85
clearTimeout(timer);
86
});
87
88
if (!data || data.edit.edits.length === 0) {
89
// cancelled, no reply, or no edits
90
return;
91
}
92
93
const needsConfirmation = data.edit.edits.some(edit => edit.metadata?.needsConfirmation);
94
let showPreview = storageService.getBoolean(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.PROFILE);
95
96
if (envService.extensionTestsLocationURI) {
97
// don't show dialog in tests
98
showPreview = false;
99
}
100
101
if (showPreview === undefined) {
102
// show a user facing message
103
104
let message: string;
105
if (data.extensionNames.length === 1) {
106
if (operation === FileOperation.CREATE) {
107
message = localize('ask.1.create', "Extension '{0}' wants to make refactoring changes with this file creation", data.extensionNames[0]);
108
} else if (operation === FileOperation.COPY) {
109
message = localize('ask.1.copy', "Extension '{0}' wants to make refactoring changes with this file copy", data.extensionNames[0]);
110
} else if (operation === FileOperation.MOVE) {
111
message = localize('ask.1.move', "Extension '{0}' wants to make refactoring changes with this file move", data.extensionNames[0]);
112
} else /* if (operation === FileOperation.DELETE) */ {
113
message = localize('ask.1.delete', "Extension '{0}' wants to make refactoring changes with this file deletion", data.extensionNames[0]);
114
}
115
} else {
116
if (operation === FileOperation.CREATE) {
117
message = localize({ key: 'ask.N.create', comment: ['{0} is a number, e.g "3 extensions want..."'] }, "{0} extensions want to make refactoring changes with this file creation", data.extensionNames.length);
118
} else if (operation === FileOperation.COPY) {
119
message = localize({ key: 'ask.N.copy', comment: ['{0} is a number, e.g "3 extensions want..."'] }, "{0} extensions want to make refactoring changes with this file copy", data.extensionNames.length);
120
} else if (operation === FileOperation.MOVE) {
121
message = localize({ key: 'ask.N.move', comment: ['{0} is a number, e.g "3 extensions want..."'] }, "{0} extensions want to make refactoring changes with this file move", data.extensionNames.length);
122
} else /* if (operation === FileOperation.DELETE) */ {
123
message = localize({ key: 'ask.N.delete', comment: ['{0} is a number, e.g "3 extensions want..."'] }, "{0} extensions want to make refactoring changes with this file deletion", data.extensionNames.length);
124
}
125
}
126
127
if (needsConfirmation) {
128
// edit which needs confirmation -> always show dialog
129
const { confirmed } = await dialogService.confirm({
130
type: Severity.Info,
131
message,
132
primaryButton: localize('preview', "Show &&Preview"),
133
cancelButton: localize('cancel', "Skip Changes")
134
});
135
showPreview = true;
136
if (!confirmed) {
137
// no changes wanted
138
return;
139
}
140
} else {
141
// choice
142
enum Choice {
143
OK = 0,
144
Preview = 1,
145
Cancel = 2
146
}
147
const { result, checkboxChecked } = await dialogService.prompt<Choice>({
148
type: Severity.Info,
149
message,
150
buttons: [
151
{
152
label: localize({ key: 'ok', comment: ['&& denotes a mnemonic'] }, "&&OK"),
153
run: () => Choice.OK
154
},
155
{
156
label: localize({ key: 'preview', comment: ['&& denotes a mnemonic'] }, "Show &&Preview"),
157
run: () => Choice.Preview
158
}
159
],
160
cancelButton: {
161
label: localize('cancel', "Skip Changes"),
162
run: () => Choice.Cancel
163
},
164
checkbox: { label: localize('again', "Do not ask me again") }
165
});
166
if (result === Choice.Cancel) {
167
// no changes wanted, don't persist cancel option
168
return;
169
}
170
showPreview = result === Choice.Preview;
171
if (checkboxChecked) {
172
storageService.store(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, showPreview, StorageScope.PROFILE, StorageTarget.USER);
173
}
174
}
175
}
176
177
logService.info('[onWill-handler] applying additional workspace edit from extensions', data.extensionNames);
178
179
await bulkEditService.apply(
180
reviveWorkspaceEditDto(data.edit, uriIdentService),
181
{ undoRedoGroupId: undoInfo?.undoRedoGroupId, showPreview }
182
);
183
}
184
185
private _progressLabel(operation: FileOperation): string {
186
switch (operation) {
187
case FileOperation.CREATE:
188
return localize('msg-create', "Running 'File Create' participants...");
189
case FileOperation.MOVE:
190
return localize('msg-rename', "Running 'File Rename' participants...");
191
case FileOperation.COPY:
192
return localize('msg-copy', "Running 'File Copy' participants...");
193
case FileOperation.DELETE:
194
return localize('msg-delete', "Running 'File Delete' participants...");
195
case FileOperation.WRITE:
196
return localize('msg-write', "Running 'File Write' participants...");
197
}
198
}
199
};
200
201
// BEFORE file operation
202
this._listener.add(workingCopyFileService.addFileOperationParticipant(fileOperationParticipant));
203
204
// AFTER file operation
205
this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => this._proxy.$onDidRunFileOperation(e.operation, e.files)));
206
}
207
208
async $watch(extensionId: string, session: number, resource: UriComponents, unvalidatedOpts: IWatchOptions, correlate: boolean): Promise<void> {
209
const uri = URI.revive(resource);
210
211
const opts: IWatchOptions = {
212
...unvalidatedOpts
213
};
214
215
// Convert a recursive watcher to a flat watcher if the path
216
// turns out to not be a folder. Recursive watching is only
217
// possible on folders, so we help all file watchers by checking
218
// early.
219
if (opts.recursive) {
220
try {
221
const stat = await this._fileService.stat(uri);
222
if (!stat.isDirectory) {
223
opts.recursive = false;
224
}
225
} catch (error) {
226
// ignore
227
}
228
}
229
230
// Correlated file watching: use an exclusive `createWatcher()`
231
// Note: currently not enabled for extensions (but leaving in in case of future usage)
232
if (correlate && !opts.recursive) {
233
this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching correlated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session}, excludes: ${JSON.stringify(opts.excludes)}, includes: ${JSON.stringify(opts.includes)})`);
234
235
const watcherDisposables = new DisposableStore();
236
const subscription = watcherDisposables.add(this._fileService.createWatcher(uri, { ...opts, recursive: false }));
237
watcherDisposables.add(subscription.onDidChange(event => {
238
this._proxy.$onFileEvent({
239
session,
240
created: event.rawAdded,
241
changed: event.rawUpdated,
242
deleted: event.rawDeleted
243
});
244
}));
245
246
this._watches.set(session, watcherDisposables);
247
}
248
249
// Uncorrelated file watching: via shared `watch()`
250
else {
251
this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching uncorrelated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session}, excludes: ${JSON.stringify(opts.excludes)}, includes: ${JSON.stringify(opts.includes)})`);
252
253
const subscription = this._fileService.watch(uri, opts);
254
this._watches.set(session, subscription);
255
}
256
}
257
258
$unwatch(session: number): void {
259
if (this._watches.has(session)) {
260
this._logService.trace(`MainThreadFileSystemEventService#$unwatch(): request to stop watching (session: ${session})`);
261
this._watches.deleteAndDispose(session);
262
}
263
}
264
265
dispose(): void {
266
this._listener.dispose();
267
this._watches.dispose();
268
}
269
}
270
271
registerAction2(class ResetMemento extends Action2 {
272
constructor() {
273
super({
274
id: 'files.participants.resetChoice',
275
title: {
276
value: localize('label', "Reset choice for 'File operation needs preview'"),
277
original: `Reset choice for 'File operation needs preview'`
278
},
279
f1: true
280
});
281
}
282
run(accessor: ServicesAccessor) {
283
accessor.get(IStorageService).remove(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.PROFILE);
284
}
285
});
286
287