API: Allow to use the file watcher for aribitrary folders (#3025) (#139881)

* API: Allow to use the file watcher for aribitrary folders (#3025)

* fix tests

* update `createFileSystemWatcher` docs

* refuse to watch resources that are watched in workspace already

* properly check proposed API

* make it work via `createFileSystemWacher` (first cut)

* more docs

* cleanup

* enable recursive watching based on pattern

* add tests

* drop out-of-workspace events when using simple patterns

* do not apply excludes when watchig files

* log extension watch requests

* also log unwatch

* improved exclude handling

* more docs

* drop proposed api needs

* remove `suite.only`

* cannot watch inside workspace more than once

* do not send extension decriptor over

* adopt latest changes

* add `baseUri` to relative pattern

* backwards compat
This commit is contained in:
Benjamin Pasero
2022-01-13 13:32:03 +01:00
committed by GitHub
parent 0cfa366323
commit e3cf7e5e1b
13 changed files with 301 additions and 64 deletions

View File

@@ -830,7 +830,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
return extHostBulkEdits.applyWorkspaceEdit(edit);
},
createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): vscode.FileSystemWatcher => {
return extHostFileSystemEvent.createFileSystemWatcher(typeConverters.GlobPattern.from(pattern), ignoreCreate, ignoreChange, ignoreDelete);
return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, extension, typeConverters.GlobPattern.from(pattern), ignoreCreate, ignoreChange, ignoreDelete);
},
get textDocuments() {
return extHostDocuments.getAllDocumentData().map(data => data.document);

View File

@@ -1043,6 +1043,9 @@ export interface MainThreadFileSystemShape extends IDisposable {
$mkdir(resource: UriComponents): Promise<void>;
$delete(resource: UriComponents, opts: files.FileDeleteOptions): Promise<void>;
$watch(extensionId: string, session: number, resource: UriComponents, opts: files.IWatchOptions): void;
$unwatch(session: number): void;
$ensureActivation(scheme: string): Promise<void>;
}

View File

@@ -4,23 +4,25 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event, AsyncEmitter, IWaitUntil, IWaitUntilData } from 'vs/base/common/event';
import { IRelativePattern, parse } from 'vs/base/common/glob';
import { GLOBSTAR, GLOB_SPLIT, parse } from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import type * as vscode from 'vscode';
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation } from './extHost.protocol';
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation, MainContext } from './extHost.protocol';
import * as typeConverter from './extHostTypeConverters';
import { Disposable, WorkspaceEdit } from './extHostTypes';
import { Disposable, RelativePattern, WorkspaceEdit } from './extHostTypes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { FileOperation } from 'vs/platform/files/common/files';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
class FileSystemWatcher implements vscode.FileSystemWatcher {
private readonly _onDidCreate = new Emitter<vscode.Uri>();
private readonly _onDidChange = new Emitter<vscode.Uri>();
private readonly _onDidDelete = new Emitter<vscode.Uri>();
private _disposable: Disposable;
private _config: number;
@@ -36,7 +38,8 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
return Boolean(this._config & 0b100);
}
constructor(dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) {
constructor(mainContext: IMainContext, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | RelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) {
const watcherDisposable = this.ensureWatching(mainContext, extension, globPattern);
this._config = 0;
if (ignoreCreateEvents) {
@@ -51,11 +54,18 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
const parsedPattern = parse(globPattern);
// 1.64.x behaviour change: given the new support to watch any folder
// we start to ignore events outside the workspace when only a string
// pattern is provided to avoid sending events to extensions that are
// unexpected.
// https://github.com/microsoft/vscode/issues/3025
const excludeOutOfWorkspaceEvents = typeof globPattern === 'string';
const subscription = dispatcher(events => {
if (!ignoreCreateEvents) {
for (let created of events.created) {
const uri = URI.revive(created);
if (parsedPattern(uri.fsPath)) {
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
this._onDidCreate.fire(uri);
}
}
@@ -63,7 +73,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
if (!ignoreChangeEvents) {
for (let changed of events.changed) {
const uri = URI.revive(changed);
if (parsedPattern(uri.fsPath)) {
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
this._onDidChange.fire(uri);
}
}
@@ -71,14 +81,34 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
if (!ignoreDeleteEvents) {
for (let deleted of events.deleted) {
const uri = URI.revive(deleted);
if (parsedPattern(uri.fsPath)) {
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
this._onDidDelete.fire(uri);
}
}
}
});
this._disposable = Disposable.from(this._onDidCreate, this._onDidChange, this._onDidDelete, subscription);
this._disposable = Disposable.from(watcherDisposable, this._onDidCreate, this._onDidChange, this._onDidDelete, subscription);
}
private ensureWatching(mainContext: IMainContext, extension: IExtensionDescription, globPattern: string | RelativePattern): Disposable {
let disposable = Disposable.from();
if (typeof globPattern === 'string') {
return disposable; // a pattern alone does not carry sufficient information to start watching anything
}
const proxy = mainContext.getProxy(MainContext.MainThreadFileSystem);
let recursive = false;
if (globPattern.pattern.includes(GLOBSTAR) || globPattern.pattern.includes(GLOB_SPLIT)) {
recursive = true; // only watch recursively if pattern indicates the need for it
}
const session = Math.random();
proxy.$watch(extension.identifier.value, session, globPattern.baseUri, { recursive, excludes: [] /* excludes are not yet surfaced in the API */ });
return Disposable.from({ dispose: () => proxy.$unwatch(session) });
}
dispose() {
@@ -118,9 +148,8 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
readonly onDidCreateFile: Event<vscode.FileCreateEvent> = this._onDidCreateFile.event;
readonly onDidDeleteFile: Event<vscode.FileDeleteEvent> = this._onDidDeleteFile.event;
constructor(
mainContext: IMainContext,
private readonly _mainContext: IMainContext,
private readonly _logService: ILogService,
private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors
) {
@@ -129,8 +158,8 @@ 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(workspace: IExtHostWorkspace, extension: IExtensionDescription, globPattern: string | RelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher {
return new FileSystemWatcher(this._mainContext, workspace, extension, this._onFileSystemEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents);
}
$onFileEvent(events: FileSystemEvents) {

View File

@@ -1392,15 +1392,25 @@ export namespace GlobPattern {
return pattern;
}
if (isRelativePattern(pattern)) {
return new types.RelativePattern(pattern.base, pattern.pattern);
if (isRelativePattern(pattern) || isLegacyRelativePattern(pattern)) {
return new types.RelativePattern(pattern.baseUri ?? pattern.base, pattern.pattern);
}
return pattern; // preserve `undefined` and `null`
}
function isRelativePattern(obj: any): obj is vscode.RelativePattern {
export function isRelativePattern(obj: any): obj is vscode.RelativePattern {
const rp = obj as vscode.RelativePattern;
return rp && URI.isUri(rp.baseUri) && typeof rp.pattern === 'string';
}
function isLegacyRelativePattern(obj: any): obj is { base: string, pattern: string } {
// Before 1.64.x, `RelativePattern` did not have any `baseUri: Uri`
// property. To preserve backwards compatibility with older extensions
// we allow this old format when creating the `vscode.RelativePattern`.
const rp = obj as { base: string, pattern: string };
return rp && typeof rp.base === 'string' && typeof rp.pattern === 'string';
}
}
@@ -1581,8 +1591,8 @@ export namespace NotebookExclusiveDocumentPattern {
}
if (isRelativePattern(pattern)) {
return new types.RelativePattern(pattern.base, pattern.pattern);
if (GlobPattern.isRelativePattern(pattern)) {
return new types.RelativePattern(pattern.baseUri, pattern.pattern);
}
if (isExclusivePattern(pattern)) {
@@ -1601,11 +1611,8 @@ export namespace NotebookExclusiveDocumentPattern {
return pattern;
}
if (isRelativePattern(pattern)) {
return {
base: pattern.base,
pattern: pattern.pattern
};
if (GlobPattern.isRelativePattern(pattern)) {
return new types.RelativePattern(pattern.baseUri, pattern.pattern);
}
return {
@@ -1628,11 +1635,6 @@ export namespace NotebookExclusiveDocumentPattern {
return true;
}
function isRelativePattern(obj: any): obj is vscode.RelativePattern {
const rp = obj as vscode.RelativePattern;
return rp && typeof rp.base === 'string' && typeof rp.pattern === 'string';
}
}
export namespace NotebookDecorationRenderOptions {

View File

@@ -2370,15 +2370,26 @@ export enum ConfigurationTarget {
@es5ClassCompat
export class RelativePattern implements IRelativePattern {
base: string;
pattern: string;
// expose a `baseFolder: URI` property as a workaround for the short-coming
// of `IRelativePattern` only supporting `base: string` which always translates
// to a `file://` URI. With `baseFolder` we can support non-file based folders
// in searches
// (https://github.com/microsoft/vscode/commit/6326543b11cf4998c8fd1564cab5c429a2416db3)
readonly baseFolder?: URI;
private _base!: string;
get base(): string {
return this._base;
}
set base(base: string) {
this._base = base;
this._baseUri = URI.file(base);
}
private _baseUri!: URI;
get baseUri(): URI {
return this._baseUri;
}
set baseUri(baseUri: URI) {
this._baseUri = baseUri;
this._base = baseUri.fsPath;
}
constructor(base: vscode.WorkspaceFolder | URI | string, pattern: string) {
if (typeof base !== 'string') {
@@ -2392,14 +2403,11 @@ export class RelativePattern implements IRelativePattern {
}
if (typeof base === 'string') {
this.baseFolder = URI.file(base);
this.base = base;
this.baseUri = URI.file(base);
} else if (URI.isUri(base)) {
this.baseFolder = base;
this.base = base.fsPath;
this.baseUri = base;
} else {
this.baseFolder = base.uri;
this.base = base.uri.fsPath;
this.baseUri = base.uri;
}
this.pattern = pattern;

View File

@@ -579,7 +579,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
export const IExtHostWorkspace = createDecorator<IExtHostWorkspace>('IExtHostWorkspace');
export interface IExtHostWorkspace extends ExtHostWorkspace, ExtHostWorkspaceShape, IExtHostWorkspaceProvider { }
function parseSearchInclude(include: RelativePattern | string | undefined): { includePattern?: string, folder?: URI; } {
function parseSearchInclude(include: RelativePattern | vscode.GlobPattern | string | undefined): { includePattern?: string, folder?: URI; } {
let includePattern: string | undefined;
let includeFolder: URI | undefined;
if (include) {
@@ -587,7 +587,7 @@ function parseSearchInclude(include: RelativePattern | string | undefined): { in
includePattern = include;
} else {
includePattern = include.pattern;
includeFolder = include.baseFolder || URI.file(include.base);
includeFolder = include.baseUri;
}
}