watcher - correlate events to their requesting source (#194776)

* watcher - emit `URI` instead of `string` for faster `fsPath` compute  (for #194341)

* wip

* more

* adopt

* some cleanup

* cleanup

* cleanup

* cleanup

* cleanup

* cleanup

* cleanup

* implement correlation

* cleanup

* add correlation

* undo, leave for later

* tests

* tests

* tests

* tests

* tests

* log cId

* simpler correlation id

* 💄

* tests

* runs

* skip normalization

* fix tests

* tests

* fix tests

* add `createWatcher` API

* partition events in ext host

* allow custom excludes

* remove disk file change

* 💄

* 💄

* 💄

* wire in

* wire in
This commit is contained in:
Benjamin Pasero 2023-10-10 10:27:18 +02:00 committed by GitHub
parent 8988b0fd43
commit 29b69437ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 887 additions and 397 deletions

View file

@ -8,6 +8,7 @@
"authSession",
"contribViewsRemote",
"contribStatusBarItems",
"createFileSystemWatcher",
"customEditorMove",
"diffCommand",
"documentFiltersExclusive",

View file

@ -27,19 +27,28 @@ suite('vscode API - workspace-watcher', () => {
}
}
teardown(assertNoRpc);
let fs: WatcherTestFs;
let disposable: vscode.Disposable;
test('createFileSystemWatcher', async function () {
const fs = new WatcherTestFs('watcherTest', false);
vscode.workspace.registerFileSystemProvider('watcherTest', fs);
function onDidWatchPromise() {
const onDidWatchPromise = new Promise<IWatchRequest>(resolve => {
fs.onDidWatch(request => resolve(request));
});
function onDidWatchPromise() {
const onDidWatchPromise = new Promise<IWatchRequest>(resolve => {
fs.onDidWatch(request => resolve(request));
});
return onDidWatchPromise;
}
return onDidWatchPromise;
}
setup(() => {
fs = new WatcherTestFs('watcherTest', false);
disposable = vscode.workspace.registerFileSystemProvider('watcherTest', fs);
});
teardown(() => {
disposable.dispose();
assertNoRpc();
});
test('createFileSystemWatcher (old style)', async function () {
// Non-recursive
let watchUri = vscode.Uri.from({ scheme: 'watcherTest', path: '/somePath/folder' });
@ -59,4 +68,29 @@ suite('vscode API - workspace-watcher', () => {
assert.strictEqual(request.uri.toString(), watchUri.toString());
assert.strictEqual(request.options.recursive, true);
});
test('createFileSystemWatcher (new style)', async function () {
// Non-recursive
let watchUri = vscode.Uri.from({ scheme: 'watcherTest', path: '/somePath/folder' });
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(watchUri, '*.txt'), { excludes: ['testing'], ignoreChangeEvents: true });
let request = await onDidWatchPromise();
assert.strictEqual(request.uri.toString(), watchUri.toString());
assert.strictEqual(request.options.recursive, false);
assert.strictEqual(request.options.excludes.length, 1);
assert.strictEqual(request.options.excludes[0], 'testing');
watcher.dispose();
// Recursive
watchUri = vscode.Uri.from({ scheme: 'watcherTest', path: '/somePath/folder' });
vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(watchUri, '**/*.txt'), { excludes: ['testing'], ignoreCreateEvents: true });
request = await onDidWatchPromise();
assert.strictEqual(request.uri.toString(), watchUri.toString());
assert.strictEqual(request.options.recursive, true);
assert.strictEqual(request.options.excludes.length, 1);
assert.strictEqual(request.options.excludes[0], 'testing');
});
});

View file

@ -11,7 +11,7 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'
import { normalize } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { IFileChange, IFileSystemProvider, IWatchOptions } from 'vs/platform/files/common/files';
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage, INonRecursiveWatchRequest, IRecursiveWatcherOptions, isRecursiveWatchRequest, IUniversalWatchRequest, toFileChanges } from 'vs/platform/files/common/watcher';
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage, INonRecursiveWatchRequest, IRecursiveWatcherOptions, isRecursiveWatchRequest, IUniversalWatchRequest, reviveFileChanges } from 'vs/platform/files/common/watcher';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
export interface IDiskFileSystemProviderOptions {
@ -72,7 +72,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
private watchUniversal(resource: URI, opts: IWatchOptions): IDisposable {
// Add to list of paths to watch universally
const pathToWatch: IUniversalWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: opts.recursive };
const pathToWatch: IUniversalWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: opts.recursive, correlationId: opts.correlationId };
const remove = insert(this.universalPathsToWatch, pathToWatch);
// Trigger update
@ -102,7 +102,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
// Create watcher if this is the first time
if (!this.universalWatcher) {
this.universalWatcher = this._register(this.createUniversalWatcher(
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
changes => this._onDidChangeFile.fire(reviveFileChanges(changes)),
msg => this.onWatcherLogMessage(msg),
this.logService.getLevel() === LogLevel.Trace
));
@ -136,7 +136,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
}
protected abstract createUniversalWatcher(
onChange: (changes: IDiskFileChange[]) => void,
onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): AbstractUniversalWatcherClient;
@ -153,7 +153,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
private watchNonRecursive(resource: URI, opts: IWatchOptions): IDisposable {
// Add to list of paths to watch non-recursively
const pathToWatch: INonRecursiveWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: false };
const pathToWatch: INonRecursiveWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes, includes: opts.includes, recursive: false, correlationId: opts.correlationId };
const remove = insert(this.nonRecursivePathsToWatch, pathToWatch);
// Trigger update
@ -183,7 +183,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
// Create watcher if this is the first time
if (!this.nonRecursiveWatcher) {
this.nonRecursiveWatcher = this._register(this.createNonRecursiveWatcher(
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
changes => this._onDidChangeFile.fire(reviveFileChanges(changes)),
msg => this.onWatcherLogMessage(msg),
this.logService.getLevel() === LogLevel.Trace
));
@ -199,7 +199,7 @@ export abstract class AbstractDiskFileSystemProvider extends Disposable implemen
}
protected abstract createNonRecursiveWatcher(
onChange: (changes: IDiskFileChange[]) => void,
onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): AbstractNonRecursiveWatcherClient;

View file

@ -10,10 +10,11 @@ import { canceled } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } from 'vs/base/common/stream';
import { URI, UriComponents } from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { createFileSystemProviderError, IFileAtomicReadOptions, FileChangeType, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions, IFileSystemProviderError } from 'vs/platform/files/common/files';
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions, IFileSystemProviderError } from 'vs/platform/files/common/files';
import { reviveFileChanges } from 'vs/platform/files/common/watcher';
export const LOCAL_FILE_SYSTEM_CHANNEL_NAME = 'localFilesystem';
@ -229,10 +230,10 @@ export class DiskFileSystemProviderClient extends Disposable implements
// for both events and errors from the watcher. So we need to
// unwrap the event from the remote and emit through the proper
// emitter.
this._register(this.channel.listen<{ resource: UriComponents; type: FileChangeType }[] | string>('fileChange', [this.sessionId])(eventsOrError => {
this._register(this.channel.listen<IFileChange[] | string>('fileChange', [this.sessionId])(eventsOrError => {
if (Array.isArray(eventsOrError)) {
const events = eventsOrError;
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type })));
this._onDidChange.fire(reviveFileChanges(events));
} else {
const error = eventsOrError;
this._onDidWatchError.fire(error);

View file

@ -18,7 +18,7 @@ import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from 'vs/base/c
import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability } from 'vs/platform/files/common/files';
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability, IWatchOptionsWithCorrelation, IFileSystemWatcher, IWatchOptionsWithoutCorrelation } from 'vs/platform/files/common/files';
import { readFileIntoStream } from 'vs/platform/files/common/io';
import { ILogService } from 'vs/platform/log/common/log';
import { ErrorNoTelemetry } from 'vs/base/common/errors';
@ -63,7 +63,17 @@ export class FileService extends Disposable implements IFileService {
this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
// Forward events from provider
providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes, !this.isPathCaseSensitive(provider)))));
providerDisposables.add(provider.onDidChangeFile(changes => {
const event = new FileChangesEvent(changes, !this.isPathCaseSensitive(provider));
// Always emit any event internally
this.internalOnDidFilesChange.fire(event);
// Only emit uncorrelated events in the global `onDidFilesChange` event
if (!event.hasCorrelation()) {
this._onDidUncorrelatedFilesChange.fire(event);
}
}));
if (typeof provider.onDidWatchError === 'function') {
providerDisposables.add(provider.onDidWatchError(error => this._onDidWatchError.fire(new Error(error))));
}
@ -1094,15 +1104,31 @@ export class FileService extends Disposable implements IFileService {
//#region File Watching
private readonly _onDidFilesChange = this._register(new Emitter<FileChangesEvent>());
readonly onDidFilesChange = this._onDidFilesChange.event;
private readonly internalOnDidFilesChange = this._register(new Emitter<FileChangesEvent>());
private readonly _onDidUncorrelatedFilesChange = this._register(new Emitter<FileChangesEvent>());
readonly onDidFilesChange = this._onDidUncorrelatedFilesChange.event; // global `onDidFilesChange` skips correlated events
private readonly _onDidWatchError = this._register(new Emitter<Error>());
readonly onDidWatchError = this._onDidWatchError.event;
private readonly activeWatchers = new Map<number /* watch request hash */, { disposable: IDisposable; count: number }>();
watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable {
private static WATCHER_CORRELATION_IDS = 0;
createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation): IFileSystemWatcher {
return this.watch(resource, {
...options,
// Explicitly set a correlation id so that file events that originate
// from requests from extensions are exclusively routed back to the
// extension host and not into the workbench.
correlationId: FileService.WATCHER_CORRELATION_IDS++
});
}
watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;
watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IFileSystemWatcher | IDisposable {
const disposables = new DisposableStore();
// Forward watch request to provider and wire in disposables
@ -1125,6 +1151,25 @@ export class FileService extends Disposable implements IFileService {
}
})();
// When a correlation identifier is set, return a specific
// watcher that only emits events matching that correalation.
const correlationId = options.correlationId;
if (typeof correlationId === 'number') {
const fileChangeEmitter = disposables.add(new Emitter<FileChangesEvent>());
disposables.add(this.internalOnDidFilesChange.event(e => {
if (e.correlates(correlationId)) {
fileChangeEmitter.fire(e);
}
}));
const watcher: IFileSystemWatcher = {
onDidChange: fileChangeEmitter.event,
dispose: () => disposables.dispose()
};
return watcher;
}
return disposables;
}

View file

