diff --git a/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts b/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts index 8a9398ce6c99e..8e02f2cb97c5d 100644 --- a/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts +++ b/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts @@ -48,7 +48,7 @@ type Event = vscode.Event; type IExtensionDescription = Plugin; type IWaitUntil = WaitUntilEvent; -class FileSystemWatcher implements vscode.FileSystemWatcher { +export class FileSystemWatcher implements vscode.FileSystemWatcher { private readonly _onDidCreate = new Emitter(); private readonly _onDidChange = new Emitter(); @@ -68,7 +68,8 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { return Boolean(this._config & 0b100); } - constructor(dispatcher: Event, globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) { + constructor(dispatcher: Event, globPattern: string | IRelativePattern, + ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean, excludes?: string[]) { this._config = 0; if (ignoreCreateEvents) { @@ -82,12 +83,13 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { } const parsedPattern = parse(globPattern); + const excludePatterns = excludes?.map(exclude => parse(exclude)) || []; const subscription = dispatcher(events => { if (!ignoreCreateEvents) { for (const created of events.created) { const uri = URI.revive(created); - if (parsedPattern(uri.fsPath)) { + if (parsedPattern(uri.fsPath) && !excludePatterns.some(p => p(uri.fsPath))) { this._onDidCreate.fire(uri); } } @@ -95,7 +97,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { if (!ignoreChangeEvents) { for (const changed of events.changed) { const uri = URI.revive(changed); - if (parsedPattern(uri.fsPath)) { + if (parsedPattern(uri.fsPath) && !excludePatterns.some(p => p(uri.fsPath))) { this._onDidChange.fire(uri); } } @@ -103,7 +105,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { if (!ignoreDeleteEvents) { for (const deleted of events.deleted) { const uri = URI.revive(deleted); - if (parsedPattern(uri.fsPath)) { + if (parsedPattern(uri.fsPath) && !excludePatterns.some(p => p(uri.fsPath))) { this._onDidDelete.fire(uri); } } @@ -113,7 +115,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher { this._disposable = Disposable.from(this._onDidCreate, this._onDidChange, this._onDidDelete, subscription); } - dispose() { + dispose(): void { this._disposable.dispose(); } @@ -160,8 +162,9 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ // --- file events - createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher { - return new FileSystemWatcher(this._onFileSystemEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); + createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean, excludes?: string[]): vscode.FileSystemWatcher { + return new FileSystemWatcher(this._onFileSystemEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents, excludes); } $onFileEvent(events: FileSystemEvents) { diff --git a/packages/plugin-ext/src/plugin/file-system-watcher.spec.ts b/packages/plugin-ext/src/plugin/file-system-watcher.spec.ts new file mode 100644 index 0000000000000..346abbe122b64 --- /dev/null +++ b/packages/plugin-ext/src/plugin/file-system-watcher.spec.ts @@ -0,0 +1,125 @@ +// ***************************************************************************** +// Copyright (C) 2019 Red Hat, Inc. and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as assert from 'assert'; +import { FileSystemWatcher } from './file-system-event-service-ext-impl'; +import { DisposableCollection, Emitter } from '@theia/core'; +import { FileSystemEvents } from '../common'; +import { URI } from './types-impl'; + +const eventSource = new Emitter(); +let disposables = new DisposableCollection(); + +function checkIgnore(ignoreCreate: number, ignoreChange: number, ignoreDelete: number): void { + const watcher = new FileSystemWatcher(eventSource.event, '**/*.js', !ignoreCreate, !ignoreChange, !ignoreDelete); + disposables.push(watcher); + const matching = URI.file('/foo/bar/zoz.js'); + + const changed: URI[] = []; + const created: URI[] = []; + const deleted: URI[] = []; + watcher.onDidChange(e => { + changed.push(e); + }); + + watcher.onDidCreate(e => { + created.push(e); + }); + + watcher.onDidDelete(e => { + deleted.push(e); + }); + + eventSource.fire({ changed: [matching], created: [matching], deleted: [matching] }); + + assert.equal(created.length, ignoreCreate); + assert.equal(deleted.length, ignoreDelete); + assert.equal(changed.length, ignoreChange); + +} + +describe('File Watcher Test', () => { + afterEach(() => { + disposables.dispose(); + disposables = new DisposableCollection(); + }); + + it('Should match files', () => { + const watcher = new FileSystemWatcher(eventSource.event, '**/*.js'); + disposables.push(watcher); + const matching = URI.file('/foo/bar/zoz.js'); + const notMatching = URI.file('/foo/bar/zoz.ts'); + const changed: URI[] = []; + const created: URI[] = []; + const deleted: URI[] = []; + watcher.onDidChange(e => { + changed.push(e); + }); + + watcher.onDidCreate(e => { + created.push(e); + }); + + watcher.onDidDelete(e => { + deleted.push(e); + }); + + const URIs = [matching, notMatching]; + eventSource.fire({ changed: URIs, created: URIs, deleted: URIs }); + assert.equal(matching.toString(), changed[0]?.toString()); + assert.equal(matching.toString(), created[0]?.toString()); + assert.equal(matching.toString(), deleted[0]?.toString()); + }); + + it('Should ignore created', () => { + checkIgnore(0, 1, 1); + }); + + it('Should ignore changed', () => { + checkIgnore(1, 0, 1); + }); + + it('Should ignore deleted', () => { + checkIgnore(1, 1, 0); + }); + + it('Should exclude files', () => { + const watcher = new FileSystemWatcher(eventSource.event, '**/*.js', false, false, false, ['**/bar/**']); + disposables.push(watcher); + const notMatching = URI.file('/foo/bar/zoz.js'); + const matching = URI.file('/foo/gux/zoz.js'); + const changed: URI[] = []; + const created: URI[] = []; + const deleted: URI[] = []; + watcher.onDidChange(e => { + changed.push(e); + }); + + watcher.onDidCreate(e => { + created.push(e); + }); + + watcher.onDidDelete(e => { + deleted.push(e); + }); + + const URIs = [matching, notMatching]; + eventSource.fire({ changed: URIs, created: URIs, deleted: URIs }); + assert.equal(matching.toString(), changed[0]?.toString()); + assert.equal(matching.toString(), created[0]?.toString()); + assert.equal(matching.toString(), deleted[0]?.toString()); + }); +}); diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 61688deddeecf..e027b45d6b84c 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -278,6 +278,7 @@ import { NotebookDocumentsExtImpl } from './notebook/notebook-documents'; import { NotebookEditorsExtImpl } from './notebook/notebook-editors'; import { TestingExtImpl } from './tests'; import { UriExtImpl } from './uri-ext'; +import { isObject } from '@theia/core'; export function createAPIObject(rawObject: T): T { return new Proxy(rawObject, { @@ -669,6 +670,22 @@ export function createAPIFactory( onDidStartTerminalShellExecution: Event.None }; + function createFileSystemWatcher(pattern: RelativePattern, options?: theia.FileSystemWatcherOptions): theia.FileSystemWatcher; + function createFileSystemWatcher(pattern: theia.GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: + boolean, ignoreDeleteEvents?: boolean): theia.FileSystemWatcher; + function createFileSystemWatcher(pattern: RelativePattern | theia.GlobPattern, + ignoreCreateOrOptions?: theia.FileSystemWatcherOptions | boolean, ignoreChangeEventsBoolean?: boolean, ignoreDeleteEventsBoolean?: boolean): theia.FileSystemWatcher { + if (isObject(ignoreCreateOrOptions)) { + const { ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents, excludes } = (ignoreCreateOrOptions as theia.FileSystemWatcherOptions); + return createAPIObject( + extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), + ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents, excludes)); + } else { + return createAPIObject( + extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), + ignoreCreateOrOptions as boolean, ignoreChangeEventsBoolean, ignoreDeleteEventsBoolean)); + } + } const workspace: typeof theia.workspace = { get fs(): theia.FileSystem { @@ -774,8 +791,7 @@ export function createAPIFactory( // Notebook extension will create a document in openNotebookDocument() or create openNotebookDocument() return notebooksExt.getNotebookDocument(uri).apiNotebook; }, - createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): theia.FileSystemWatcher => - createAPIObject(extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), ignoreCreate, ignoreChange, ignoreDelete)), + createFileSystemWatcher, findFiles(include: theia.GlobPattern, exclude?: theia.GlobPattern | null, maxResults?: number, token?: CancellationToken): PromiseLike { return workspaceExt.findFiles(include, exclude, maxResults, token); }, diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index fe8d5f6200d28..c98c3ccef5691 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -23,6 +23,7 @@ import './theia-extra'; import './theia.proposed.canonicalUriProvider'; +import './theia.proposed.createFileSystemWatcher'; import './theia.proposed.customEditorMove'; import './theia.proposed.debugVisualization'; import './theia.proposed.diffCommand'; diff --git a/packages/plugin/src/theia.proposed.createFileSystemWatcher.ts b/packages/plugin/src/theia.proposed.createFileSystemWatcher.ts new file mode 100644 index 0000000000000..17d4d641266b7 --- /dev/null +++ b/packages/plugin/src/theia.proposed.createFileSystemWatcher.ts @@ -0,0 +1,65 @@ +// ***************************************************************************** +// Copyright (C) 2024 STMicroelectronics and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// code copied and modified from https://github.com/microsoft/vscode/blob/release/1.93/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts + +declare module '@theia/plugin' { + + export interface FileSystemWatcherOptions { + + /** + * Ignore when files have been created. + */ + readonly ignoreCreateEvents?: boolean; + + /** + * Ignore when files have been changed. + */ + readonly ignoreChangeEvents?: boolean; + + /** + * Ignore when files have been deleted. + */ + readonly ignoreDeleteEvents?: boolean; + + /** + * An optional set of glob patterns to exclude from watching. + * Glob patterns are always matched relative to the watched folder. + */ + readonly excludes: string[]; + } + + export namespace workspace { + + /** + * A variant of {@link workspace.createFileSystemWatcher} that optionally allows to specify + * a set of glob patterns to exclude from watching. + * + * It provides the following advantages over the other {@link workspace.createFileSystemWatcher} + * method: + * - the configured excludes from `files.watcherExclude` setting are NOT applied + * - requests for recursive file watchers inside the opened workspace are NOT ignored + * - the watcher is ONLY notified for events from this request and not from any other watcher + * + * As such, this method is prefered in cases where you want full control over the watcher behavior + * without being impacted by settings or other watchers that are installed. + */ + export function createFileSystemWatcher(pattern: RelativePattern, options?: FileSystemWatcherOptions): FileSystemWatcher; + } +}