diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index 50b271be179..572acf83917 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js'; -import { FileOperation, IFileService, IFilesConfiguration, IWatchOptions } from '../../../platform/files/common/files.js'; +import { FileOperation, IFileService, IWatchOptions } from '../../../platform/files/common/files.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostContext, ExtHostFileSystemEventServiceShape, MainContext, MainThreadFileSystemEventServiceShape } from '../common/extHost.protocol.js'; import { localize } from '../../../nls.js'; @@ -22,12 +22,7 @@ import { ILogService } from '../../../platform/log/common/log.js'; import { IEnvironmentService } from '../../../platform/environment/common/environment.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { reviveWorkspaceEditDto } from './mainThreadBulkEdits.js'; -import { GLOBSTAR } from '../../../base/common/glob.js'; -import { rtrim } from '../../../base/common/strings.js'; import { UriComponents, URI } from '../../../base/common/uri.js'; -import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; -import { normalizeWatcherPattern } from '../../../platform/files/common/watcher.js'; -import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js'; @extHostNamedCustomer(MainContext.MainThreadFileSystemEventService) export class MainThreadFileSystemEventService implements MainThreadFileSystemEventServiceShape { @@ -50,9 +45,7 @@ export class MainThreadFileSystemEventService implements MainThreadFileSystemEve @ILogService logService: ILogService, @IEnvironmentService envService: IEnvironmentService, @IUriIdentityService uriIdentService: IUriIdentityService, - @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @ILogService private readonly _logService: ILogService, - @IConfigurationService private readonly _configurationService: IConfigurationService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService); @@ -234,9 +227,9 @@ export class MainThreadFileSystemEventService implements MainThreadFileSystemEve } } - // Correlated file watching is taken as is + // Correlated file watching: use an exclusive `createWatcher()` if (correlate) { - this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching correlated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); + 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)})`); const watcherDisposables = new DisposableStore(); const subscription = watcherDisposables.add(this._fileService.createWatcher(uri, opts)); @@ -252,60 +245,9 @@ export class MainThreadFileSystemEventService implements MainThreadFileSystemEve this._watches.set(session, watcherDisposables); } - // Uncorrelated file watching gets special treatment + // Uncorrelated file watching: via shared `watch()` else { - this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching uncorrelated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); - - const workspaceFolder = this._contextService.getWorkspaceFolder(uri); - - // Automatically add `files.watcherExclude` patterns when watching - // recursively to give users a chance to configure exclude rules - // for reducing the overhead of watching recursively - if (opts.recursive && opts.excludes.length === 0) { - const config = this._configurationService.getValue(); - if (config.files?.watcherExclude) { - for (const key in config.files.watcherExclude) { - if (key && config.files.watcherExclude[key] === true) { - opts.excludes.push(key); - } - } - } - } - - // Non-recursive watching inside the workspace will overlap with - // our standard workspace watchers. To prevent duplicate events, - // we only want to include events for files that are otherwise - // excluded via `files.watcherExclude`. As such, we configure - // to include each configured exclude pattern so that only those - // events are reported that are otherwise excluded. - // However, we cannot just use the pattern as is, because a pattern - // such as `bar` for a exclude, will work to exclude any of - // `/bar` but will not work as include for files within - // `bar` unless a suffix of `/**` if added. - // (https://github.com/microsoft/vscode/issues/148245) - else if (!opts.recursive && workspaceFolder) { - const config = this._configurationService.getValue(); - if (config.files?.watcherExclude) { - for (const key in config.files.watcherExclude) { - if (key && config.files.watcherExclude[key] === true) { - if (!opts.includes) { - opts.includes = []; - } - - const includePattern = `${rtrim(key, '/')}/${GLOBSTAR}`; - opts.includes.push(normalizeWatcherPattern(workspaceFolder.uri.fsPath, includePattern)); - } - } - } - - // Still ignore watch request if there are actually no configured - // exclude rules, because in that case our default recursive watcher - // should be able to take care of all events. - if (!opts.includes || opts.includes.length === 0) { - this._logService.trace(`MainThreadFileSystemEventService#$watch(): ignoring request to start watching because path is inside workspace and no excludes are configured (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`); - return; - } - } + 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)})`); const subscription = this._fileService.watch(uri, opts); this._watches.set(session, subscription); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a885606999e..4d118bcb91b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1003,22 +1003,22 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createFileSystemWatcher: (pattern, optionsOrIgnoreCreate, ignoreChange?, ignoreDelete?): vscode.FileSystemWatcher => { let options: FileSystemWatcherCreateOptions | undefined = undefined; - if (typeof optionsOrIgnoreCreate === 'boolean') { + if (optionsOrIgnoreCreate && typeof optionsOrIgnoreCreate !== 'boolean') { + checkProposedApiEnabled(extension, 'createFileSystemWatcher'); + options = { + ...optionsOrIgnoreCreate, + correlate: true + }; + } else { options = { ignoreCreateEvents: Boolean(optionsOrIgnoreCreate), ignoreChangeEvents: Boolean(ignoreChange), ignoreDeleteEvents: Boolean(ignoreDelete), correlate: false }; - } else if (optionsOrIgnoreCreate) { - checkProposedApiEnabled(extension, 'createFileSystemWatcher'); - options = { - ...optionsOrIgnoreCreate, - correlate: true - }; } - return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, extension, pattern, options); + return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, configProvider, extension, pattern, options); }, get textDocuments() { return extHostDocuments.getAllDocumentData().map(data => data.document); diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 6a6cf4e3094..48aea545da0 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event, AsyncEmitter, IWaitUntil, IWaitUntilData } from '../../../base/common/event.js'; -import { GLOBSTAR, GLOB_SPLIT, parse } from '../../../base/common/glob.js'; +import { GLOBSTAR, GLOB_SPLIT, IRelativePattern, parse } from '../../../base/common/glob.js'; import { URI } from '../../../base/common/uri.js'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js'; import type * as vscode from 'vscode'; @@ -12,11 +12,14 @@ import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, Sou import * as typeConverter from './extHostTypeConverters.js'; import { Disposable, WorkspaceEdit } from './extHostTypes.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; -import { FileChangeFilter, FileOperation } from '../../../platform/files/common/files.js'; +import { FileChangeFilter, FileOperation, IGlobPatterns } from '../../../platform/files/common/files.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; import { Lazy } from '../../../base/common/lazy.js'; +import { ExtHostConfigProvider } from './extHostConfiguration.js'; +import { rtrim } from '../../../base/common/strings.js'; +import { normalizeWatcherPattern } from '../../../platform/files/common/watcher.js'; export interface FileSystemWatcherCreateOptions { readonly correlate: boolean; @@ -51,15 +54,15 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { return Boolean(this._config & 0b100); } - constructor(mainContext: IMainContext, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event, globPattern: string | IRelativePatternDto, options?: FileSystemWatcherCreateOptions) { + constructor(mainContext: IMainContext, configuration: ExtHostConfigProvider, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions) { this._config = 0; - if (options?.ignoreCreateEvents) { + if (options.ignoreCreateEvents) { this._config += 0b001; } - if (options?.ignoreChangeEvents) { + if (options.ignoreChangeEvents) { this._config += 0b010; } - if (options?.ignoreDeleteEvents) { + if (options.ignoreDeleteEvents) { this._config += 0b100; } @@ -75,7 +78,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { // 1.84.x introduces new proposed API for a watcher to set exclude // rules. In these cases, we turn the file watcher into correlation // mode and ignore any event that does not match the correlation ID. - const excludeUncorrelatedEvents = options?.correlate; + const excludeUncorrelatedEvents = options.correlate; const subscription = dispatcher(events => { if (typeof events.session === 'number' && events.session !== this.session) { @@ -86,7 +89,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { return; // ignore events from other non-correlating file watcher when we are in correlation mode } - if (!options?.ignoreCreateEvents) { + if (!options.ignoreCreateEvents) { for (const created of events.created) { const uri = URI.revive(created); if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { @@ -94,7 +97,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } } } - if (!options?.ignoreChangeEvents) { + if (!options.ignoreChangeEvents) { for (const changed of events.changed) { const uri = URI.revive(changed); if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { @@ -102,7 +105,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } } } - if (!options?.ignoreDeleteEvents) { + if (!options.ignoreDeleteEvents) { for (const deleted of events.deleted) { const uri = URI.revive(deleted); if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) { @@ -112,17 +115,17 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } }); - this._disposable = Disposable.from(this.ensureWatching(mainContext, extension, globPattern, options, options?.correlate), this._onDidCreate, this._onDidChange, this._onDidDelete, subscription); + this._disposable = Disposable.from(this.ensureWatching(mainContext, workspace, configuration, extension, globPattern, options, options.correlate), this._onDidCreate, this._onDidChange, this._onDidDelete, subscription); } - private ensureWatching(mainContext: IMainContext, extension: IExtensionDescription, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions | undefined, correlate: boolean | undefined): Disposable { + private ensureWatching(mainContext: IMainContext, workspace: IExtHostWorkspace, configuration: ExtHostConfigProvider, extension: IExtensionDescription, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions, correlate: boolean | undefined): Disposable { const disposable = Disposable.from(); if (typeof globPattern === 'string') { return disposable; // workspace is already watched by default, no need to watch again! } - if (options?.ignoreChangeEvents && options?.ignoreCreateEvents && options?.ignoreDeleteEvents) { + if (options.ignoreChangeEvents && options.ignoreCreateEvents && options.ignoreDeleteEvents) { return disposable; // no need to watch if we ignore all events } @@ -133,26 +136,85 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { recursive = true; // only watch recursively if pattern indicates the need for it } + const excludes = options.excludes ?? []; + let includes: Array | undefined = undefined; let filter: FileChangeFilter | undefined; + + // Correlated: adjust filter based on arguments if (correlate) { - if (options?.ignoreChangeEvents || options?.ignoreCreateEvents || options?.ignoreDeleteEvents) { + if (options.ignoreChangeEvents || options.ignoreCreateEvents || options.ignoreDeleteEvents) { filter = FileChangeFilter.UPDATED | FileChangeFilter.ADDED | FileChangeFilter.DELETED; - if (options?.ignoreChangeEvents) { + if (options.ignoreChangeEvents) { filter &= ~FileChangeFilter.UPDATED; } - if (options?.ignoreCreateEvents) { + if (options.ignoreCreateEvents) { filter &= ~FileChangeFilter.ADDED; } - if (options?.ignoreDeleteEvents) { + if (options.ignoreDeleteEvents) { filter &= ~FileChangeFilter.DELETED; } } } - proxy.$watch(extension.identifier.value, this.session, globPattern.baseUri, { recursive, excludes: options?.excludes ?? [], filter }, Boolean(correlate)); + // Uncorrelated: adjust includes and excludes based on settings + else { + + // Automatically add `files.watcherExclude` patterns when watching + // recursively to give users a chance to configure exclude rules + // for reducing the overhead of watching recursively + if (recursive && excludes.length === 0) { + const watcherExcludes = configuration.getConfiguration().get('files.watcherExclude'); + if (watcherExcludes) { + for (const key in watcherExcludes) { + if (key && watcherExcludes[key] === true) { + excludes.push(key); + } + } + } + } + + // Non-recursive watching inside the workspace will overlap with + // our standard workspace watchers. To prevent duplicate events, + // we only want to include events for files that are otherwise + // excluded via `files.watcherExclude`. As such, we configure + // to include each configured exclude pattern so that only those + // events are reported that are otherwise excluded. + // However, we cannot just use the pattern as is, because a pattern + // such as `bar` for a exclude, will work to exclude any of + // `/bar` but will not work as include for files within + // `bar` unless a suffix of `/**` if added. + // (https://github.com/microsoft/vscode/issues/148245) + else if (!recursive) { + const workspaceUri = workspace.getWorkspaceFolder(URI.revive(globPattern.baseUri))?.uri; + if (workspaceUri) { + const watcherExcludes = configuration.getConfiguration().get('files.watcherExclude'); + if (watcherExcludes) { + for (const key in watcherExcludes) { + if (key && watcherExcludes[key] === true) { + const includePattern = `${rtrim(key, '/')}/${GLOBSTAR}`; + if (!includes) { + includes = []; + } + + includes.push(normalizeWatcherPattern(workspaceUri.fsPath, includePattern)); + } + } + } + + // Still ignore watch request if there are actually no configured + // exclude rules, because in that case our default recursive watcher + // should be able to take care of all events. + if (!includes || includes.length === 0) { + return disposable; + } + } + } + } + + proxy.$watch(extension.identifier.value, this.session, globPattern.baseUri, { recursive, excludes, includes, filter }, Boolean(correlate)); return Disposable.from({ dispose: () => proxy.$unwatch(this.session) }); } @@ -220,8 +282,8 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ //--- file events - createFileSystemWatcher(workspace: IExtHostWorkspace, extension: IExtensionDescription, globPattern: vscode.GlobPattern, options?: FileSystemWatcherCreateOptions): vscode.FileSystemWatcher { - return new FileSystemWatcher(this._mainContext, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), options); + createFileSystemWatcher(workspace: IExtHostWorkspace, configProvider: ExtHostConfigProvider, extension: IExtensionDescription, globPattern: vscode.GlobPattern, options: FileSystemWatcherCreateOptions): vscode.FileSystemWatcher { + return new FileSystemWatcher(this._mainContext, configProvider, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), options); } $onFileEvent(events: FileSystemEvents) { diff --git a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts index dff6e2e37c2..6ffae58bc79 100644 --- a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts @@ -22,13 +22,13 @@ suite('ExtHostFileSystemEventService', () => { drain: undefined! }; - const watcher1 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingInteresting', { correlate: false }); + const watcher1 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, undefined!, '**/somethingInteresting', { correlate: false }); assert.strictEqual(watcher1.ignoreChangeEvents, false); assert.strictEqual(watcher1.ignoreCreateEvents, false); assert.strictEqual(watcher1.ignoreDeleteEvents, false); watcher1.dispose(); - const watcher2 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingBoring', { ignoreCreateEvents: true, ignoreChangeEvents: true, ignoreDeleteEvents: true, correlate: false }); + const watcher2 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, undefined!, '**/somethingBoring', { ignoreCreateEvents: true, ignoreChangeEvents: true, ignoreDeleteEvents: true, correlate: false }); assert.strictEqual(watcher2.ignoreChangeEvents, true); assert.strictEqual(watcher2.ignoreCreateEvents, true); assert.strictEqual(watcher2.ignoreDeleteEvents, true);