Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostFileSystemEventService.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 { Emitter, Event, AsyncEmitter, IWaitUntil, IWaitUntilData } from '../../../base/common/event.js';
7
import { GLOBSTAR, GLOB_SPLIT, IRelativePattern, parse } from '../../../base/common/glob.js';
8
import { URI } from '../../../base/common/uri.js';
9
import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';
10
import type * as vscode from 'vscode';
11
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation, MainContext, IRelativePatternDto } from './extHost.protocol.js';
12
import * as typeConverter from './extHostTypeConverters.js';
13
import { Disposable, WorkspaceEdit } from './extHostTypes.js';
14
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
15
import { FileChangeFilter, FileOperation, IGlobPatterns } from '../../../platform/files/common/files.js';
16
import { CancellationToken } from '../../../base/common/cancellation.js';
17
import { ILogService } from '../../../platform/log/common/log.js';
18
import { IExtHostWorkspace } from './extHostWorkspace.js';
19
import { Lazy } from '../../../base/common/lazy.js';
20
import { ExtHostConfigProvider } from './extHostConfiguration.js';
21
import { rtrim } from '../../../base/common/strings.js';
22
import { normalizeWatcherPattern } from '../../../platform/files/common/watcher.js';
23
24
export interface FileSystemWatcherCreateOptions {
25
readonly ignoreCreateEvents?: boolean;
26
readonly ignoreChangeEvents?: boolean;
27
readonly ignoreDeleteEvents?: boolean;
28
}
29
30
class FileSystemWatcher implements vscode.FileSystemWatcher {
31
32
private readonly session = Math.random();
33
34
private readonly _onDidCreate = new Emitter<vscode.Uri>();
35
private readonly _onDidChange = new Emitter<vscode.Uri>();
36
private readonly _onDidDelete = new Emitter<vscode.Uri>();
37
38
private _disposable: Disposable;
39
private _config: number;
40
41
get ignoreCreateEvents(): boolean {
42
return Boolean(this._config & 0b001);
43
}
44
45
get ignoreChangeEvents(): boolean {
46
return Boolean(this._config & 0b010);
47
}
48
49
get ignoreDeleteEvents(): boolean {
50
return Boolean(this._config & 0b100);
51
}
52
53
constructor(mainContext: IMainContext, configuration: ExtHostConfigProvider, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions) {
54
this._config = 0;
55
if (options.ignoreCreateEvents) {
56
this._config += 0b001;
57
}
58
if (options.ignoreChangeEvents) {
59
this._config += 0b010;
60
}
61
if (options.ignoreDeleteEvents) {
62
this._config += 0b100;
63
}
64
65
const parsedPattern = parse(globPattern);
66
67
// 1.64.x behaviour change: given the new support to watch any folder
68
// we start to ignore events outside the workspace when only a string
69
// pattern is provided to avoid sending events to extensions that are
70
// unexpected.
71
// https://github.com/microsoft/vscode/issues/3025
72
const excludeOutOfWorkspaceEvents = typeof globPattern === 'string';
73
74
// 1.84.x introduces new proposed API for a watcher to set exclude
75
// rules. In these cases, we turn the file watcher into correlation
76
// mode and ignore any event that does not match the correlation ID.
77
//
78
// Update (Feb 2025): proposal is discontinued, so the previous
79
// `options.correlate` is always `false`.
80
const excludeUncorrelatedEvents = false;
81
82
const subscription = dispatcher(events => {
83
if (typeof events.session === 'number' && events.session !== this.session) {
84
return; // ignore events from other file watchers that are in correlation mode
85
}
86
87
if (excludeUncorrelatedEvents && typeof events.session === 'undefined') {
88
return; // ignore events from other non-correlating file watcher when we are in correlation mode
89
}
90
91
if (!options.ignoreCreateEvents) {
92
for (const created of events.created) {
93
const uri = URI.revive(created);
94
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
95
this._onDidCreate.fire(uri);
96
}
97
}
98
}
99
if (!options.ignoreChangeEvents) {
100
for (const changed of events.changed) {
101
const uri = URI.revive(changed);
102
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
103
this._onDidChange.fire(uri);
104
}
105
}
106
}
107
if (!options.ignoreDeleteEvents) {
108
for (const deleted of events.deleted) {
109
const uri = URI.revive(deleted);
110
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
111
this._onDidDelete.fire(uri);
112
}
113
}
114
}
115
});
116
117
this._disposable = Disposable.from(this.ensureWatching(mainContext, workspace, configuration, extension, globPattern, options, false), this._onDidCreate, this._onDidChange, this._onDidDelete, subscription);
118
}
119
120
private ensureWatching(mainContext: IMainContext, workspace: IExtHostWorkspace, configuration: ExtHostConfigProvider, extension: IExtensionDescription, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions, correlate: boolean | undefined): Disposable {
121
const disposable = Disposable.from();
122
123
if (typeof globPattern === 'string') {
124
return disposable; // workspace is already watched by default, no need to watch again!
125
}
126
127
if (options.ignoreChangeEvents && options.ignoreCreateEvents && options.ignoreDeleteEvents) {
128
return disposable; // no need to watch if we ignore all events
129
}
130
131
const proxy = mainContext.getProxy(MainContext.MainThreadFileSystemEventService);
132
133
let recursive = false;
134
if (globPattern.pattern.includes(GLOBSTAR) || globPattern.pattern.includes(GLOB_SPLIT)) {
135
recursive = true; // only watch recursively if pattern indicates the need for it
136
}
137
138
const excludes = [];
139
let includes: Array<string | IRelativePattern> | undefined = undefined;
140
let filter: FileChangeFilter | undefined;
141
142
// Correlated: adjust filter based on arguments
143
if (correlate) {
144
if (options.ignoreChangeEvents || options.ignoreCreateEvents || options.ignoreDeleteEvents) {
145
filter = FileChangeFilter.UPDATED | FileChangeFilter.ADDED | FileChangeFilter.DELETED;
146
147
if (options.ignoreChangeEvents) {
148
filter &= ~FileChangeFilter.UPDATED;
149
}
150
151
if (options.ignoreCreateEvents) {
152
filter &= ~FileChangeFilter.ADDED;
153
}
154
155
if (options.ignoreDeleteEvents) {
156
filter &= ~FileChangeFilter.DELETED;
157
}
158
}
159
}
160
161
// Uncorrelated: adjust includes and excludes based on settings
162
else {
163
164
// Automatically add `files.watcherExclude` patterns when watching
165
// recursively to give users a chance to configure exclude rules
166
// for reducing the overhead of watching recursively
167
if (recursive && excludes.length === 0) {
168
const workspaceFolder = workspace.getWorkspaceFolder(URI.revive(globPattern.baseUri));
169
const watcherExcludes = configuration.getConfiguration('files', workspaceFolder).get<IGlobPatterns>('watcherExclude');
170
if (watcherExcludes) {
171
for (const key in watcherExcludes) {
172
if (key && watcherExcludes[key] === true) {
173
excludes.push(key);
174
}
175
}
176
}
177
}
178
179
// Non-recursive watching inside the workspace will overlap with
180
// our standard workspace watchers. To prevent duplicate events,
181
// we only want to include events for files that are otherwise
182
// excluded via `files.watcherExclude`. As such, we configure
183
// to include each configured exclude pattern so that only those
184
// events are reported that are otherwise excluded.
185
// However, we cannot just use the pattern as is, because a pattern
186
// such as `bar` for a exclude, will work to exclude any of
187
// `<workspace path>/bar` but will not work as include for files within
188
// `bar` unless a suffix of `/**` if added.
189
// (https://github.com/microsoft/vscode/issues/148245)
190
else if (!recursive) {
191
const workspaceFolder = workspace.getWorkspaceFolder(URI.revive(globPattern.baseUri));
192
if (workspaceFolder) {
193
const watcherExcludes = configuration.getConfiguration('files', workspaceFolder).get<IGlobPatterns>('watcherExclude');
194
if (watcherExcludes) {
195
for (const key in watcherExcludes) {
196
if (key && watcherExcludes[key] === true) {
197
const includePattern = `${rtrim(key, '/')}/${GLOBSTAR}`;
198
if (!includes) {
199
includes = [];
200
}
201
202
includes.push(normalizeWatcherPattern(workspaceFolder.uri.fsPath, includePattern));
203
}
204
}
205
}
206
207
// Still ignore watch request if there are actually no configured
208
// exclude rules, because in that case our default recursive watcher
209
// should be able to take care of all events.
210
if (!includes || includes.length === 0) {
211
return disposable;
212
}
213
}
214
}
215
}
216
217
proxy.$watch(extension.identifier.value, this.session, globPattern.baseUri, { recursive, excludes, includes, filter }, Boolean(correlate));
218
219
return Disposable.from({ dispose: () => proxy.$unwatch(this.session) });
220
}
221
222
dispose() {
223
this._disposable.dispose();
224
}
225
226
get onDidCreate(): Event<vscode.Uri> {
227
return this._onDidCreate.event;
228
}
229
230
get onDidChange(): Event<vscode.Uri> {
231
return this._onDidChange.event;
232
}
233
234
get onDidDelete(): Event<vscode.Uri> {
235
return this._onDidDelete.event;
236
}
237
}
238
239
interface IExtensionListener<E> {
240
extension: IExtensionDescription;
241
(e: E): any;
242
}
243
244
class LazyRevivedFileSystemEvents implements FileSystemEvents {
245
246
readonly session: number | undefined;
247
248
private _created = new Lazy(() => this._events.created.map(URI.revive) as URI[]);
249
get created(): URI[] { return this._created.value; }
250
251
private _changed = new Lazy(() => this._events.changed.map(URI.revive) as URI[]);
252
get changed(): URI[] { return this._changed.value; }
253
254
private _deleted = new Lazy(() => this._events.deleted.map(URI.revive) as URI[]);
255
get deleted(): URI[] { return this._deleted.value; }
256
257
constructor(private readonly _events: FileSystemEvents) {
258
this.session = this._events.session;
259
}
260
}
261
262
export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServiceShape {
263
264
private readonly _onFileSystemEvent = new Emitter<FileSystemEvents>();
265
266
private readonly _onDidRenameFile = new Emitter<vscode.FileRenameEvent>();
267
private readonly _onDidCreateFile = new Emitter<vscode.FileCreateEvent>();
268
private readonly _onDidDeleteFile = new Emitter<vscode.FileDeleteEvent>();
269
private readonly _onWillRenameFile = new AsyncEmitter<vscode.FileWillRenameEvent>();
270
private readonly _onWillCreateFile = new AsyncEmitter<vscode.FileWillCreateEvent>();
271
private readonly _onWillDeleteFile = new AsyncEmitter<vscode.FileWillDeleteEvent>();
272
273
readonly onDidRenameFile: Event<vscode.FileRenameEvent> = this._onDidRenameFile.event;
274
readonly onDidCreateFile: Event<vscode.FileCreateEvent> = this._onDidCreateFile.event;
275
readonly onDidDeleteFile: Event<vscode.FileDeleteEvent> = this._onDidDeleteFile.event;
276
277
constructor(
278
private readonly _mainContext: IMainContext,
279
private readonly _logService: ILogService,
280
private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors
281
) {
282
//
283
}
284
285
//--- file events
286
287
createFileSystemWatcher(workspace: IExtHostWorkspace, configProvider: ExtHostConfigProvider, extension: IExtensionDescription, globPattern: vscode.GlobPattern, options: FileSystemWatcherCreateOptions): vscode.FileSystemWatcher {
288
return new FileSystemWatcher(this._mainContext, configProvider, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), options);
289
}
290
291
$onFileEvent(events: FileSystemEvents) {
292
this._onFileSystemEvent.fire(new LazyRevivedFileSystemEvents(events));
293
}
294
295
//--- file operations
296
297
$onDidRunFileOperation(operation: FileOperation, files: SourceTargetPair[]): void {
298
switch (operation) {
299
case FileOperation.MOVE:
300
this._onDidRenameFile.fire(Object.freeze({ files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }));
301
break;
302
case FileOperation.DELETE:
303
this._onDidDeleteFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) }));
304
break;
305
case FileOperation.CREATE:
306
case FileOperation.COPY:
307
this._onDidCreateFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) }));
308
break;
309
default:
310
//ignore, dont send
311
}
312
}
313
314
315
getOnWillRenameFileEvent(extension: IExtensionDescription): Event<vscode.FileWillRenameEvent> {
316
return this._createWillExecuteEvent(extension, this._onWillRenameFile);
317
}
318
319
getOnWillCreateFileEvent(extension: IExtensionDescription): Event<vscode.FileWillCreateEvent> {
320
return this._createWillExecuteEvent(extension, this._onWillCreateFile);
321
}
322
323
getOnWillDeleteFileEvent(extension: IExtensionDescription): Event<vscode.FileWillDeleteEvent> {
324
return this._createWillExecuteEvent(extension, this._onWillDeleteFile);
325
}
326
327
private _createWillExecuteEvent<E extends IWaitUntil>(extension: IExtensionDescription, emitter: AsyncEmitter<E>): Event<E> {
328
return (listener, thisArg, disposables) => {
329
const wrappedListener: IExtensionListener<E> = function wrapped(e: E) { listener.call(thisArg, e); };
330
wrappedListener.extension = extension;
331
return emitter.event(wrappedListener, undefined, disposables);
332
};
333
}
334
335
async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise<IWillRunFileOperationParticipation | undefined> {
336
switch (operation) {
337
case FileOperation.MOVE:
338
return await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, timeout, token);
339
case FileOperation.DELETE:
340
return await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);
341
case FileOperation.CREATE:
342
case FileOperation.COPY:
343
return await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);
344
}
345
return undefined;
346
}
347
348
private async _fireWillEvent<E extends IWaitUntil>(emitter: AsyncEmitter<E>, data: IWaitUntilData<E>, timeout: number, token: CancellationToken): Promise<IWillRunFileOperationParticipation | undefined> {
349
350
const extensionNames = new Set<string>();
351
const edits: [IExtensionDescription, WorkspaceEdit][] = [];
352
353
await emitter.fireAsync(data, token, async (thenable: Promise<unknown>, listener) => {
354
// ignore all results except for WorkspaceEdits. Those are stored in an array.
355
const now = Date.now();
356
const result = await Promise.resolve(thenable);
357
if (result instanceof WorkspaceEdit) {
358
edits.push([(<IExtensionListener<E>>listener).extension, result]);
359
extensionNames.add((<IExtensionListener<E>>listener).extension.displayName ?? (<IExtensionListener<E>>listener).extension.identifier.value);
360
}
361
362
if (Date.now() - now > timeout) {
363
this._logService.warn('SLOW file-participant', (<IExtensionListener<E>>listener).extension.identifier);
364
}
365
});
366
367
if (token.isCancellationRequested) {
368
return undefined;
369
}
370
371
if (edits.length === 0) {
372
return undefined;
373
}
374
375
// concat all WorkspaceEdits collected via waitUntil-call and send them over to the renderer
376
const dto: IWorkspaceEditDto = { edits: [] };
377
for (const [, edit] of edits) {
378
const { edits } = typeConverter.WorkspaceEdit.from(edit, {
379
getTextDocumentVersion: uri => this._extHostDocumentsAndEditors.getDocument(uri)?.version,
380
getNotebookDocumentVersion: () => undefined,
381
});
382
dto.edits = dto.edits.concat(edits);
383
}
384
return { edit: dto, extensionNames: Array.from(extensionNames) };
385
}
386
}
387
388