Path: blob/main/src/vs/workbench/api/common/extHostFileSystemEventService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event, AsyncEmitter, IWaitUntil, IWaitUntilData } from '../../../base/common/event.js';6import { GLOBSTAR, GLOB_SPLIT, IRelativePattern, parse } from '../../../base/common/glob.js';7import { URI } from '../../../base/common/uri.js';8import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';9import type * as vscode from 'vscode';10import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation, MainContext, IRelativePatternDto } from './extHost.protocol.js';11import * as typeConverter from './extHostTypeConverters.js';12import { Disposable, WorkspaceEdit } from './extHostTypes.js';13import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';14import { FileChangeFilter, FileOperation, IGlobPatterns } from '../../../platform/files/common/files.js';15import { CancellationToken } from '../../../base/common/cancellation.js';16import { ILogService } from '../../../platform/log/common/log.js';17import { IExtHostWorkspace } from './extHostWorkspace.js';18import { Lazy } from '../../../base/common/lazy.js';19import { ExtHostConfigProvider } from './extHostConfiguration.js';20import { rtrim } from '../../../base/common/strings.js';21import { normalizeWatcherPattern } from '../../../platform/files/common/watcher.js';2223export interface FileSystemWatcherCreateOptions {24readonly ignoreCreateEvents?: boolean;25readonly ignoreChangeEvents?: boolean;26readonly ignoreDeleteEvents?: boolean;27}2829class FileSystemWatcher implements vscode.FileSystemWatcher {3031private readonly session = Math.random();3233private readonly _onDidCreate = new Emitter<vscode.Uri>();34private readonly _onDidChange = new Emitter<vscode.Uri>();35private readonly _onDidDelete = new Emitter<vscode.Uri>();3637private _disposable: Disposable;38private _config: number;3940get ignoreCreateEvents(): boolean {41return Boolean(this._config & 0b001);42}4344get ignoreChangeEvents(): boolean {45return Boolean(this._config & 0b010);46}4748get ignoreDeleteEvents(): boolean {49return Boolean(this._config & 0b100);50}5152constructor(mainContext: IMainContext, configuration: ExtHostConfigProvider, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions) {53this._config = 0;54if (options.ignoreCreateEvents) {55this._config += 0b001;56}57if (options.ignoreChangeEvents) {58this._config += 0b010;59}60if (options.ignoreDeleteEvents) {61this._config += 0b100;62}6364const parsedPattern = parse(globPattern);6566// 1.64.x behaviour change: given the new support to watch any folder67// we start to ignore events outside the workspace when only a string68// pattern is provided to avoid sending events to extensions that are69// unexpected.70// https://github.com/microsoft/vscode/issues/302571const excludeOutOfWorkspaceEvents = typeof globPattern === 'string';7273// 1.84.x introduces new proposed API for a watcher to set exclude74// rules. In these cases, we turn the file watcher into correlation75// mode and ignore any event that does not match the correlation ID.76//77// Update (Feb 2025): proposal is discontinued, so the previous78// `options.correlate` is always `false`.79const excludeUncorrelatedEvents = false;8081const subscription = dispatcher(events => {82if (typeof events.session === 'number' && events.session !== this.session) {83return; // ignore events from other file watchers that are in correlation mode84}8586if (excludeUncorrelatedEvents && typeof events.session === 'undefined') {87return; // ignore events from other non-correlating file watcher when we are in correlation mode88}8990if (!options.ignoreCreateEvents) {91for (const created of events.created) {92const uri = URI.revive(created);93if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {94this._onDidCreate.fire(uri);95}96}97}98if (!options.ignoreChangeEvents) {99for (const changed of events.changed) {100const uri = URI.revive(changed);101if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {102this._onDidChange.fire(uri);103}104}105}106if (!options.ignoreDeleteEvents) {107for (const deleted of events.deleted) {108const uri = URI.revive(deleted);109if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {110this._onDidDelete.fire(uri);111}112}113}114});115116this._disposable = Disposable.from(this.ensureWatching(mainContext, workspace, configuration, extension, globPattern, options, false), this._onDidCreate, this._onDidChange, this._onDidDelete, subscription);117}118119private ensureWatching(mainContext: IMainContext, workspace: IExtHostWorkspace, configuration: ExtHostConfigProvider, extension: IExtensionDescription, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions, correlate: boolean | undefined): Disposable {120const disposable = Disposable.from();121122if (typeof globPattern === 'string') {123return disposable; // workspace is already watched by default, no need to watch again!124}125126if (options.ignoreChangeEvents && options.ignoreCreateEvents && options.ignoreDeleteEvents) {127return disposable; // no need to watch if we ignore all events128}129130const proxy = mainContext.getProxy(MainContext.MainThreadFileSystemEventService);131132let recursive = false;133if (globPattern.pattern.includes(GLOBSTAR) || globPattern.pattern.includes(GLOB_SPLIT)) {134recursive = true; // only watch recursively if pattern indicates the need for it135}136137const excludes = [];138let includes: Array<string | IRelativePattern> | undefined = undefined;139let filter: FileChangeFilter | undefined;140141// Correlated: adjust filter based on arguments142if (correlate) {143if (options.ignoreChangeEvents || options.ignoreCreateEvents || options.ignoreDeleteEvents) {144filter = FileChangeFilter.UPDATED | FileChangeFilter.ADDED | FileChangeFilter.DELETED;145146if (options.ignoreChangeEvents) {147filter &= ~FileChangeFilter.UPDATED;148}149150if (options.ignoreCreateEvents) {151filter &= ~FileChangeFilter.ADDED;152}153154if (options.ignoreDeleteEvents) {155filter &= ~FileChangeFilter.DELETED;156}157}158}159160// Uncorrelated: adjust includes and excludes based on settings161else {162163// Automatically add `files.watcherExclude` patterns when watching164// recursively to give users a chance to configure exclude rules165// for reducing the overhead of watching recursively166if (recursive && excludes.length === 0) {167const workspaceFolder = workspace.getWorkspaceFolder(URI.revive(globPattern.baseUri));168const watcherExcludes = configuration.getConfiguration('files', workspaceFolder).get<IGlobPatterns>('watcherExclude');169if (watcherExcludes) {170for (const key in watcherExcludes) {171if (key && watcherExcludes[key] === true) {172excludes.push(key);173}174}175}176}177178// Non-recursive watching inside the workspace will overlap with179// our standard workspace watchers. To prevent duplicate events,180// we only want to include events for files that are otherwise181// excluded via `files.watcherExclude`. As such, we configure182// to include each configured exclude pattern so that only those183// events are reported that are otherwise excluded.184// However, we cannot just use the pattern as is, because a pattern185// such as `bar` for a exclude, will work to exclude any of186// `<workspace path>/bar` but will not work as include for files within187// `bar` unless a suffix of `/**` if added.188// (https://github.com/microsoft/vscode/issues/148245)189else if (!recursive) {190const workspaceFolder = workspace.getWorkspaceFolder(URI.revive(globPattern.baseUri));191if (workspaceFolder) {192const watcherExcludes = configuration.getConfiguration('files', workspaceFolder).get<IGlobPatterns>('watcherExclude');193if (watcherExcludes) {194for (const key in watcherExcludes) {195if (key && watcherExcludes[key] === true) {196const includePattern = `${rtrim(key, '/')}/${GLOBSTAR}`;197if (!includes) {198includes = [];199}200201includes.push(normalizeWatcherPattern(workspaceFolder.uri.fsPath, includePattern));202}203}204}205206// Still ignore watch request if there are actually no configured207// exclude rules, because in that case our default recursive watcher208// should be able to take care of all events.209if (!includes || includes.length === 0) {210return disposable;211}212}213}214}215216proxy.$watch(extension.identifier.value, this.session, globPattern.baseUri, { recursive, excludes, includes, filter }, Boolean(correlate));217218return Disposable.from({ dispose: () => proxy.$unwatch(this.session) });219}220221dispose() {222this._disposable.dispose();223}224225get onDidCreate(): Event<vscode.Uri> {226return this._onDidCreate.event;227}228229get onDidChange(): Event<vscode.Uri> {230return this._onDidChange.event;231}232233get onDidDelete(): Event<vscode.Uri> {234return this._onDidDelete.event;235}236}237238interface IExtensionListener<E> {239extension: IExtensionDescription;240(e: E): any;241}242243class LazyRevivedFileSystemEvents implements FileSystemEvents {244245readonly session: number | undefined;246247private _created = new Lazy(() => this._events.created.map(URI.revive) as URI[]);248get created(): URI[] { return this._created.value; }249250private _changed = new Lazy(() => this._events.changed.map(URI.revive) as URI[]);251get changed(): URI[] { return this._changed.value; }252253private _deleted = new Lazy(() => this._events.deleted.map(URI.revive) as URI[]);254get deleted(): URI[] { return this._deleted.value; }255256constructor(private readonly _events: FileSystemEvents) {257this.session = this._events.session;258}259}260261export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServiceShape {262263private readonly _onFileSystemEvent = new Emitter<FileSystemEvents>();264265private readonly _onDidRenameFile = new Emitter<vscode.FileRenameEvent>();266private readonly _onDidCreateFile = new Emitter<vscode.FileCreateEvent>();267private readonly _onDidDeleteFile = new Emitter<vscode.FileDeleteEvent>();268private readonly _onWillRenameFile = new AsyncEmitter<vscode.FileWillRenameEvent>();269private readonly _onWillCreateFile = new AsyncEmitter<vscode.FileWillCreateEvent>();270private readonly _onWillDeleteFile = new AsyncEmitter<vscode.FileWillDeleteEvent>();271272readonly onDidRenameFile: Event<vscode.FileRenameEvent> = this._onDidRenameFile.event;273readonly onDidCreateFile: Event<vscode.FileCreateEvent> = this._onDidCreateFile.event;274readonly onDidDeleteFile: Event<vscode.FileDeleteEvent> = this._onDidDeleteFile.event;275276constructor(277private readonly _mainContext: IMainContext,278private readonly _logService: ILogService,279private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors280) {281//282}283284//--- file events285286createFileSystemWatcher(workspace: IExtHostWorkspace, configProvider: ExtHostConfigProvider, extension: IExtensionDescription, globPattern: vscode.GlobPattern, options: FileSystemWatcherCreateOptions): vscode.FileSystemWatcher {287return new FileSystemWatcher(this._mainContext, configProvider, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), options);288}289290$onFileEvent(events: FileSystemEvents) {291this._onFileSystemEvent.fire(new LazyRevivedFileSystemEvents(events));292}293294//--- file operations295296$onDidRunFileOperation(operation: FileOperation, files: SourceTargetPair[]): void {297switch (operation) {298case FileOperation.MOVE:299this._onDidRenameFile.fire(Object.freeze({ files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }));300break;301case FileOperation.DELETE:302this._onDidDeleteFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) }));303break;304case FileOperation.CREATE:305case FileOperation.COPY:306this._onDidCreateFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) }));307break;308default:309//ignore, dont send310}311}312313314getOnWillRenameFileEvent(extension: IExtensionDescription): Event<vscode.FileWillRenameEvent> {315return this._createWillExecuteEvent(extension, this._onWillRenameFile);316}317318getOnWillCreateFileEvent(extension: IExtensionDescription): Event<vscode.FileWillCreateEvent> {319return this._createWillExecuteEvent(extension, this._onWillCreateFile);320}321322getOnWillDeleteFileEvent(extension: IExtensionDescription): Event<vscode.FileWillDeleteEvent> {323return this._createWillExecuteEvent(extension, this._onWillDeleteFile);324}325326private _createWillExecuteEvent<E extends IWaitUntil>(extension: IExtensionDescription, emitter: AsyncEmitter<E>): Event<E> {327return (listener, thisArg, disposables) => {328const wrappedListener: IExtensionListener<E> = function wrapped(e: E) { listener.call(thisArg, e); };329wrappedListener.extension = extension;330return emitter.event(wrappedListener, undefined, disposables);331};332}333334async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise<IWillRunFileOperationParticipation | undefined> {335switch (operation) {336case FileOperation.MOVE:337return await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, timeout, token);338case FileOperation.DELETE:339return await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);340case FileOperation.CREATE:341case FileOperation.COPY:342return await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);343}344return undefined;345}346347private async _fireWillEvent<E extends IWaitUntil>(emitter: AsyncEmitter<E>, data: IWaitUntilData<E>, timeout: number, token: CancellationToken): Promise<IWillRunFileOperationParticipation | undefined> {348349const extensionNames = new Set<string>();350const edits: [IExtensionDescription, WorkspaceEdit][] = [];351352await emitter.fireAsync(data, token, async (thenable: Promise<unknown>, listener) => {353// ignore all results except for WorkspaceEdits. Those are stored in an array.354const now = Date.now();355const result = await Promise.resolve(thenable);356if (result instanceof WorkspaceEdit) {357edits.push([(<IExtensionListener<E>>listener).extension, result]);358extensionNames.add((<IExtensionListener<E>>listener).extension.displayName ?? (<IExtensionListener<E>>listener).extension.identifier.value);359}360361if (Date.now() - now > timeout) {362this._logService.warn('SLOW file-participant', (<IExtensionListener<E>>listener).extension.identifier);363}364});365366if (token.isCancellationRequested) {367return undefined;368}369370if (edits.length === 0) {371return undefined;372}373374// concat all WorkspaceEdits collected via waitUntil-call and send them over to the renderer375const dto: IWorkspaceEditDto = { edits: [] };376for (const [, edit] of edits) {377const { edits } = typeConverter.WorkspaceEdit.from(edit, {378getTextDocumentVersion: uri => this._extHostDocumentsAndEditors.getDocument(uri)?.version,379getNotebookDocumentVersion: () => undefined,380});381dto.edits = dto.edits.concat(edits);382}383return { edit: dto, extensionNames: Array.from(extensionNames) };384}385}386387388