mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
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:
parent
8988b0fd43
commit
29b69437ab
|
@ -8,6 +8,7 @@
|
|||
"authSession",
|
||||
"contribViewsRemote",
|
||||
"contribStatusBarItems",
|
||||
"createFileSystemWatcher",
|
||||
"customEditorMove",
|
||||
"diffCommand",
|
||||
"documentFiltersExclusive",
|
||||
|
|
|
@ -27,11 +27,8 @@ suite('vscode API - workspace-watcher', () => {
|
|||
}
|
||||
}
|
||||
|
||||
teardown(assertNoRpc);
|
||||
|
||||
test('createFileSystemWatcher', async function () {
|
||||
const fs = new WatcherTestFs('watcherTest', false);
|
||||
vscode.workspace.registerFileSystemProvider('watcherTest', fs);
|
||||
let fs: WatcherTestFs;
|
||||
let disposable: vscode.Disposable;
|
||||
|
||||
function onDidWatchPromise() {
|
||||
const onDidWatchPromise = new Promise<IWatchRequest>(resolve => {
|
||||
|
@ -41,6 +38,18 @@ suite('vscode API - workspace-watcher', () => {
|
|||
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' });
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(watchUri, '*.txt'));
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,25 +967,44 @@ export class FileChangesEvent {
|
|||
this.rawDeleted.push(change.resource);
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}))
|
||||
);
|
||||
}));
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
return Array.from(requestsMap.values());
|
||||
requestsForCorrelation.set(path, request);
|
||||
}
|
||||
|
||||
return Array.from(mapCorrelationtoRequests.values()).map(requests => Array.from(requests.values())).flat();
|
||||
}
|
||||
|
||||
async setVerboseLogging(enabled: boolean): Promise<void> {
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,13 +570,34 @@ 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);
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
const path = isLinux ? request.path : request.path.toLowerCase(); // adjust for case sensitivity
|
||||
|
||||
let requestsForCorrelation = mapCorrelationtoRequests.get(request.correlationId);
|
||||
if (!requestsForCorrelation) {
|
||||
requestsForCorrelation = new Map<string, IRecursiveWatchRequest>();
|
||||
mapCorrelationtoRequests.set(request.correlationId, requestsForCorrelation);
|
||||
}
|
||||
|
||||
requestsForCorrelation.set(path, 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
|
||||
|
@ -584,10 +606,10 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
|
|||
// However, allow explicit requests to watch folders
|
||||
// that are symbolic links because the Parcel watcher
|
||||
// does not allow to recursively watch symbolic links.
|
||||
for (const request of requests) {
|
||||
if (request.excludes.includes(GLOBSTAR)) {
|
||||
continue; // path is ignored entirely (via `**` glob exclude)
|
||||
}
|
||||
|
||||
const requestTrie = TernarySearchTree.forPaths<IRecursiveWatchRequest>(!isLinux);
|
||||
|
||||
for (const request of requestsForCorrelation.values()) {
|
||||
|
||||
// Check for overlapping requests
|
||||
if (requestTrie.findSubstr(request.path)) {
|
||||
|
@ -624,7 +646,10 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher {
|
|||
requestTrie.set(request.path, request);
|
||||
}
|
||||
|
||||
return Array.from(requestTrie).map(([, request]) => request);
|
||||
normalizedRequests.push(...Array.from(requestTrie).map(([, request]) => request));
|
||||
}
|
||||
|
||||
return normalizedRequests;
|
||||
}
|
||||
|
||||
async setVerboseLogging(enabled: boolean): Promise<void> {
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
];
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }),
|
||||
|
|
38
src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts
vendored
Normal file
38
src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue