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