@ -19,6 +19,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { isWeb } from 'vs/base/common/platform';
import { Schemas } from 'vs/base/common/network';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Lazy } from 'vs/base/common/lazy';
//#region file service & providers
@ -237,10 +238,27 @@ export interface IFileService {
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
*
* Note: recursive file watching is not supported from this method. Only events from files
* that are direct children of the provided resource will be reported.
* The watcher runs correlated and thus, file events will be reported on the returned
* `IFileSystemWatcher` and not on the generic `IFileService.onDidFilesChange` event.
*/
watch(resource: URI, options?: IWatchOptions): IDisposable;
createWatcher(resource: URI, options: IWatchOptionsWithoutCorrelation): IFileSystemWatcher;
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
*
* The watcher runs correlated and thus, file events will be reported on the returned
* `IFileSystemWatcher` and not on the generic `IFileService.onDidFilesChange` event.
*/
watch(resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
*
* The watcher runs uncorrelated and thus will report all events from `IFileService.onDidFilesChange`.
* This means, most listeners in the application will receive your events. It is encouraged to
* use correlated watchers (via `IWatchOptionsWithCorrelation`) to limit events to your listener.
*/
watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable;
/**
* Frees up any resources occupied by this service.
@ -484,13 +502,13 @@ export interface IStat {
readonly permissions?: FilePermission;
}
export interface IWatchOptions {
export interface IWatchOptionsWithoutCorrelation {
/**
* Set to `true` to watch for changes recursively in a folder
* and all of its children.
*/
readonly recursive: boolean;
recursive: boolean;
/**
* A set of glob patterns or paths to exclude from watching.
@ -511,6 +529,36 @@ export interface IWatchOptions {
includes?: Array<string | IRelativePattern>;
}
export interface IWatchOptions extends IWatchOptionsWithoutCorrelation {
/**
* If provided, file change events from the watcher that
* are a result of this watch request will carry the same
* id.
*/
readonly correlationId?: number;
}
export interface IWatchOptionsWithCorrelation extends IWatchOptions {
readonly correlationId: number;
}
export interface IFileSystemWatcher extends IDisposable {
/**
* An event which fires on file/folder change only for changes
* that correlate to the watch request with matching correlation
* identifier.
*/
readonly onDidChange: Event<FileChangesEvent>;
}
export function isFileSystemWatcher(thing: unknown): thing is IFileSystemWatcher {
const candidate = thing as IFileSystemWatcher | undefined;
return !!candidate && typeof candidate.onDidChange === 'function';
}
export const enum FileSystemProviderCapabilities {
/**
@ -882,32 +930,32 @@ export interface IFileChange {
/**
* The type of change that occurred to the file.
*/
readonly type: FileChangeType;
type: FileChangeType;
/**
* The unified resource identifier of the file that changed.
*/
readonly resource: URI;
/**
* If provided when starting the file watcher, the correlation
* identifier will match the original file watching request as
* a way to identify the original component that is interested
* in the change.
*/
readonly cId?: number;
}
export class FileChangesEvent {
private readonly added: TernarySearchTree<URI, IFileChange> | undefined = undefined;
private readonly updated: TernarySearchTree<URI, IFileChange> | undefined = undefined;
private readonly deleted: TernarySearchTree<URI, IFileChange> | undefined = undefined;
private static readonly MIXED_CORRELATION = null;
constructor(changes: readonly IFileChange[], ignorePathCasing: boolean) {
const entriesByType = new Map<FileChangeType, [URI, IFileChange][]>();
private readonly correlationId: number | undefined | typeof FileChangesEvent.MIXED_CORRELATION = undefined;
constructor(changes: readonly IFileChange[], private readonly ignorePathCasing: boolean) {
for (const change of changes) {
const array = entriesByType.get(change.type);
if (array) {
array.push([change.resource, change]);
} else {
entriesByType.set(change.type, [[change.resource, change]]);
}
// Split by type
switch (change.type) {
case FileChangeType.ADDED:
this.rawAdded.push(change.resource);
@ -919,26 +967,45 @@ export class FileChangesEvent {
this.rawDeleted.push(change.resource);
break;
}
}
for (const [key, value] of entriesByType) {
switch (key) {
case FileChangeType.ADDED:
this.added = TernarySearchTree.forUris<IFileChange>(() => ignorePathCasing);
this.added.fill(value);
break;
case FileChangeType.UPDATED:
this.updated = TernarySearchTree.forUris<IFileChange>(() => ignorePathCasing);
this.updated.fill(value);
break;
case FileChangeType.DELETED:
this.deleted = TernarySearchTree.forUris<IFileChange>(() => ignorePathCasing);
this.deleted.fill(value);
break;
// Figure out events correlation
if (this.correlationId !== FileChangesEvent.MIXED_CORRELATION) {
if (typeof change.cId === 'number') {
if (this.correlationId === undefined) {
this.correlationId = change.cId; // correlation not yet set, just take it
} else if (this.correlationId !== change.cId) {
this.correlationId = FileChangesEvent.MIXED_CORRELATION; // correlation mismatch, we have mixed correlation
}
} else {
if (this.correlationId !== undefined) {
this.correlationId = FileChangesEvent.MIXED_CORRELATION; // correlation mismatch, we have mixed correlation
}
}
}
}
}
private readonly added = new Lazy(() => {
const added = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
added.fill(this.rawAdded.map(resource => [resource, true]));
return added;
});
private readonly updated = new Lazy(() => {
const updated = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
updated.fill(this.rawUpdated.map(resource => [resource, true]));
return updated;
});
private readonly deleted = new Lazy(() => {
const deleted = TernarySearchTree.forUris<boolean>(() => this.ignorePathCasing);
deleted.fill(this.rawDeleted.map(resource => [resource, true]));
return deleted;
});
/**
* Find out if the file change events match the provided resource.
*
@ -966,33 +1033,33 @@ export class FileChangesEvent {
// Added
if (!hasTypesFilter || types.includes(FileChangeType.ADDED)) {
if (this.added?.get(resource)) {
if (this.added.value.get(resource)) {
return true;
}
if (options.includeChildren && this.added?.findSuperstr(resource)) {
if (options.includeChildren && this.added.value.findSuperstr(resource)) {
return true;
}
}
// Updated
if (!hasTypesFilter || types.includes(FileChangeType.UPDATED)) {
if (this.updated?.get(resource)) {
if (this.updated.value.get(resource)) {
return true;
}
if (options.includeChildren && this.updated?.findSuperstr(resource)) {
if (options.includeChildren && this.updated.value.findSuperstr(resource)) {
return true;
}
}
// Deleted
if (!hasTypesFilter || types.includes(FileChangeType.DELETED)) {
if (this.deleted?.findSubstr(resource) /* deleted also considers parent folders */) {
if (this.deleted.value.findSubstr(resource) /* deleted also considers parent folders */) {
return true;
}
if (options.includeChildren && this.deleted?.findSuperstr(resource)) {
if (options.includeChildren && this.deleted.value.findSuperstr(resource)) {
return true;
}
}
@ -1004,21 +1071,47 @@ export class FileChangesEvent {
* Returns if this event contains added files.
*/
gotAdded(): boolean {
return !!this.added;
return this.rawAdded.length > 0;
}
/**
* Returns if this event contains deleted files.
*/
gotDeleted(): boolean {
return !!this.deleted;
return this.rawDeleted.length > 0;
}
/**
* Returns if this event contains updated files.
*/
gotUpdated(): boolean {
return !!this.updated;
return this.rawUpdated.length > 0;
}
/**
* Returns if this event contains changes that correlate to the
* provided `correlationId`.
*
* File change event correlation is an advanced watch feature that
* allows to identify from which watch request the events originate
* from. This correlation allows to route events specifically
* only to the requestor and not emit them to all listeners.
*/
correlates(correlationId: number): boolean {
return this.correlationId === correlationId;
}
/**
* Figure out if the event contains changes that correlate to one
* correlation identifier.
*
* File change event correlation is an advanced watch feature that
* allows to identify from which watch request the events originate
* from. This correlation allows to route events specifically
* only to the requestor and not emit them to all listeners.
*/
hasCorrelation(): boolean {
return typeof this.correlationId === 'number';
}
/**

View file

@ -34,6 +34,13 @@ interface IWatchRequest {
* events.
*/
readonly includes?: Array<string | IRelativePattern>;
/**
* If provided, file change events from the watcher that
* are a result of this watch request will carry the same
* id.
*/
readonly correlationId?: number;
}
export interface INonRecursiveWatchRequest extends IWatchRequest {
@ -70,7 +77,7 @@ interface IWatcher {
* A normalized file change event from the raw events
* the watcher emits.
*/
readonly onDidChangeFile: Event<IDiskFileChange[]>;
readonly onDidChangeFile: Event<IFileChange[]>;
/**
* An event to indicate a message that should get logged.
@ -148,7 +155,7 @@ export abstract class AbstractWatcherClient extends Disposable {
private restartCounter = 0;
constructor(
private readonly onFileChanges: (changes: IDiskFileChange[]) => void,
private readonly onFileChanges: (changes: IFileChange[]) => void,
private readonly onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
private options: {
@ -234,7 +241,7 @@ export abstract class AbstractWatcherClient extends Disposable {
export abstract class AbstractNonRecursiveWatcherClient extends AbstractWatcherClient {
constructor(
onFileChanges: (changes: IDiskFileChange[]) => void,
onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
) {
@ -247,7 +254,7 @@ export abstract class AbstractNonRecursiveWatcherClient extends AbstractWatcherC
export abstract class AbstractUniversalWatcherClient extends AbstractWatcherClient {
constructor(
onFileChanges: (changes: IDiskFileChange[]) => void,
onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
) {
@ -257,24 +264,20 @@ export abstract class AbstractUniversalWatcherClient extends AbstractWatcherClie
protected abstract override createWatcher(disposables: DisposableStore): IUniversalWatcher;
}
export interface IDiskFileChange {
type: FileChangeType;
readonly resource: URI;
}
export interface ILogMessage {
readonly type: 'trace' | 'warn' | 'error' | 'info' | 'debug';
readonly message: string;
}
export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] {
export function reviveFileChanges(changes: IFileChange[]): IFileChange[] {
return changes.map(change => ({
type: change.type,
resource: URI.revive(change.resource)
resource: URI.revive(change.resource),
cId: change.cId
}));
}
export function coalesceEvents(changes: IDiskFileChange[]): IDiskFileChange[] {
export function coalesceEvents(changes: IFileChange[]): IFileChange[] {
// Build deltas
const coalescer = new EventCoalescer();
@ -312,10 +315,10 @@ export function parseWatcherPatterns(path: string, patterns: Array<string | IRel
class EventCoalescer {
private readonly coalesced = new Set<IDiskFileChange>();
private readonly mapPathToChange = new Map<string, IDiskFileChange>();
private readonly coalesced = new Set<IFileChange>();
private readonly mapPathToChange = new Map<string, IFileChange>();
private toKey(event: IDiskFileChange): string {
private toKey(event: IFileChange): string {
if (isLinux) {
return event.resource.fsPath;
}
@ -323,7 +326,7 @@ class EventCoalescer {
return event.resource.fsPath.toLowerCase(); // normalise to file system case sensitivity
}
processEvent(event: IDiskFileChange): void {
processEvent(event: IFileChange): void {
const existingEvent = this.mapPathToChange.get(this.toKey(event));
let keepEvent = false;
@ -370,8 +373,8 @@ class EventCoalescer {
}
}
coalesce(): IDiskFileChange[] {
const addOrChangeEvents: IDiskFileChange[] = [];
coalesce(): IFileChange[] {
const addOrChangeEvents: IFileChange[] = [];
const deletedPaths: string[] = [];
// This algorithm will remove all DELETE events up to the root folder

View file

@ -19,9 +19,9 @@ import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'
import { URI } from 'vs/base/common/uri';
import { IDirent, Promises, RimRafMode, SymlinkSupport } from 'vs/base/node/pfs';
import { localize } from 'vs/nls';
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability } from 'vs/platform/files/common/files';
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileChange } from 'vs/platform/files/common/files';
import { readFileIntoStream } from 'vs/platform/files/common/io';
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage } from 'vs/platform/files/common/watcher';
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage } from 'vs/platform/files/common/watcher';
import { ILogService } from 'vs/platform/log/common/log';
import { AbstractDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/common/diskFileSystemProvider';
import { toErrorMessage } from 'vs/base/common/errorMessage';
@ -812,7 +812,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
//#region File Watching
protected createUniversalWatcher(
onChange: (changes: IDiskFileChange[]) => void,
onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): AbstractUniversalWatcherClient {
@ -820,7 +820,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
}
protected createNonRecursiveWatcher(
onChange: (changes: IDiskFileChange[]) => void,
onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): AbstractNonRecursiveWatcherClient {

View file

@ -292,7 +292,8 @@ export abstract class AbstractSessionFileWatcher extends Disposable implements I
sessionEmitter.fire(
events.map(e => ({
resource: this.uriTransformer.transformOutgoingURI(e.resource),
type: e.type
type: e.type,
cId: e.cId
}))
);
}));

View file

@ -4,13 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IDiskFileChange, ILogMessage, AbstractNonRecursiveWatcherClient, INonRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { IFileChange } from 'vs/platform/files/common/files';
import { ILogMessage, AbstractNonRecursiveWatcherClient, INonRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher';
export class NodeJSWatcherClient extends AbstractNonRecursiveWatcherClient {
constructor(
onFileChanges: (changes: IDiskFileChange[]) => void,
onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
) {

View file

@ -7,7 +7,8 @@ import { Event, Emitter } from 'vs/base/common/event';
import { patternsEquals } from 'vs/base/common/glob';
import { Disposable } from 'vs/base/common/lifecycle';
import { isLinux } from 'vs/base/common/platform';
import { IDiskFileChange, ILogMessage, INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { IFileChange } from 'vs/platform/files/common/files';
import { ILogMessage, INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib';
export interface INodeJSWatcherInstance {
@ -25,7 +26,7 @@ export interface INodeJSWatcherInstance {
export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher {
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
private readonly _onDidChangeFile = this._register(new Emitter<IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
@ -61,7 +62,7 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher {
// Logging
if (requestsToStartWatching.length) {
this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : '<all>'})`).join(',')}`);
this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : '<all>'}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : '<none>'})`).join(',')}`);
}
if (pathsToStopWatching.length) {
@ -107,15 +108,22 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher {
}
private normalizeRequests(requests: INonRecursiveWatchRequest[]): INonRecursiveWatchRequest[] {
const requestsMap = new Map<string, INonRecursiveWatchRequest>();
const mapCorrelationtoRequests = new Map<number | undefined /* correlation */, Map<string, INonRecursiveWatchRequest>>();
// Ignore requests for the same paths
// Ignore requests for the same paths that have the same correlation
for (const request of requests) {
const path = isLinux ? request.path : request.path.toLowerCase(); // adjust for case sensitivity
requestsMap.set(path, request);
let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId);
if (!requestsForCorrelation) {
requestsForCorrelation = new Map<string, INonRecursiveWatchRequest>();
mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation);
}
requestsForCorrelation.set(path, request);
}
return Array.from(requestsMap.values());
return Array.from(mapCorrelationtoRequests.values()).map(requests => Array.from(requests.values())).flat();
}
async setVerboseLogging(enabled: boolean): Promise<void> {

View file

@ -11,11 +11,12 @@ import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/
import { normalizeNFC } from 'vs/base/common/normalization';
import { basename, dirname, join } from 'vs/base/common/path';
import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { realcase } from 'vs/base/node/extpath';
import { Promises } from 'vs/base/node/pfs';
import { FileChangeType } from 'vs/platform/files/common/files';
import { IDiskFileChange, ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
export class NodeJSFileWatcherLibrary extends Disposable {
@ -35,7 +36,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// recursive watcher because we can have many individual
// node.js watchers per request.
// (https://github.com/microsoft/vscode/issues/124723)
private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IDiskFileChange>(
private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IFileChange>(
{
maxWorkChunkSize: 100, // only process up to 100 changes at once before...
throttleDelay: 200, // ...resting for 200ms until we process events again...
@ -46,7 +47,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// Aggregate file changes over FILE_CHANGES_HANDLER_DELAY
// to coalesce events and reduce spam.
private readonly fileChangesAggregator = this._register(new RunOnceWorker<IDiskFileChange>(events => this.handleFileChanges(events), NodeJSFileWatcherLibrary.FILE_CHANGES_HANDLER_DELAY));
private readonly fileChangesAggregator = this._register(new RunOnceWorker<IFileChange>(events => this.handleFileChanges(events), NodeJSFileWatcherLibrary.FILE_CHANGES_HANDLER_DELAY));
private readonly excludes = parseWatcherPatterns(this.request.path, this.request.excludes);
private readonly includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined;
@ -57,7 +58,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
constructor(
private request: INonRecursiveWatchRequest,
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onDidFilesChange: (changes: IFileChange[]) => void,
private onLogMessage?: (msg: ILogMessage) => void,
private verboseLogging?: boolean
) {
@ -128,6 +129,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
const disposables = new DisposableStore();
try {
const requestResource = URI.file(this.request.path);
const pathBasename = basename(path);
// Creating watcher can fail with an exception
@ -261,7 +263,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
type = FileChangeType.DELETED;
}
this.onFileChange({ resource: URI.file(join(this.request.path, changedFileName)), type });
this.onFileChange({ resource: joinPath(requestResource, changedFileName), type, cId: this.request.correlationId });
}, NodeJSFileWatcherLibrary.FILE_DELETE_HANDLER_DELAY);
mapPathToStatDisposable.set(changedFileName, toDisposable(() => clearTimeout(timeoutHandle)));
@ -280,7 +282,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
folderChildren.add(changedFileName);
}
this.onFileChange({ resource: URI.file(join(this.request.path, changedFileName)), type });
this.onFileChange({ resource: joinPath(requestResource, changedFileName), type, cId: this.request.correlationId });
}
}
@ -319,14 +321,14 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// File still exists, so emit as change event and reapply the watcher
if (fileExists) {
this.onFileChange({ resource: URI.file(this.request.path), type: FileChangeType.UPDATED }, true /* skip excludes/includes (file is explicitly watched) */);
this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
disposables.add(await this.doWatch(path, false));
}
// File seems to be really gone, so emit a deleted event and dispose
else {
this.onFileChange({ resource: URI.file(this.request.path), type: FileChangeType.DELETED }, true /* skip excludes/includes (file is explicitly watched) */);
this.onFileChange({ resource: requestResource, type: FileChangeType.DELETED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
// Important to flush the event delivery
// before disposing the watcher, otherwise
@ -345,7 +347,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// File changed
else {
this.onFileChange({ resource: URI.file(this.request.path), type: FileChangeType.UPDATED }, true /* skip excludes/includes (file is explicitly watched) */);
this.onFileChange({ resource: requestResource, type: FileChangeType.UPDATED, cId: this.request.correlationId }, true /* skip excludes/includes (file is explicitly watched) */);
}
}
});
@ -361,7 +363,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
});
}
private onFileChange(event: IDiskFileChange, skipIncludeExcludeChecks = false): void {
private onFileChange(event: IFileChange, skipIncludeExcludeChecks = false): void {
if (this.cts.token.isCancellationRequested) {
return;
}
@ -385,7 +387,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
}
}
private handleFileChanges(fileChanges: IDiskFileChange[]): void {
private handleFileChanges(fileChanges: IFileChange[]): void {
// Coalesce events: merge events of same kind
const coalescedFileChanges = coalesceEvents(fileChanges);
@ -394,7 +396,7 @@ export class NodeJSFileWatcherLibrary extends Disposable {
// Logging
if (this.verboseLogging) {
for (const event of coalescedFileChanges) {
this.trace(`>> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`);
this.trace(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`);
}
}

View file

@ -20,8 +20,8 @@ import { dirname, normalize } from 'vs/base/common/path';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib';
import { FileChangeType } from 'vs/platform/files/common/files';
import { IDiskFileChange, ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
export interface IParcelWatcherInstance {
@ -49,7 +49,7 @@ export interface IParcelWatcherInstance {
/**
* An event aggregator to coalesce events and reduce duplicates.
*/
readonly worker: RunOnceWorker<IDiskFileChange>;
readonly worker: RunOnceWorker<IFileChange>;
/**
* Stops and disposes the watcher. This operation is async to await
@ -70,7 +70,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events';
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
private readonly _onDidChangeFile = this._register(new Emitter<IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
@ -95,7 +95,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
// Reduce likelyhood of spam from file events via throttling.
// (https://github.com/microsoft/vscode/issues/124723)
private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IDiskFileChange>(
private readonly throttledFileChangesEmitter = this._register(new ThrottledWorker<IFileChange>(
{
maxWorkChunkSize: 500, // only process up to 500 changes at once before...
throttleDelay: 200, // ...resting for 200ms until we process events again...
@ -150,7 +150,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
// Logging
if (requestsToStartWatching.length) {
this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : '<all>'})`).join(',')}`);
this.trace(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes.length > 0 ? request.excludes : '<none>'}, includes: ${request.includes && request.includes.length > 0 ? JSON.stringify(request.includes) : '<all>'}, correlationId: ${typeof request.correlationId === 'number' ? request.correlationId : '<none>'})`).join(',')}`);
}
if (pathsToStopWatching.length) {
@ -185,7 +185,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
ready: instance.p,
restarts,
token: cts.token,
worker: new RunOnceWorker<IDiskFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
worker: new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
stop: async () => {
cts.dispose(true);
@ -256,7 +256,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
ready: instance.p,
restarts,
token: cts.token,
worker: new RunOnceWorker<IDiskFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
worker: new RunOnceWorker<IFileChange>(events => this.handleParcelEvents(events, watcher), ParcelWatcher.FILE_CHANGES_HANDLER_DELAY),
stop: async () => {
cts.dispose(true);
@ -315,7 +315,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength);
// Check for includes
const includedEvents = this.handleIncludes(parcelEvents, includes);
const includedEvents = this.handleIncludes(watcher, parcelEvents, includes);
// Add to event aggregator for later processing
for (const includedEvent of includedEvents) {
@ -323,8 +323,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
}
}
private handleIncludes(parcelEvents: parcelWatcher.Event[], includes: ParsedPattern[] | undefined): IDiskFileChange[] {
const events: IDiskFileChange[] = [];
private handleIncludes(watcher: IParcelWatcherInstance, parcelEvents: parcelWatcher.Event[], includes: ParsedPattern[] | undefined): IFileChange[] {
const events: IFileChange[] = [];
for (const { path, type: parcelEventType } of parcelEvents) {
const type = ParcelWatcher.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(parcelEventType)!;
@ -338,14 +338,14 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
this.trace(` >> ignored (not included) ${path}`);
}
} else {
events.push({ type, resource: URI.file(path) });
events.push({ type, resource: URI.file(path), cId: watcher.request.correlationId });
}
}
return events;
}
private handleParcelEvents(parcelEvents: IDiskFileChange[], watcher: IParcelWatcherInstance): void {
private handleParcelEvents(parcelEvents: IFileChange[], watcher: IParcelWatcherInstance): void {
// Coalesce events: merge events of same kind
const coalescedEvents = coalesceEvents(parcelEvents);
@ -354,7 +354,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
const { events: filteredEvents, rootDeleted } = this.filterEvents(coalescedEvents, watcher);
// Broadcast to clients
this.emitEvents(filteredEvents);
this.emitEvents(filteredEvents, watcher);
// Handle root path deletes
if (rootDeleted) {
@ -362,7 +362,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
}
}
private emitEvents(events: IDiskFileChange[]): void {
private emitEvents(events: IFileChange[], watcher: IParcelWatcherInstance): void {
if (events.length === 0) {
return;
}
@ -370,7 +370,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
// Logging
if (this.verboseLogging) {
for (const event of events) {
this.trace(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`);
const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`;
this.trace(typeof watcher.request.correlationId === 'number' ? `${traceMsg} (correlationId: ${watcher.request.correlationId})` : traceMsg);
}
}
@ -440,8 +441,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
}
}
private filterEvents(events: IDiskFileChange[], watcher: IParcelWatcherInstance): { events: IDiskFileChange[]; rootDeleted?: boolean } {
const filteredEvents: IDiskFileChange[] = [];
private filterEvents(events: IFileChange[], watcher: IParcelWatcherInstance): { events: IFileChange[]; rootDeleted?: boolean } {
const filteredEvents: IFileChange[] = [];
let rootDeleted = false;
for (const event of events) {
@ -467,7 +468,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
const parentPath = dirname(watcher.request.path);
if (existsSync(parentPath)) {
const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false }, changes => {
const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, changes => {
if (watcher.token.isCancellationRequested) {
return; // return early when disposed
}
@ -569,62 +570,86 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
}
protected normalizeRequests(requests: IRecursiveWatchRequest[], validatePaths = true): IRecursiveWatchRequest[] {
const requestTrie = TernarySearchTree.forPaths<IRecursiveWatchRequest>(!isLinux);
// Sort requests by path length to have shortest first
// to have a way to prevent children to be watched if
// parents exist.
requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length);
// Only consider requests for watching that are not
// a child of an existing request path to prevent
// duplication. In addition, drop any request where
// everything is excluded (via `**` glob).
//
// However, allow explicit requests to watch folders
// that are symbolic links because the Parcel watcher
// does not allow to recursively watch symbolic links.
// Map request paths to correlation and ignore identical paths
const mapCorrelationtoRequests = new Map<number | undefined /* correlation */, Map<string, IRecursiveWatchRequest>>();
for (const request of requests) {
if (request.excludes.includes(GLOBSTAR)) {
continue; // path is ignored entirely (via `**` glob exclude)
}
// Check for overlapping requests
if (requestTrie.findSubstr(request.path)) {
try {
const realpath = realpathSync(request.path);
if (realpath === request.path) {
this.trace(`ignoring a path for watching who's parent is already watched: ${request.path}`);
const path = isLinux ? request.path : request.path.toLowerCase(); // adjust for case sensitivity
continue;
}
} catch (error) {
this.trace(`ignoring a path for watching who's realpath failed to resolve: ${request.path} (error: ${error})`);
continue;
}
let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId);
if (!requestsForCorrelation) {
requestsForCorrelation = new Map<string, IRecursiveWatchRequest>();
mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation);
}
// Check for invalid paths
if (validatePaths) {
try {
const stat = statSync(request.path);
if (!stat.isDirectory()) {
this.trace(`ignoring a path for watching that is a file and not a folder: ${request.path}`);
continue;
}
} catch (error) {
this.trace(`ignoring a path for watching who's stat info failed to resolve: ${request.path} (error: ${error})`);
continue;
}
}
requestTrie.set(request.path, request);
requestsForCorrelation.set(path, request);
}
return Array.from(requestTrie).map(([, request]) => request);
const normalizedRequests: IRecursiveWatchRequest[] = [];
for (const requestsForCorrelation of mapCorrelationtoRequests.values()) {
// Only consider requests for watching that are not
// a child of an existing request path to prevent
// duplication. In addition, drop any request where
// everything is excluded (via `**` glob).
//
// However, allow explicit requests to watch folders
// that are symbolic links because the Parcel watcher
// does not allow to recursively watch symbolic links.
const requestTrie = TernarySearchTree.forPaths<IRecursiveWatchRequest>(!isLinux);
for (const request of requestsForCorrelation.values()) {
// Check for overlapping requests
if (requestTrie.findSubstr(request.path)) {
try {
const realpath = realpathSync(request.path);
if (realpath === request.path) {
this.trace(`ignoring a path for watching who's parent is already watched: ${request.path}`);
continue;
}
} catch (error) {
this.trace(`ignoring a path for watching who's realpath failed to resolve: ${request.path} (error: ${error})`);
continue;
}
}
// Check for invalid paths
if (validatePaths) {
try {
const stat = statSync(request.path);
if (!stat.isDirectory()) {
this.trace(`ignoring a path for watching that is a file and not a folder: ${request.path}`);
continue;
}
} catch (error) {
this.trace(`ignoring a path for watching who's stat info failed to resolve: ${request.path} (error: ${error})`);
continue;
}
}
requestTrie.set(request.path, request);
}
normalizedRequests.push(...Array.from(requestTrie).map(([, request]) => request));
}
return normalizedRequests;
}
async setVerboseLogging(enabled: boolean): Promise<void> {

View file

@ -7,12 +7,13 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network';
import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage, IUniversalWatcher } from 'vs/platform/files/common/watcher';
import { IFileChange } from 'vs/platform/files/common/files';
import { AbstractUniversalWatcherClient, ILogMessage, IUniversalWatcher } from 'vs/platform/files/common/watcher';
export class UniversalWatcherClient extends AbstractUniversalWatcherClient {
constructor(
onFileChanges: (changes: IDiskFileChange[]) => void,
onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
) {

View file

@ -7,12 +7,13 @@ import * as assert from 'assert';
import { DeferredPromise, timeout } from 'vs/base/common/async';
import { bufferToReadable, bufferToStream, VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isEqual } from 'vs/base/common/resources';
import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { IFileOpenOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileType, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IStat, IFileAtomicReadOptions, IFileAtomicWriteOptions, IFileAtomicDeleteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileAtomicOptions } from 'vs/platform/files/common/files';
import { IFileOpenOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileType, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IStat, IFileAtomicReadOptions, IFileAtomicWriteOptions, IFileAtomicDeleteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileSystemProviderWithFileAtomicWriteCapability, IFileAtomicOptions, IFileChange, isFileSystemWatcher, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
import { NullLogService } from 'vs/platform/log/common/log';
@ -142,6 +143,58 @@ suite('File Service', () => {
service.dispose();
});
test('watch - with corelation', async () => {
const service = disposables.add(new FileService(new NullLogService()));
const provider = new class extends NullFileSystemProvider {
private readonly _testOnDidChangeFile = new Emitter<readonly IFileChange[]>();
override readonly onDidChangeFile: Event<readonly IFileChange[]> = this._testOnDidChangeFile.event;
fireFileChange(changes: readonly IFileChange[]) {
this._testOnDidChangeFile.fire(changes);
}
};
disposables.add(service.registerProvider('test', provider));
await service.activateProvider('test');
const globalEvents: FileChangesEvent[] = [];
disposables.add(service.onDidFilesChange(e => {
globalEvents.push(e);
}));
const watcher0 = disposables.add(service.watch(URI.parse('test://watch/folder1'), { recursive: true, excludes: [], includes: [] }));
assert.strictEqual(isFileSystemWatcher(watcher0), false);
const watcher1 = disposables.add(service.watch(URI.parse('test://watch/folder2'), { recursive: true, excludes: [], includes: [], correlationId: 100 }));
assert.strictEqual(isFileSystemWatcher(watcher1), true);
const watcher2 = disposables.add(service.watch(URI.parse('test://watch/folder3'), { recursive: true, excludes: [], includes: [], correlationId: 200 }));
assert.strictEqual(isFileSystemWatcher(watcher2), true);
const watcher1Events: FileChangesEvent[] = [];
disposables.add(watcher1.onDidChange(e => {
watcher1Events.push(e);
}));
const watcher2Events: FileChangesEvent[] = [];
disposables.add(watcher2.onDidChange(e => {
watcher2Events.push(e);
}));
provider.fireFileChange([{ resource: URI.parse('test://watch/folder1'), type: FileChangeType.ADDED }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder2'), type: FileChangeType.ADDED, cId: 100 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder2'), type: FileChangeType.ADDED, cId: 100 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder3/file'), type: FileChangeType.UPDATED, cId: 200 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder3'), type: FileChangeType.UPDATED, cId: 200 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 50 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 60 }]);
provider.fireFileChange([{ resource: URI.parse('test://watch/folder4'), type: FileChangeType.ADDED, cId: 70 }]);
assert.strictEqual(globalEvents.length, 1);
assert.strictEqual(watcher1Events.length, 2);
assert.strictEqual(watcher2Events.length, 2);
});
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060) - async', async () => {
testReadErrorBubbles(true);
});

View file

@ -8,7 +8,7 @@ import { isEqual, isEqualOrParent } from 'vs/base/common/extpath';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils';
import { FileChangesEvent, FileChangeType, isParent } from 'vs/platform/files/common/files';
import { FileChangesEvent, FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files';
suite('Files', () => {
@ -109,6 +109,51 @@ suite('Files', () => {
}
});
test('FileChangesEvent - correlation', function () {
let changes: IFileChange[] = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED },
];
let event: FileChangesEvent = new FileChangesEvent(changes, true);
assert.strictEqual(event.hasCorrelation(), false);
assert.strictEqual(event.correlates(100), false);
changes = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED, cId: 100 },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED, cId: 100 },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED, cId: 100 },
];
event = new FileChangesEvent(changes, true);
assert.strictEqual(event.hasCorrelation(), true);
assert.strictEqual(event.correlates(100), true);
assert.strictEqual(event.correlates(120), false);
changes = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED, cId: 100 },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED, cId: 100 },
];
event = new FileChangesEvent(changes, true);
assert.strictEqual(event.hasCorrelation(), false);
assert.strictEqual(event.correlates(100), false);
assert.strictEqual(event.correlates(120), false);
changes = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED, cId: 100 },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED, cId: 120 },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED, cId: 100 },
];
event = new FileChangesEvent(changes, true);
assert.strictEqual(event.hasCorrelation(), false);
assert.strictEqual(event.correlates(100), false);
assert.strictEqual(event.correlates(120), false);
});
function testIsEqual(testMethod: (pA: string, pB: string, ignoreCase: boolean) => boolean): void {
// corner cases

View file

@ -11,7 +11,7 @@ import { isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { IDiskFileChange, coalesceEvents, toFileChanges, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
import { coalesceEvents, reviveFileChanges, parseWatcherPatterns } from 'vs/platform/files/common/watcher';
class TestFileWatcher extends Disposable {
private readonly _onDidFilesChange: Emitter<{ raw: IFileChange[]; event: FileChangesEvent }>;
@ -26,23 +26,23 @@ class TestFileWatcher extends Disposable {
return this._onDidFilesChange.event;
}
report(changes: IDiskFileChange[]): void {
report(changes: IFileChange[]): void {
this.onRawFileEvents(changes);
}
private onRawFileEvents(events: IDiskFileChange[]): void {
private onRawFileEvents(events: IFileChange[]): void {
// Coalesce
const coalescedEvents = coalesceEvents(events);
// Emit through event emitter
if (coalescedEvents.length > 0) {
this._onDidFilesChange.fire({ raw: toFileChanges(coalescedEvents), event: this.toFileChangesEvent(coalescedEvents) });
this._onDidFilesChange.fire({ raw: reviveFileChanges(coalescedEvents), event: this.toFileChangesEvent(coalescedEvents) });
}
}
private toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
return new FileChangesEvent(toFileChanges(changes), !isLinux);
private toFileChangesEvent(changes: IFileChange[]): FileChangesEvent {
return new FileChangesEvent(reviveFileChanges(changes), !isLinux);
}
}
@ -126,7 +126,7 @@ suite('Watcher Events Normalizer', () => {
const updated = URI.file('/users/data/src/updated.txt');
const deleted = URI.file('/users/data/src/deleted.txt');
const raw: IDiskFileChange[] = [
const raw: IFileChange[] = [
{ resource: added, type: FileChangeType.ADDED },
{ resource: updated, type: FileChangeType.UPDATED },
{ resource: deleted, type: FileChangeType.DELETED },
@ -159,7 +159,7 @@ suite('Watcher Events Normalizer', () => {
const addedFile = URI.file(path === Path.UNIX ? '/users/data/src/added.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt');
const updatedFile = URI.file(path === Path.UNIX ? '/users/data/src/updated.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt');
const raw: IDiskFileChange[] = [
const raw: IFileChange[] = [
{ resource: deletedFolderA, type: FileChangeType.DELETED },
{ resource: deletedFolderB, type: FileChangeType.DELETED },
{ resource: deletedFolderBF1, type: FileChangeType.DELETED },
@ -194,7 +194,7 @@ suite('Watcher Events Normalizer', () => {
const deleted = URI.file('/users/data/src/related');
const unrelated = URI.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [
const raw: IFileChange[] = [
{ resource: created, type: FileChangeType.ADDED },
{ resource: deleted, type: FileChangeType.DELETED },
{ resource: unrelated, type: FileChangeType.UPDATED },
@ -219,7 +219,7 @@ suite('Watcher Events Normalizer', () => {
const created = URI.file('/users/data/src/related');
const unrelated = URI.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [
const raw: IFileChange[] = [
{ resource: deleted, type: FileChangeType.DELETED },
{ resource: created, type: FileChangeType.ADDED },
{ resource: unrelated, type: FileChangeType.UPDATED },
@ -245,7 +245,7 @@ suite('Watcher Events Normalizer', () => {
const updated = URI.file('/users/data/src/related');
const unrelated = URI.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [
const raw: IFileChange[] = [
{ resource: created, type: FileChangeType.ADDED },
{ resource: updated, type: FileChangeType.UPDATED },
{ resource: unrelated, type: FileChangeType.UPDATED },
@ -273,7 +273,7 @@ suite('Watcher Events Normalizer', () => {
const deleted = URI.file('/users/data/src/related');
const unrelated = URI.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [
const raw: IFileChange[] = [
{ resource: updated, type: FileChangeType.UPDATED },
{ resource: updated2, type: FileChangeType.UPDATED },
{ resource: unrelated, type: FileChangeType.UPDATED },
@ -300,7 +300,7 @@ suite('Watcher Events Normalizer', () => {
const oldPath = URI.file('/users/data/src/added');
const newPath = URI.file('/users/data/src/ADDED');
const raw: IDiskFileChange[] = [
const raw: IFileChange[] = [
{ resource: newPath, type: FileChangeType.ADDED },
{ resource: oldPath, type: FileChangeType.DELETED }
];

View file

@ -17,13 +17,16 @@ import { DeferredPromise } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcher';
import { FileAccess } from 'vs/base/common/network';
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { addUNCHostToAllowlist } from 'vs/base/node/unc';
// this suite has shown flaky runs in Azure pipelines where
// tasks would just hang and timeout after a while (not in
// mocha but generally). as such they will run only on demand
// whenever we update the watcher library.
((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (node.js)', () => {
flakySuite('File Watcher (node.js)', () => {
class TestNodeJSWatcher extends NodeJSWatcher {
@ -105,16 +108,22 @@ import { FileAccess } from 'vs/base/common/network';
}
}
async function awaitEvent(service: TestNodeJSWatcher, path: string, type: FileChangeType): Promise<void> {
async function awaitEvent(service: TestNodeJSWatcher, path: string, type: FileChangeType, correlationId?: number | null, expectedCount?: number): Promise<void> {
if (loggingEnabled) {
console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`);
}
// Await the event
await new Promise<void>(resolve => {
let counter = 0;
const disposable = service.onDidChangeFile(events => {
for (const event of events) {
if (event.resource.fsPath === path && event.type === type) {
if (extUriBiasedIgnorePathCase.isEqual(event.resource, URI.file(path)) && event.type === type && (correlationId === null || event.cId === correlationId)) {
counter++;
if (typeof expectedCount === 'number' && counter < expectedCount) {
continue; // not yet
}
disposable.dispose();
resolve();
break;
@ -406,6 +415,13 @@ import { FileAccess } from 'vs/base/common/network';
return basicCrudTest(join(testDir, 'files-includes.txt'));
});
test('correlationId is supported', async function () {
const correlationId = Math.random();
await watcher.watch([{ correlationId, path: testDir, excludes: [], recursive: false }]);
return basicCrudTest(join(testDir, 'newFile.txt'), undefined, correlationId);
});
(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (folder watch)', async function () {
const link = join(testDir, 'deep-linked');
const linkTarget = join(testDir, 'deep');
@ -416,23 +432,23 @@ import { FileAccess } from 'vs/base/common/network';
return basicCrudTest(join(link, 'newFile.txt'));
});
async function basicCrudTest(filePath: string, skipAdd?: boolean): Promise<void> {
async function basicCrudTest(filePath: string, skipAdd?: boolean, correlationId?: number | null, expectedCount?: number): Promise<void> {
let changeFuture: Promise<unknown>;
// New file
if (!skipAdd) {
changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED);
changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, correlationId, expectedCount);
await Promises.writeFile(filePath, 'Hello World');
await changeFuture;
}
// Change file
changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED);
changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, correlationId, expectedCount);
await Promises.writeFile(filePath, 'Hello Change');
await changeFuture;
// Delete file
changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED);
changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, correlationId, expectedCount);
await Promises.unlink(await Promises.realpath(filePath)); // support symlinks
await changeFuture;
}
@ -448,6 +464,7 @@ import { FileAccess } from 'vs/base/common/network';
});
(!isWindows /* UNC is windows only */ ? test.skip : test)('unc support (folder watch)', async function () {
addUNCHostToAllowlist('localhost');
// Local UNC paths are in the form of: \\localhost\c$\my_dir
const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}`;
@ -458,6 +475,7 @@ import { FileAccess } from 'vs/base/common/network';
});
(!isWindows /* UNC is windows only */ ? test.skip : test)('unc support (file watch)', async function () {
addUNCHostToAllowlist('localhost');
// Local UNC paths are in the form of: \\localhost\c$\my_dir
const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}\\lorem.txt`;
@ -528,4 +546,17 @@ import { FileAccess } from 'vs/base/common/network';
return watchPromise;
});
test('watching same or overlapping paths supported when correlation is applied', async () => {
// same path, same options
await watcher.watch([
{ path: testDir, excludes: [], recursive: false, correlationId: 1 },
{ path: testDir, excludes: [], recursive: false, correlationId: 2, },
{ path: testDir, excludes: [], recursive: false, correlationId: undefined }
]);
await basicCrudTest(join(testDir, 'newFile.txt'), undefined, null, 3);
await basicCrudTest(join(testDir, 'otherNewFile.txt'), undefined, null, 3);
});
});

View file

@ -7,23 +7,26 @@ import * as assert from 'assert';
import { realpathSync } from 'fs';
import { tmpdir } from 'os';
import { timeout } from 'vs/base/common/async';
import { join } from 'vs/base/common/path';
import { dirname, join } from 'vs/base/common/path';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { Promises, RimRafMode } from 'vs/base/node/pfs';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { FileChangeType } from 'vs/platform/files/common/files';
import { FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { ParcelWatcher } from 'vs/platform/files/node/watcher/parcel/parcelWatcher';
import { IDiskFileChange, IRecursiveWatchRequest } from 'vs/platform/files/common/watcher';
import { IRecursiveWatchRequest } from 'vs/platform/files/common/watcher';
import { getDriveLetter } from 'vs/base/common/extpath';
import { ltrim } from 'vs/base/common/strings';
import { FileAccess } from 'vs/base/common/network';
import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { addUNCHostToAllowlist } from 'vs/base/node/unc';
// this suite has shown flaky runs in Azure pipelines where
// tasks would just hang and timeout after a while (not in
// mocha but generally). as such they will run only on demand
// whenever we update the watcher library.
((process.env['BUILD_SOURCEVERSION'] || process.env['CI']) ? suite.skip : flakySuite)('File Watcher (parcel)', () => {
flakySuite('File Watcher (parcel)', () => {
class TestParcelWatcher extends ParcelWatcher {
@ -103,16 +106,22 @@ import { FileAccess } from 'vs/base/common/network';
}
}
async function awaitEvent(service: TestParcelWatcher, path: string, type: FileChangeType, failOnEventReason?: string): Promise<IDiskFileChange[]> {
async function awaitEvent(watcher: TestParcelWatcher, path: string, type: FileChangeType, failOnEventReason?: string, correlationId?: number | null, expectedCount?: number): Promise<IFileChange[]> {
if (loggingEnabled) {
console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`);
}
// Await the event
const res = await new Promise<IDiskFileChange[]>((resolve, reject) => {
const disposable = service.onDidChangeFile(events => {
const res = await new Promise<IFileChange[]>((resolve, reject) => {
let counter = 0;
const disposable = watcher.onDidChangeFile(events => {
for (const event of events) {
if (event.resource.fsPath === path && event.type === type) {
if (extUriBiasedIgnorePathCase.isEqual(event.resource, URI.file(path)) && event.type === type && (correlationId === null || event.cId === correlationId)) {
counter++;
if (typeof expectedCount === 'number' && counter < expectedCount) {
continue; // not yet
}
disposable.dispose();
if (failOnEventReason) {
reject(new Error(`Unexpected file event: ${failOnEventReason}`));
@ -134,14 +143,14 @@ import { FileAccess } from 'vs/base/common/network';
return res;
}
function awaitMessage(service: TestParcelWatcher, type: 'trace' | 'warn' | 'error' | 'info' | 'debug'): Promise<void> {
function awaitMessage(watcher: TestParcelWatcher, type: 'trace' | 'warn' | 'error' | 'info' | 'debug'): Promise<void> {
if (loggingEnabled) {
console.log(`Awaiting message of type ${type}`);
}
// Await the message
return new Promise<void>(resolve => {
const disposable = service.onDidLogMessage(msg => {
const disposable = watcher.onDidLogMessage(msg => {
if (msg.type === type) {
disposable.dispose();
resolve();
@ -287,20 +296,20 @@ import { FileAccess } from 'vs/base/common/network';
return basicCrudTest(join(testDir, 'deep', 'newFile.txt'));
});
async function basicCrudTest(filePath: string): Promise<void> {
async function basicCrudTest(filePath: string, correlationId?: number | null, expectedCount?: number): Promise<void> {
// New file
let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED);
let changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, undefined, correlationId, expectedCount);
await Promises.writeFile(filePath, 'Hello World');
await changeFuture;
// Change file
changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED);
changeFuture = awaitEvent(watcher, filePath, FileChangeType.UPDATED, undefined, correlationId, expectedCount);
await Promises.writeFile(filePath, 'Hello Change');
await changeFuture;
// Delete file
changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED);
changeFuture = awaitEvent(watcher, filePath, FileChangeType.DELETED, undefined, correlationId, expectedCount);
await Promises.unlink(filePath);
await changeFuture;
}
@ -509,6 +518,7 @@ import { FileAccess } from 'vs/base/common/network';
});
(!isWindows /* UNC is windows only */ ? test.skip : test)('unc support', async function () {
addUNCHostToAllowlist('localhost');
// Local UNC paths are in the form of: \\localhost\c$\my_dir
const uncPath = `\\\\localhost\\${getDriveLetter(testDir)?.toLowerCase()}$\\${ltrim(testDir.substr(testDir.indexOf(':') + 1), '\\')}`;
@ -532,7 +542,7 @@ import { FileAccess } from 'vs/base/common/network';
await watcher.watch([{ path: invalidPath, excludes: [], recursive: true }]);
});
test('deleting watched path is handled properly', async function () {
(isWindows /* flaky on windows */ ? test.skip : test)('deleting watched path is handled properly', async function () {
const watchedPath = join(testDir, 'deep');
await watcher.watch([{ path: watchedPath, excludes: [], recursive: true }]);
@ -555,6 +565,13 @@ import { FileAccess } from 'vs/base/common/network';
await changeFuture;
});
test('correlationId is supported', async function () {
const correlationId = Math.random();
await watcher.watch([{ correlationId, path: testDir, excludes: [], recursive: true }]);
return basicCrudTest(join(testDir, 'newFile.txt'), correlationId);
});
test('should not exclude roots that do not overlap', () => {
if (isWindows) {
assert.deepStrictEqual(watcher.testNormalizePaths(['C:\\a']), ['C:\\a']);
@ -584,4 +601,49 @@ import { FileAccess } from 'vs/base/common/network';
test('should ignore when everything excluded', () => {
assert.deepStrictEqual(watcher.testNormalizePaths(['/foo/bar', '/bar'], ['**', 'something']), []);
});
test('watching same or overlapping paths supported when correlation is applied', async () => {
// same path, same options
await watcher.watch([
{ path: testDir, excludes: [], recursive: true, correlationId: 1 },
{ path: testDir, excludes: [], recursive: true, correlationId: 2, },
{ path: testDir, excludes: [], recursive: true, correlationId: undefined }
]);
await basicCrudTest(join(testDir, 'newFile.txt'), null, 3);
await basicCrudTest(join(testDir, 'otherNewFile.txt'), null, 3);
// same path, different options
await watcher.watch([
{ path: testDir, excludes: [], recursive: true, correlationId: 1 },
{ path: testDir, excludes: [], recursive: true, correlationId: 2 },
{ path: testDir, excludes: [], recursive: true, correlationId: undefined },
{ path: testDir, excludes: [join(realpathSync(testDir), 'deep')], recursive: true, correlationId: 3 },
{ path: testDir, excludes: [join(realpathSync(testDir), 'other')], recursive: true, correlationId: 4 },
]);
await basicCrudTest(join(testDir, 'newFile.txt'), null, 5);
await basicCrudTest(join(testDir, 'otherNewFile.txt'), null, 5);
// overlapping paths (same options)
await watcher.watch([
{ path: dirname(testDir), excludes: [], recursive: true, correlationId: 1 },
{ path: testDir, excludes: [], recursive: true, correlationId: 2 },
{ path: join(testDir, 'deep'), excludes: [], recursive: true, correlationId: 3 },
]);
await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3);
await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3);
// overlapping paths (different options)
await watcher.watch([
{ path: dirname(testDir), excludes: [], recursive: true, correlationId: 1 },
{ path: testDir, excludes: [join(realpathSync(testDir), 'some')], recursive: true, correlationId: 2 },
{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'other')], recursive: true, correlationId: 3 },
]);
await basicCrudTest(join(testDir, 'deep', 'newFile.txt'), null, 3);
await basicCrudTest(join(testDir, 'deep', 'otherNewFile.txt'), null, 3);
});
});

View file

@ -156,7 +156,8 @@ export class FileUserDataProvider extends Disposable implements
if (this.watchResources.findSubstr(userDataResource)) {
userDataChanges.push({
resource: userDataResource,
type: change.type
type: change.type,
cId: change.cId
});
}
}

View file

@ -6,17 +6,10 @@
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable, toDisposable, DisposableStore, DisposableMap } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IFileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, IFileOverwriteOptions, IFileDeleteOptions, IFileOpenOptions, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability, FilePermission, toFileSystemProviderErrorCode, IFilesConfiguration, IFileStatWithPartialMetadata, IFileStat } from 'vs/platform/files/common/files';
import { IFileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, IFileOverwriteOptions, IFileDeleteOptions, IFileOpenOptions, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability, FilePermission, toFileSystemProviderErrorCode, IFileStatWithPartialMetadata, IFileStat } from 'vs/platform/files/common/files';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { ExtHostContext, ExtHostFileSystemShape, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../common/extHost.protocol';
import { VSBuffer } from 'vs/base/common/buffer';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files';
import { normalizeWatcherPattern } from 'vs/platform/files/common/watcher';
import { GLOBSTAR } from 'vs/base/common/glob';
import { rtrim } from 'vs/base/common/strings';
import { IMarkdownString } from 'vs/base/common/htmlContent';
@extHostNamedCustomer(MainContext.MainThreadFileSystem)
@ -25,14 +18,10 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape {
private readonly _proxy: ExtHostFileSystemShape;
private readonly _fileProvider = new DisposableMap<number, RemoteFileSystemProvider>();
private readonly _disposables = new DisposableStore();
private readonly _watches = new DisposableMap<number>();
constructor(
extHostContext: IExtHostContext,
@IWorkbenchFileService private readonly _fileService: IWorkbenchFileService,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@ILogService private readonly _logService: ILogService,
@IConfigurationService private readonly _configurationService: IConfigurationService
@IFileService private readonly _fileService: IFileService
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystem);
@ -48,7 +37,6 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape {
dispose(): void {
this._disposables.dispose();
this._fileProvider.dispose();
this._watches.dispose();
}
async $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: IMarkdownString): Promise<void> {
@ -165,99 +153,7 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape {
return this._fileService.activateProvider(scheme);
}
async $watch(extensionId: string, session: number, resource: UriComponents, unvalidatedOpts: IWatchOptions): Promise<void> {
const uri = URI.revive(resource);
const workspaceFolder = this._contextService.getWorkspaceFolder(uri);
const opts = { ...unvalidatedOpts };
// Convert a recursive watcher to a flat watcher if the path
// turns out to not be a folder. Recursive watching is only
// possible on folders, so we help all file watchers by checking
// early.
if (opts.recursive) {
try {
const stat = await this._fileService.stat(uri);
if (!stat.isDirectory) {
opts.recursive = false;
}
} catch (error) {
this._logService.error(`MainThreadFileSystem#$watch(): failed to stat a resource for file watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session}): ${error}`);
}
}
// Refuse to watch anything that is already watched via
// our workspace watchers in case the request is a
// recursive file watcher.
// Still allow for non-recursive watch requests as a way
// to bypass configured exclude rules though
// (see https://github.com/microsoft/vscode/issues/146066)
if (workspaceFolder && opts.recursive) {
this._logService.trace(`MainThreadFileSystem#$watch(): ignoring request to start watching because path is inside workspace (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
return;
}
this._logService.trace(`MainThreadFileSystem#$watch(): request to start watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
// 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) {
const config = this._configurationService.getValue<IFilesConfiguration>();
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (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
// `<workspace path>/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 (workspaceFolder) {
const config = this._configurationService.getValue<IFilesConfiguration>();
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (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(`MainThreadFileSystem#$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;
}
}
const subscription = this._fileService.watch(uri, opts);
this._watches.set(session, subscription);
}
$unwatch(session: number): void {
if (this._watches.has(session)) {
this._logService.trace(`MainThreadFileSystem#$unwatch(): request to stop watching (session: ${session})`);
this._watches.deleteAndDispose(session);
}
}
}
class RemoteFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileFolderCopyCapability {

View file

@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle';
import { FileOperation, IFileService } from 'vs/platform/files/common/files';
import { extHostCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { ExtHostContext } from '../common/extHost.protocol';
import { DisposableMap, DisposableStore } from 'vs/base/common/lifecycle';
import { FileOperation, IFileService, IFilesConfiguration, IWatchOptions } from 'vs/platform/files/common/files';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { ExtHostContext, ExtHostFileSystemEventServiceShape, MainContext, MainThreadFileSystemEventServiceShape } from '../common/extHost.protocol';
import { localize } from 'vs/nls';
import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
@ -22,17 +22,26 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { reviveWorkspaceEditDto } from 'vs/workbench/api/browser/mainThreadBulkEdits';
import { GLOBSTAR } from 'vs/base/common/glob';
import { rtrim } from 'vs/base/common/strings';
import { UriComponents, URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { normalizeWatcherPattern } from 'vs/platform/files/common/watcher';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@extHostCustomer
export class MainThreadFileSystemEventService {
@extHostNamedCustomer(MainContext.MainThreadFileSystemEventService)
export class MainThreadFileSystemEventService implements MainThreadFileSystemEventServiceShape {
static readonly MementoKeyAdditionalEdits = `file.particpants.additionalEdits`;
private readonly _proxy: ExtHostFileSystemEventServiceShape;
private readonly _listener = new DisposableStore();
private readonly _watches = new DisposableMap<number>();
constructor(
extHostContext: IExtHostContext,
@IFileService fileService: IFileService,
@IFileService private readonly _fileService: IFileService,
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
@IBulkEditService bulkEditService: IBulkEditService,
@IProgressService progressService: IProgressService,
@ -40,19 +49,22 @@ export class MainThreadFileSystemEventService {
@IStorageService storageService: IStorageService,
@ILogService logService: ILogService,
@IEnvironmentService envService: IEnvironmentService,
@IUriIdentityService uriIdentService: IUriIdentityService
@IUriIdentityService uriIdentService: IUriIdentityService,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@ILogService private readonly _logService: ILogService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService);
const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService);
this._listener.add(fileService.onDidFilesChange(event => {
proxy.$onFileEvent({
this._listener.add(_fileService.onDidFilesChange(event => {
this._proxy.$onFileEvent({
created: event.rawAdded,
changed: event.rawUpdated,
deleted: event.rawDeleted
});
}));
const that = this;
const fileOperationParticipant = new class implements IWorkingCopyFileOperationParticipant {
async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, timeout: number, token: CancellationToken) {
if (undoInfo?.isUndoing) {
@ -69,7 +81,7 @@ export class MainThreadFileSystemEventService {
delay: Math.min(timeout / 2, 3000)
}, () => {
// race extension host event delivery against timeout AND user-cancel
const onWillEvent = proxy.$onWillRunFileOperation(operation, files, timeout, cts.token);
const onWillEvent = that._proxy.$onWillRunFileOperation(operation, files, timeout, cts.token);
return raceCancellation(onWillEvent, cts.token);
}, () => {
// user-cancel
@ -197,11 +209,132 @@ export class MainThreadFileSystemEventService {
this._listener.add(workingCopyFileService.addFileOperationParticipant(fileOperationParticipant));
// AFTER file operation
this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files)));
this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => this._proxy.$onDidRunFileOperation(e.operation, e.files)));
}
async $watch(extensionId: string, session: number, resource: UriComponents, unvalidatedOpts: IWatchOptions): Promise<void> {
const uri = URI.revive(resource);
const correlate = Array.isArray(unvalidatedOpts?.excludes) && unvalidatedOpts.excludes.length > 0; // TODO@bpasero for now only correlate proposed new file system watcher API with excludes
const opts: IWatchOptions = {
...unvalidatedOpts
};
// Convert a recursive watcher to a flat watcher if the path
// turns out to not be a folder. Recursive watching is only
// possible on folders, so we help all file watchers by checking
// early.
if (opts.recursive) {
try {
const stat = await this._fileService.stat(uri);
if (!stat.isDirectory) {
opts.recursive = false;
}
} catch (error) {
this._logService.error(`MainThreadFileSystemEventService#$watch(): failed to stat a resource for file watching (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session}): ${error}`);
}
}
// Correlated file watching is taken as is
if (correlate) {
this._logService.trace(`MainThreadFileSystemEventService#$watch(): request to start watching correlated (extension: ${extensionId}, path: ${uri.toString(true)}, recursive: ${opts.recursive}, session: ${session})`);
const watcherDisposables = new DisposableStore();
const subscription = watcherDisposables.add(this._fileService.createWatcher(uri, opts));
watcherDisposables.add(subscription.onDidChange(event => {
this._proxy.$onFileEvent({
session,
created: event.rawAdded,
changed: event.rawUpdated,
deleted: event.rawDeleted
});
}));
this._watches.set(session, watcherDisposables);
}
// Uncorrelated file watching gets special treatment
else {
// Refuse to watch anything that is already watched via
// our workspace watchers in case the request is a
// recursive file watcher and does not opt-in to event
// correlation via specific exclude rules.
// Still allow for non-recursive watch requests as a way
// to bypass configured exclude rules though
// (see https://github.com/microsoft/vscode/issues/146066)
const workspaceFolder = this._contextService.getWorkspaceFolder(uri);
if (workspaceFolder && opts.recursive) {
this._logService.trace(`MainThreadFileSystemEventService#$watch(): ignoring request to start watching because path is inside workspace (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})`);
// 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<IFilesConfiguration>();
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (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
// `<workspace path>/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 (workspaceFolder) {
const config = this._configurationService.getValue<IFilesConfiguration>();
if (config.files?.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (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;
}
}
const subscription = this._fileService.watch(uri, opts);
this._watches.set(session, subscription);
}
}
$unwatch(session: number): void {
if (this._watches.has(session)) {
this._logService.trace(`MainThreadFileSystemEventService#$unwatch(): request to stop watching (session: ${session})`);
this._watches.deleteAndDispose(session);
}
}
dispose(): void {
this._listener.dispose();
this._watches.dispose();
}
}

View file

@ -938,8 +938,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
applyEdit(edit: vscode.WorkspaceEdit, metadata?: vscode.WorkspaceEditMetadata): Thenable<boolean> {
return extHostBulkEdits.applyWorkspaceEdit(edit, extension, metadata);
},
createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): vscode.FileSystemWatcher => {
return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, extension, pattern, ignoreCreate, ignoreChange, ignoreDelete);
createFileSystemWatcher: (pattern, optionsOrIgnoreCreate, ignoreChange?, ignoreDelete?): vscode.FileSystemWatcher => {
let options: vscode.FileSystemWatcherOptions | undefined = undefined;
if (typeof optionsOrIgnoreCreate === 'boolean') {
options = {
ignoreCreateEvents: Boolean(optionsOrIgnoreCreate),
ignoreChangeEvents: Boolean(ignoreChange),
ignoreDeleteEvents: Boolean(ignoreDelete)
};
} else if (optionsOrIgnoreCreate) {
checkProposedApiEnabled(extension, 'createFileSystemWatcher');
options = optionsOrIgnoreCreate;
}
return extHostFileSystemEvent.createFileSystemWatcher(extHostWorkspace, extension, pattern, options);
},
get textDocuments() {
return extHostDocuments.getAllDocumentData().map(data => data.document);

View file

@ -1310,10 +1310,12 @@ export interface MainThreadFileSystemShape extends IDisposable {
$mkdir(resource: UriComponents): Promise<void>;
$delete(resource: UriComponents, opts: files.IFileDeleteOptions): Promise<void>;
$ensureActivation(scheme: string): Promise<void>;
}
export interface MainThreadFileSystemEventServiceShape extends IDisposable {
$watch(extensionId: string, session: number, resource: UriComponents, opts: files.IWatchOptions): void;
$unwatch(session: number): void;
$ensureActivation(scheme: string): Promise<void>;
}
export interface MainThreadLabelServiceShape extends IDisposable {
@ -1762,6 +1764,7 @@ export interface ExtHostExtensionServiceShape {
}
export interface FileSystemEvents {
session?: number;
created: UriComponents[];
changed: UriComponents[];
deleted: UriComponents[];
@ -2700,6 +2703,7 @@ export const MainContext = {
MainThreadProfileContentHandlers: createProxyIdentifier<MainThreadProfileContentHandlersShape>('MainThreadProfileContentHandlers'),
MainThreadWorkspace: createProxyIdentifier<MainThreadWorkspaceShape>('MainThreadWorkspace'),
MainThreadFileSystem: createProxyIdentifier<MainThreadFileSystemShape>('MainThreadFileSystem'),
MainThreadFileSystemEventService: createProxyIdentifier<MainThreadFileSystemEventServiceShape>('MainThreadFileSystemEventService'),
MainThreadExtensionService: createProxyIdentifier<MainThreadExtensionServiceShape>('MainThreadExtensionService'),
MainThreadSCM: createProxyIdentifier<MainThreadSCMShape>('MainThreadSCM'),
MainThreadSearch: createProxyIdentifier<MainThreadSearchShape>('MainThreadSearch'),

View file

@ -18,8 +18,18 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { Lazy } from 'vs/base/common/lazy';
interface FileSystemWatcherCreateOptions {
readonly ignoreCreateEvents?: boolean;
readonly ignoreChangeEvents?: boolean;
readonly ignoreDeleteEvents?: boolean;
readonly excludes?: string[];
}
class FileSystemWatcher implements vscode.FileSystemWatcher {
private readonly session = Math.random();
private readonly _onDidCreate = new Emitter<vscode.Uri>();
private readonly _onDidChange = new Emitter<vscode.Uri>();
private readonly _onDidDelete = new Emitter<vscode.Uri>();
@ -39,17 +49,15 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
return Boolean(this._config & 0b100);
}
constructor(mainContext: IMainContext, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePatternDto, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) {
const watcherDisposable = this.ensureWatching(mainContext, extension, globPattern);
constructor(mainContext: IMainContext, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePatternDto, options?: FileSystemWatcherCreateOptions) {
this._config = 0;
if (ignoreCreateEvents) {
if (options?.ignoreCreateEvents) {
this._config += 0b001;
}
if (ignoreChangeEvents) {
if (options?.ignoreChangeEvents) {
this._config += 0b010;
}
if (ignoreDeleteEvents) {
if (options?.ignoreDeleteEvents) {
this._config += 0b100;
}
@ -63,7 +71,11 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
const excludeOutOfWorkspaceEvents = typeof globPattern === 'string';
const subscription = dispatcher(events => {
if (!ignoreCreateEvents) {
if (typeof events.session === 'number' && events.session !== this.session) {
return; // ignore events from other file watchers
}
if (!options?.ignoreCreateEvents) {
for (const created of events.created) {
const uri = URI.revive(created);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
@ -71,7 +83,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
}
}
}
if (!ignoreChangeEvents) {
if (!options?.ignoreChangeEvents) {
for (const changed of events.changed) {
const uri = URI.revive(changed);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
@ -79,7 +91,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
}
}
}
if (!ignoreDeleteEvents) {
if (!options?.ignoreDeleteEvents) {
for (const deleted of events.deleted) {
const uri = URI.revive(deleted);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
@ -89,27 +101,26 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
}
});
this._disposable = Disposable.from(watcherDisposable, this._onDidCreate, this._onDidChange, this._onDidDelete, subscription);
this._disposable = Disposable.from(this.ensureWatching(mainContext, extension, globPattern, options), this._onDidCreate, this._onDidChange, this._onDidDelete, subscription);
}
private ensureWatching(mainContext: IMainContext, extension: IExtensionDescription, globPattern: string | IRelativePatternDto): Disposable {
private ensureWatching(mainContext: IMainContext, extension: IExtensionDescription, globPattern: string | IRelativePatternDto, options?: FileSystemWatcherCreateOptions): Disposable {
const disposable = Disposable.from();
if (typeof globPattern === 'string') {
return disposable; // a pattern alone does not carry sufficient information to start watching anything
return disposable; // workspace is already watched by default, no need to watch again!
}
const proxy = mainContext.getProxy(MainContext.MainThreadFileSystem);
const proxy = mainContext.getProxy(MainContext.MainThreadFileSystemEventService);
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 */ });
proxy.$watch(extension.identifier.value, this.session, globPattern.baseUri, { recursive, excludes: options?.excludes ?? [] });
return Disposable.from({ dispose: () => proxy.$unwatch(session) });
return Disposable.from({ dispose: () => proxy.$unwatch(this.session) });
}
dispose() {
@ -138,6 +149,8 @@ class LazyRevivedFileSystemEvents implements FileSystemEvents {
constructor(private readonly _events: FileSystemEvents) { }
readonly session = this._events.session;
private _created = new Lazy(() => this._events.created.map(URI.revive) as URI[]);
get created(): URI[] { return this._created.value; }
@ -173,15 +186,14 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
//--- file events
createFileSystemWatcher(workspace: IExtHostWorkspace, extension: IExtensionDescription, globPattern: vscode.GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher {
return new FileSystemWatcher(this._mainContext, workspace, extension, this._onFileSystemEvent.event, typeConverter.GlobPattern.from(globPattern), ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents);
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);
}
$onFileEvent(events: FileSystemEvents) {
this._onFileSystemEvent.fire(new LazyRevivedFileSystemEvents(events));
}
//--- file operations
$onDidRunFileOperation(operation: FileOperation, files: SourceTargetPair[]): void {

View file

@ -22,13 +22,13 @@ suite('ExtHostFileSystemEventService', () => {
drain: undefined!
};
const watcher1 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingInteresting', false, false, false);
const watcher1 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingInteresting', {});
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', true, true, true);
const watcher2 = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!).createFileSystemWatcher(undefined!, undefined!, '**/somethingBoring', { ignoreCreateEvents: true, ignoreChangeEvents: true, ignoreDeleteEvents: true });
assert.strictEqual(watcher2.ignoreChangeEvents, true);
assert.strictEqual(watcher2.ignoreCreateEvents, true);
assert.strictEqual(watcher2.ignoreDeleteEvents, true);

View file

@ -20,7 +20,7 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteA
import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService';
import { IRemoteAuthorityResolverService, RemoteConnectionType } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas, connectionTokenCookieName } from 'vs/base/common/network';
import { IAnyWorkspaceIdentifier, IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW_WORKSPACE, isTemporaryWorkspace, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
@ -261,7 +261,7 @@ export class BrowserMain extends Disposable {
// Files
const fileLogger = new BufferLogger();
const fileService = this._register(new FileService(fileLogger));
serviceCollection.set(IWorkbenchFileService, fileService);
serviceCollection.set(IFileService, fileService);
// Logger
const loggerService = new FileLoggerService(getLogLevel(environmentService), logsPath, fileService);
@ -429,7 +429,8 @@ export class BrowserMain extends Disposable {
}
}
private async registerIndexedDBFileSystemProviders(environmentService: IWorkbenchEnvironmentService, fileService: IWorkbenchFileService, logService: ILogService, loggerService: ILoggerService, logsPath: URI): Promise<void> {
private async registerIndexedDBFileSystemProviders(environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService, loggerService: ILoggerService, logsPath: URI): Promise<void> {
// IndexedDB is used for logging and user data
let indexedDB: IndexedDB | undefined;
const userDataStore = 'vscode-userdata-store';

View file

@ -7,7 +7,7 @@ import { localize } from 'vs/nls';
import { IDisposable, Disposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { IFileService, IFilesConfiguration } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { ResourceMap } from 'vs/base/common/map';
import { INotificationService, Severity, NeverShowAgainScope, NotificationPriority } from 'vs/platform/notification/common/notification';
@ -15,14 +15,13 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { isAbsolute } from 'vs/base/common/path';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files';
export class WorkspaceWatcher extends Disposable {
private readonly watchedWorkspaces = new ResourceMap<IDisposable>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));
constructor(
@IWorkbenchFileService private readonly fileService: IWorkbenchFileService,
@IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@INotificationService private readonly notificationService: INotificationService,

View file

@ -29,7 +29,7 @@ import { IRemoteAuthorityResolverService, RemoteConnectionType } from 'vs/platfo
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { FileService } from 'vs/platform/files/common/fileService';
import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files';
import { IFileService } from 'vs/platform/files/common/files';
import { RemoteFileSystemProviderClient } from 'vs/workbench/services/remote/common/remoteFileSystemProviderClient';
import { ConfigurationCache } from 'vs/workbench/services/configuration/common/configurationCache';
import { ISignService } from 'vs/platform/sign/common/sign';
@ -227,7 +227,7 @@ export class DesktopMain extends Disposable {
// Files
const fileService = this._register(new FileService(logService));
serviceCollection.set(IWorkbenchFileService, fileService);
serviceCollection.set(IFileService, fileService);
// Remote
const remoteAuthorityResolverService = new RemoteAuthorityResolverService(productService, new ElectronRemoteResourceLoader(environmentService.window.id, mainProcessService, fileService));

View file

@ -32,6 +32,7 @@ export const allApiProposals = Object.freeze({
contribStatusBarItems: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts',
contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts',
contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts',
createFileSystemWatcher: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts',
customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts',
debugFocus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugFocus.d.ts',
diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts',

View file

@ -1,22 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IFileService, IWatchOptions } from 'vs/platform/files/common/files';
import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IWorkbenchFileService = refineServiceDecorator<IFileService, IWorkbenchFileService>(IFileService);
export interface IWorkbenchFileService extends IFileService {
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
*
* Note: watching a folder does not report events recursively unless the provided options
* explicitly opt-in to recursive watching.
*/
watch(resource: URI, options?: IWatchOptions): IDisposable;
}

View file

@ -5,14 +5,14 @@
import { Event } from 'vs/base/common/event';
import { isLinux } from 'vs/base/common/platform';
import { FileSystemProviderCapabilities, IFileDeleteOptions, IStat, FileType, IFileReadStreamOptions, IFileWriteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileAtomicReadOptions, IFileSystemProviderWithFileCloneCapability } from 'vs/platform/files/common/files';
import { FileSystemProviderCapabilities, IFileDeleteOptions, IStat, FileType, IFileReadStreamOptions, IFileWriteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileAtomicReadOptions, IFileSystemProviderWithFileCloneCapability, IFileChange } from 'vs/platform/files/common/files';
import { AbstractDiskFileSystemProvider } from 'vs/platform/files/common/diskFileSystemProvider';
import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ReadableStreamEvents } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri';
import { DiskFileSystemProviderClient, LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient';
import { IDiskFileChange, ILogMessage, AbstractUniversalWatcherClient } from 'vs/platform/files/common/watcher';
import { ILogMessage, AbstractUniversalWatcherClient } from 'vs/platform/files/common/watcher';
import { UniversalWatcherClient } from 'vs/workbench/services/files/electron-sandbox/watcherClient';
import { ILogService } from 'vs/platform/log/common/log';
import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService';
@ -132,7 +132,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
//#region File Watching
protected createUniversalWatcher(
onChange: (changes: IDiskFileChange[]) => void,
onChange: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): AbstractUniversalWatcherClient {

View file

@ -5,13 +5,14 @@
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { getDelayedChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage, IRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { IFileChange } from 'vs/platform/files/common/files';
import { AbstractUniversalWatcherClient, ILogMessage, IRecursiveWatcher } from 'vs/platform/files/common/watcher';
import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService';
export class UniversalWatcherClient extends AbstractUniversalWatcherClient {
constructor(
onFileChanges: (changes: IDiskFileChange[]) => void,
onFileChanges: (changes: IFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean,
private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService

View file

@ -45,9 +45,6 @@ import { Codicon } from 'vs/base/common/codicons';
import { listErrorForeground } from 'vs/platform/theme/common/colorRegistry';
import { firstOrDefault } from 'vs/base/common/arrays';
/**
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
*/
export abstract class AbstractTextFileService extends Disposable implements ITextFileService {
declare readonly _serviceBrand: undefined;

View file

@ -23,7 +23,7 @@ import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbe
import { IWorkspaceContextService, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { FileOperationEvent, IFileService, IFileStat, IFileStatResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IReadFileStreamOptions, IFileSystemProviderCapabilitiesChangeEvent, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files';
import { FileOperationEvent, IFileService, IFileStat, IFileStatResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IReadFileStreamOptions, IFileSystemProviderCapabilitiesChangeEvent, IFileStatWithPartialMetadata, IFileSystemWatcher, IWatchOptionsWithCorrelation } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/model';
import { LanguageService } from 'vs/editor/common/services/languageService';
import { ModelService } from 'vs/editor/common/services/modelService';
@ -1148,7 +1148,17 @@ export class TestFileService implements IFileService {
async del(_resource: URI, _options?: { useTrash?: boolean; recursive?: boolean }): Promise<void> { }
createWatcher(resource: URI, options: IWatchOptions): IFileSystemWatcher {
return {
onDidChange: Event.None,
dispose: () => { }
};
}
readonly watches: URI[] = [];
watch(_resource: URI, options: IWatchOptionsWithCorrelation): IFileSystemWatcher;
watch(_resource: URI): IDisposable;
watch(_resource: URI): IDisposable {
this.watches.push(_resource);
@ -1368,7 +1378,7 @@ export class RemoteFileSystemProvider implements IFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities = this.wrappedFsp.capabilities;
readonly onDidChangeCapabilities: Event<void> = this.wrappedFsp.onDidChangeCapabilities;
readonly onDidChangeFile: Event<readonly IFileChange[]> = Event.map(this.wrappedFsp.onDidChangeFile, changes => changes.map((c): IFileChange => {
readonly onDidChangeFile: Event<readonly IFileChange[]> = Event.map(this.wrappedFsp.onDidChangeFile, changes => changes.map(c => {
return {
type: c.type,
resource: c.resource.with({ scheme: Schemas.vscodeRemote, authority: this.remoteAuthority }),

View file

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// https://github.com/microsoft/vscode/issues/169724 @bpasero
declare module 'vscode' {
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 {
export function createFileSystemWatcher(pattern: RelativePattern, options?: FileSystemWatcherOptions): FileSystemWatcher;
}
}