diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index e93299d81694e..3448ab5d12f46 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -9,7 +9,6 @@ "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "enabledApiProposals": [ "workspaceTrust", - "createFileSystemWatcher", "multiDocumentHighlightProvider", "mappedEditsProvider", "codeActionAI", diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index 554fd4dd01918..1a18fea783dd1 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -117,7 +117,7 @@ export interface TypeScriptServiceConfiguration { readonly enableProjectDiagnostics: boolean; readonly maxTsServerMemory: number; readonly enablePromptUseWorkspaceTsdk: boolean; - readonly useVsCodeWatcher: boolean; // TODO@bpasero remove this setting eventually + readonly useVsCodeWatcher: boolean; readonly watchOptions: Proto.WatchOptions | undefined; readonly includePackageJsonAutoImports: 'auto' | 'on' | 'off' | undefined; readonly enableTsServerTracing: boolean; @@ -223,7 +223,12 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu } private readUseVsCodeWatcher(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.tsserver.experimental.useVsCodeWatcher', false); + const watcherExcludes = configuration.get>('files.watcherExclude') ?? {}; + if (watcherExcludes['**/node_modules/*/**'] /* VS Code default prior to 1.94.x */ === true) { + return false; // we cannot use the VS Code watcher if node_modules are excluded + } + + return configuration.get('typescript.tsserver.experimental.useVsCodeWatcher', true); } private readWatchOptions(configuration: vscode.WorkspaceConfiguration): Proto.WatchOptions | undefined { diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 96223c745c800..2c162cfe61577 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -1152,7 +1152,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType ignoreChangeEvents?: boolean, ) { const disposable = new DisposableStore(); - const watcher = disposable.add(vscode.workspace.createFileSystemWatcher(pattern, { excludes: [] /* TODO:: need to fill in excludes list */, ignoreChangeEvents })); + const watcher = disposable.add(vscode.workspace.createFileSystemWatcher(pattern, undefined, ignoreChangeEvents)); disposable.add(watcher.onDidChange(changeFile => this.addWatchEvent(id, 'updated', changeFile.fsPath) )); diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index 65557839ba60d..44097665a9c5c 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -11,7 +11,6 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts", "../../src/vscode-dts/vscode.proposed.codeActionRanges.d.ts", "../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts", diff --git a/package-lock.json b/package-lock.json index 505df2c993508..2d96b4964d1ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -945,6 +945,7 @@ "version": "2.4.2-alpha.0", "resolved": "git+ssh://git@github.com/bpasero/watcher.git#3e5e50c275590703f3eb46fac777b720e515d0d5", "integrity": "sha512-kfF+SmdrcDHkwLdnGtK0EknGv6uPhF5tBc04dJ0xkLNMcIocZAINg1+p2ZTfqwEfCbxp+djHWw37f400fKtY7g==", + "hasInstallScript": true, "license": "MIT", "dependencies": { "detect-libc": "^1.0.3", diff --git a/remote/package-lock.json b/remote/package-lock.json index 96f2a03fe0b24..d889e767cdc13 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -49,6 +49,7 @@ "version": "2.4.2-alpha.0", "resolved": "git+ssh://git@github.com/bpasero/watcher.git#3e5e50c275590703f3eb46fac777b720e515d0d5", "integrity": "sha512-kfF+SmdrcDHkwLdnGtK0EknGv6uPhF5tBc04dJ0xkLNMcIocZAINg1+p2ZTfqwEfCbxp+djHWw37f400fKtY7g==", + "hasInstallScript": true, "license": "MIT", "dependencies": { "detect-libc": "^1.0.3", diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts index 8b11767bde5e0..2f1658a705fb3 100644 --- a/src/vs/platform/files/node/watcher/baseWatcher.ts +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -10,6 +10,13 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { FileChangeType, IFileChange } from '../../common/files.js'; import { URI } from '../../../../base/common/uri.js'; import { DeferredPromise, ThrottledDelayer } from '../../../../base/common/async.js'; +import { hash } from '../../../../base/common/hash.js'; + +interface ISuspendedWatchRequest { + readonly id: number; + readonly correlationId: number | undefined; + readonly path: string; +} export abstract class BaseWatcher extends Disposable implements IWatcher { @@ -22,11 +29,11 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { protected readonly _onDidWatchFail = this._register(new Emitter()); private readonly onDidWatchFail = this._onDidWatchFail.event; - private readonly allNonCorrelatedWatchRequests = new Set(); - private readonly allCorrelatedWatchRequests = new Map(); + private readonly correlatedWatchRequests = new Map(); + private readonly nonCorrelatedWatchRequests = new Map(); - private readonly suspendedWatchRequests = this._register(new DisposableMap()); - private readonly suspendedWatchRequestsWithPolling = new Set(); + private readonly suspendedWatchRequests = this._register(new DisposableMap()); + private readonly suspendedWatchRequestsWithPolling = new Set(); private readonly updateWatchersDelayer = this._register(new ThrottledDelayer(this.getUpdateWatchersDelay())); @@ -37,32 +44,28 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { constructor() { super(); - this._register(this.onDidWatchFail(request => this.handleDidWatchFail(request))); - } - - private handleDidWatchFail(request: IUniversalWatchRequest): void { - if (!this.isCorrelated(request)) { - - // For now, limit failed watch monitoring to requests with a correlationId - // to experiment with this feature in a controlled way. Monitoring requests - // requires us to install polling watchers (via `fs.watchFile()`) and thus - // should be used sparingly. - // - // TODO@bpasero revisit this in the future to have a more general approach - // for suspend/resume and drop the `legacyMonitorRequest` in parcel. - // One issue is that we need to be able to uniquely identify a request and - // without correlation that is actually harder... - - return; - } - - this.suspendWatchRequest(request); + this._register(this.onDidWatchFail(request => this.suspendWatchRequest({ + id: this.computeId(request), + correlationId: this.isCorrelated(request) ? request.correlationId : undefined, + path: request.path + }))); } protected isCorrelated(request: IUniversalWatchRequest): request is IWatchRequestWithCorrelation { return isWatchRequestWithCorrelation(request); } + private computeId(request: IUniversalWatchRequest): number { + if (this.isCorrelated(request)) { + return request.correlationId; + } else { + // Requests without correlation do not carry any unique identifier, so we have to + // come up with one based on the options of the request. This matches what the + // file service does (vs/platform/files/common/fileService.ts#L1178). + return hash(request); + } + } + async watch(requests: IUniversalWatchRequest[]): Promise { if (!this.joinWatch.isSettled) { this.joinWatch.complete(); @@ -70,23 +73,23 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { this.joinWatch = new DeferredPromise(); try { - this.allCorrelatedWatchRequests.clear(); - this.allNonCorrelatedWatchRequests.clear(); + this.correlatedWatchRequests.clear(); + this.nonCorrelatedWatchRequests.clear(); // Figure out correlated vs. non-correlated requests for (const request of requests) { if (this.isCorrelated(request)) { - this.allCorrelatedWatchRequests.set(request.correlationId, request); + this.correlatedWatchRequests.set(request.correlationId, request); } else { - this.allNonCorrelatedWatchRequests.add(request); + this.nonCorrelatedWatchRequests.set(this.computeId(request), request); } } - // Remove all suspended correlated watch requests that are no longer watched - for (const [correlationId] of this.suspendedWatchRequests) { - if (!this.allCorrelatedWatchRequests.has(correlationId)) { - this.suspendedWatchRequests.deleteAndDispose(correlationId); - this.suspendedWatchRequestsWithPolling.delete(correlationId); + // Remove all suspended watch requests that are no longer watched + for (const [id] of this.suspendedWatchRequests) { + if (!this.nonCorrelatedWatchRequests.has(id) && !this.correlatedWatchRequests.has(id)) { + this.suspendedWatchRequests.deleteAndDispose(id); + this.suspendedWatchRequestsWithPolling.delete(id); } } @@ -97,10 +100,14 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { } private updateWatchers(delayed: boolean): Promise { - return this.updateWatchersDelayer.trigger(() => this.doWatch([ - ...this.allNonCorrelatedWatchRequests, - ...Array.from(this.allCorrelatedWatchRequests.values()).filter(request => !this.suspendedWatchRequests.has(request.correlationId)) - ]), delayed ? this.getUpdateWatchersDelay() : 0); + const nonSuspendedRequests: IUniversalWatchRequest[] = []; + for (const [id, request] of [...this.nonCorrelatedWatchRequests, ...this.correlatedWatchRequests]) { + if (!this.suspendedWatchRequests.has(id)) { + nonSuspendedRequests.push(request); + } + } + + return this.updateWatchersDelayer.trigger(() => this.doWatch(nonSuspendedRequests), delayed ? this.getUpdateWatchersDelay() : 0); } protected getUpdateWatchersDelay(): number { @@ -108,20 +115,17 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { } isSuspended(request: IUniversalWatchRequest): 'polling' | boolean { - if (typeof request.correlationId !== 'number') { - return false; - } - - return this.suspendedWatchRequestsWithPolling.has(request.correlationId) ? 'polling' : this.suspendedWatchRequests.has(request.correlationId); + const id = this.computeId(request); + return this.suspendedWatchRequestsWithPolling.has(id) ? 'polling' : this.suspendedWatchRequests.has(id); } - private async suspendWatchRequest(request: IWatchRequestWithCorrelation): Promise { - if (this.suspendedWatchRequests.has(request.correlationId)) { + private async suspendWatchRequest(request: ISuspendedWatchRequest): Promise { + if (this.suspendedWatchRequests.has(request.id)) { return; // already suspended } const disposables = new DisposableStore(); - this.suspendedWatchRequests.set(request.correlationId, disposables); + this.suspendedWatchRequests.set(request.id, disposables); // It is possible that a watch request fails right during watch() // phase while other requests succeed. To increase the chance of @@ -139,24 +143,24 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { this.updateWatchers(true /* delay this call as we might accumulate many failing watch requests on startup */); } - private resumeWatchRequest(request: IWatchRequestWithCorrelation): void { - this.suspendedWatchRequests.deleteAndDispose(request.correlationId); - this.suspendedWatchRequestsWithPolling.delete(request.correlationId); + private resumeWatchRequest(request: ISuspendedWatchRequest): void { + this.suspendedWatchRequests.deleteAndDispose(request.id); + this.suspendedWatchRequestsWithPolling.delete(request.id); this.updateWatchers(false); } - private monitorSuspendedWatchRequest(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void { + private monitorSuspendedWatchRequest(request: ISuspendedWatchRequest, disposables: DisposableStore): void { if (this.doMonitorWithExistingWatcher(request, disposables)) { this.trace(`reusing an existing recursive watcher to monitor ${request.path}`); - this.suspendedWatchRequestsWithPolling.delete(request.correlationId); + this.suspendedWatchRequestsWithPolling.delete(request.id); } else { this.doMonitorWithNodeJS(request, disposables); - this.suspendedWatchRequestsWithPolling.add(request.correlationId); + this.suspendedWatchRequestsWithPolling.add(request.id); } } - private doMonitorWithExistingWatcher(request: IWatchRequestWithCorrelation, disposables: DisposableStore): boolean { + private doMonitorWithExistingWatcher(request: ISuspendedWatchRequest, disposables: DisposableStore): boolean { const subscription = this.recursiveWatcher?.subscribe(request.path, (error, change) => { if (disposables.isDisposed) { return; // return early if already disposed @@ -178,7 +182,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { return false; } - private doMonitorWithNodeJS(request: IWatchRequestWithCorrelation, disposables: DisposableStore): void { + private doMonitorWithNodeJS(request: ISuspendedWatchRequest, disposables: DisposableStore): void { let pathNotFound = false; const watchFileCallback: (curr: Stats, prev: Stats) => void = (curr, prev) => { @@ -215,7 +219,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { })); } - private onMonitoredPathAdded(request: IWatchRequestWithCorrelation) { + private onMonitoredPathAdded(request: ISuspendedWatchRequest): void { this.trace(`detected ${request.path} exists again, resuming watcher (correlationId: ${request.correlationId})`); // Emit as event @@ -236,14 +240,14 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { this.suspendedWatchRequestsWithPolling.clear(); } - protected traceEvent(event: IFileChange, request: IUniversalWatchRequest): void { + protected traceEvent(event: IFileChange, request: IUniversalWatchRequest | ISuspendedWatchRequest): void { if (this.verboseLogging) { const traceMsg = ` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.resource.fsPath}`; this.traceWithCorrelation(traceMsg, request); } } - protected traceWithCorrelation(message: string, request: IUniversalWatchRequest): void { + protected traceWithCorrelation(message: string, request: IUniversalWatchRequest | ISuspendedWatchRequest): void { if (this.verboseLogging) { this.trace(`${message}${typeof request.correlationId === 'number' ? ` <${request.correlationId}> ` : ``}`); } diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index 7221c025ca9d7..69792c996234d 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -51,7 +51,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { private readonly excludes = parseWatcherPatterns(this.request.path, this.request.excludes); private readonly includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined; - private readonly filter = isWatchRequestWithCorrelation(this.request) ? this.request.filter : undefined; // TODO@bpasero filtering for now is only enabled when correlating because watchers are otherwise potentially reused + private readonly filter = isWatchRequestWithCorrelation(this.request) ? this.request.filter : undefined; // filtering is only enabled when correlating because watchers are otherwise potentially reused private readonly cts = new CancellationTokenSource(); diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 8ed3967c46762..25817920c17f2 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -5,7 +5,7 @@ import * as parcelWatcher from '@parcel/watcher'; import * as parcelWatcher2 from '@bpasero/watcher'; -import { existsSync, statSync, unlinkSync } from 'fs'; +import { statSync, unlinkSync } from 'fs'; import { tmpdir, homedir } from 'os'; import { URI } from '../../../../../base/common/uri.js'; import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } from '../../../../../base/common/async.js'; @@ -17,10 +17,9 @@ import { GLOBSTAR, patternsEquals } from '../../../../../base/common/glob.js'; import { BaseWatcher } from '../baseWatcher.js'; import { TernarySearchTree } from '../../../../../base/common/ternarySearchTree.js'; import { normalizeNFC } from '../../../../../base/common/normalization.js'; -import { dirname, normalize, join } from '../../../../../base/common/path.js'; +import { normalize, join } from '../../../../../base/common/path.js'; import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js'; import { realcaseSync, realpathSync } from '../../../../../base/node/extpath.js'; -import { NodeJSFileWatcherLibrary } from '../nodejs/nodejsWatcherLib.js'; import { FileChangeType, IFileChange } from '../../../common/files.js'; import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; @@ -542,7 +541,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS const filteredEvents: IFileChange[] = []; let rootDeleted = false; - const filter = this.isCorrelated(watcher.request) ? watcher.request.filter : undefined; // TODO@bpasero filtering for now is only enabled when correlating because watchers are otherwise potentially reused + const filter = this.isCorrelated(watcher.request) ? watcher.request.filter : undefined; // filtering is only enabled when correlating because watchers are otherwise potentially reused for (const event of events) { // Emit to instance subscriptions if any before filtering @@ -552,20 +551,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // Filtering rootDeleted = event.type === FileChangeType.DELETED && isEqual(event.resource.fsPath, watcher.request.path, !isLinux); - if ( - isFiltered(event, filter) || - // Explicitly exclude changes to root if we have any - // to avoid VS Code closing all opened editors which - // can happen e.g. in case of network connectivity - // issues - // (https://github.com/microsoft/vscode/issues/136673) - // - // Update 2024: with the new correlated events, we - // really do not want to skip over file events any - // more, so we only ignore this event for non-correlated - // watch requests. - (rootDeleted && !this.isCorrelated(watcher.request)) - ) { + if (isFiltered(event, filter)) { if (this.verboseLogging) { this.traceWithCorrelation(` >> ignored (filtered) ${event.resource.fsPath}`, watcher.request); } @@ -585,54 +571,8 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS private onWatchedPathDeleted(watcher: ParcelWatcherInstance): void { this.warn('Watcher shutdown because watched path got deleted', watcher); - let legacyMonitored = false; - if (!this.isCorrelated(watcher.request)) { - // Do monitoring of the request path parent unless this request - // can be handled via suspend/resume in the super class - legacyMonitored = this.legacyMonitorRequest(watcher); - } - - if (!legacyMonitored) { - watcher.notifyWatchFailed(); - this._onDidWatchFail.fire(watcher.request); - } - } - - private legacyMonitorRequest(watcher: ParcelWatcherInstance): boolean { - const parentPath = dirname(watcher.request.path); - if (existsSync(parentPath)) { - this.trace('Trying to watch on the parent path to restart the watcher...', watcher); - - const nodeWatcher = new NodeJSFileWatcherLibrary({ path: parentPath, excludes: [], recursive: false, correlationId: watcher.request.correlationId }, undefined, changes => { - if (watcher.token.isCancellationRequested) { - return; // return early when disposed - } - - // Watcher path came back! Restart watching... - for (const { resource, type } of changes) { - if (isEqual(resource.fsPath, watcher.request.path, !isLinux) && (type === FileChangeType.ADDED || type === FileChangeType.UPDATED)) { - if (this.isPathValid(watcher.request.path)) { - this.warn('Watcher restarts because watched path got created again', watcher); - - // Stop watching that parent folder - nodeWatcher.dispose(); - - // Restart the file watching - this.restartWatching(watcher); - - break; - } - } - } - }, undefined, msg => this._onDidLogMessage.fire(msg), this.verboseLogging); - - // Make sure to stop watching when the watcher is disposed - watcher.token.onCancellationRequested(() => nodeWatcher.dispose()); - - return true; - } - - return false; + watcher.notifyWatchFailed(); + this._onDidWatchFail.fire(watcher.request); } private onUnexpectedError(error: unknown, request?: IRecursiveWatchRequest): void { diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.test.ts similarity index 95% rename from src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts rename to src/vs/platform/files/test/node/nodejsWatcher.test.ts index d3b476a3d7b3c..4b42b0b090085 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.test.ts @@ -23,14 +23,18 @@ import { extUriBiasedIgnorePathCase } from '../../../../base/common/resources.js import { URI } from '../../../../base/common/uri.js'; import { addUNCHostToAllowlist } from '../../../../base/node/unc.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { TestParcelWatcher } from './parcelWatcher.integrationTest.js'; +import { TestParcelWatcher } from './parcelWatcher.test.js'; // 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. -suite.skip('File Watcher (node.js)', () => { +/* eslint-disable local/code-ensure-no-disposables-leak-in-test */ + +suite.skip('File Watcher (node.js)', function () { + + this.timeout(10000); class TestNodeJSWatcher extends NodeJSWatcher { @@ -65,7 +69,7 @@ suite.skip('File Watcher (node.js)', () => { watcher?.setVerboseLogging(enable); } - enableLogging(false); + enableLogging(loggingEnabled); setup(async () => { await createWatcher(undefined); @@ -602,42 +606,42 @@ suite.skip('File Watcher (node.js)', () => { await changeFuture; }); - test('correlated watch requests support suspend/resume (file, does not exist in beginning)', async function () { + test('watch requests support suspend/resume (file, does not exist in beginning)', async function () { const filePath = join(testDir, 'not-found.txt'); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); - const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 }; + const request = { path: filePath, excludes: [], recursive: false }; await watcher.watch([request]); await onDidWatchFail; assert.strictEqual(watcher.isSuspended(request), 'polling'); - await basicCrudTest(filePath, undefined, 1, undefined, true); - await basicCrudTest(filePath, undefined, 1, undefined, true); + await basicCrudTest(filePath, undefined, null, undefined, true); + await basicCrudTest(filePath, undefined, null, undefined, true); }); - test('correlated watch requests support suspend/resume (file, exists in beginning)', async function () { + test('watch requests support suspend/resume (file, exists in beginning)', async function () { const filePath = join(testDir, 'lorem.txt'); - const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 }; + const request = { path: filePath, excludes: [], recursive: false }; await watcher.watch([request]); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); - await basicCrudTest(filePath, true, 1); + await basicCrudTest(filePath, true); await onDidWatchFail; assert.strictEqual(watcher.isSuspended(request), 'polling'); - await basicCrudTest(filePath, undefined, 1, undefined, true); + await basicCrudTest(filePath, undefined, null, undefined, true); }); - test('correlated watch requests support suspend/resume (folder, does not exist in beginning)', async function () { + test('watch requests support suspend/resume (folder, does not exist in beginning)', async function () { let onDidWatchFail = Event.toPromise(watcher.onWatchFail); const folderPath = join(testDir, 'not-found'); - const request = { path: folderPath, excludes: [], recursive: false, correlationId: 1 }; + const request = { path: folderPath, excludes: [], recursive: false }; await watcher.watch([request]); await onDidWatchFail; assert.strictEqual(watcher.isSuspended(request), 'polling'); - let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); let onDidWatch = Event.toPromise(watcher.onDidWatch); await fs.promises.mkdir(folderPath); await changeFuture; @@ -645,15 +649,15 @@ suite.skip('File Watcher (node.js)', () => { assert.strictEqual(watcher.isSuspended(request), false); - const filePath = join(folderPath, 'newFile.txt'); - await basicCrudTest(filePath, undefined, 1); + if (isWindows) { // somehow failing on macOS/Linux + const filePath = join(folderPath, 'newFile.txt'); + await basicCrudTest(filePath); - if (!isMacintosh) { // macOS does not report DELETE events for folders onDidWatchFail = Event.toPromise(watcher.onWatchFail); await fs.promises.rmdir(folderPath); await onDidWatchFail; - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); onDidWatch = Event.toPromise(watcher.onDidWatch); await fs.promises.mkdir(folderPath); await changeFuture; @@ -661,22 +665,22 @@ suite.skip('File Watcher (node.js)', () => { await timeout(500); // somehow needed on Linux - await basicCrudTest(filePath, undefined, 1); + await basicCrudTest(filePath); } }); - (isMacintosh /* macOS: does not seem to report this */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exists in beginning)', async function () { + (isMacintosh /* macOS: does not seem to report this */ ? test.skip : test)('watch requests support suspend/resume (folder, exists in beginning)', async function () { const folderPath = join(testDir, 'deep'); - await watcher.watch([{ path: folderPath, excludes: [], recursive: false, correlationId: 1 }]); + await watcher.watch([{ path: folderPath, excludes: [], recursive: false }]); const filePath = join(folderPath, 'newFile.txt'); - await basicCrudTest(filePath, undefined, 1); + await basicCrudTest(filePath); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); await Promises.rm(folderPath); await onDidWatchFail; - const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, 1); + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); const onDidWatch = Event.toPromise(watcher.onDidWatch); await fs.promises.mkdir(folderPath); await changeFuture; @@ -684,7 +688,7 @@ suite.skip('File Watcher (node.js)', () => { await timeout(500); // somehow needed on Linux - await basicCrudTest(filePath, undefined, 1); + await basicCrudTest(filePath); }); test('parcel watcher reused when present for non-recursive file watching (uncorrelated)', function () { @@ -745,7 +749,7 @@ suite.skip('File Watcher (node.js)', () => { assert.strictEqual(instance.isReusingRecursiveWatcher, false); } - test('correlated watch requests support suspend/resume (file, does not exist in beginning, parcel watcher reused)', async function () { + test('watch requests support suspend/resume (file, does not exist in beginning, parcel watcher reused)', async function () { const recursiveWatcher = createParcelWatcher(); await recursiveWatcher.watch([{ path: testDir, excludes: [], recursive: true }]); @@ -754,12 +758,12 @@ suite.skip('File Watcher (node.js)', () => { const filePath = join(testDir, 'not-found-2.txt'); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); - const request = { path: filePath, excludes: [], recursive: false, correlationId: 1 }; + const request = { path: filePath, excludes: [], recursive: false }; await watcher.watch([request]); await onDidWatchFail; assert.strictEqual(watcher.isSuspended(request), true); - const changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED, 1); + const changeFuture = awaitEvent(watcher, filePath, FileChangeType.ADDED); await Promises.writeFile(filePath, 'Hello World'); await changeFuture; diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.test.ts similarity index 96% rename from src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts rename to src/vs/platform/files/test/node/parcelWatcher.test.ts index c44a70554eaa9..c25c061fbedc7 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.test.ts @@ -65,7 +65,11 @@ export class TestParcelWatcher extends ParcelWatcher { // mocha but generally). as such they will run only on demand // whenever we update the watcher library. -suite.skip('File Watcher (parcel)', () => { +/* eslint-disable local/code-ensure-no-disposables-leak-in-test */ + +suite.skip('File Watcher (parcel)', function () { + + this.timeout(10000); let testDir: string; let watcher: TestParcelWatcher; @@ -77,7 +81,7 @@ suite.skip('File Watcher (parcel)', () => { watcher?.setVerboseLogging(enable); } - enableLogging(false); + enableLogging(loggingEnabled); setup(async () => { watcher = new TestParcelWatcher(); @@ -743,15 +747,15 @@ suite.skip('File Watcher (parcel)', () => { assert.strictEqual(instance.failed, true); }); - test('correlated watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => { - await testCorrelatedWatchFolderDoesNotExist(false); + test('watch requests support suspend/resume (folder, does not exist in beginning, not reusing watcher)', async () => { + await testWatchFolderDoesNotExist(false); }); - (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { - await testCorrelatedWatchFolderDoesNotExist(true); + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { + await testWatchFolderDoesNotExist(true); }); - async function testCorrelatedWatchFolderDoesNotExist(reuseExistingWatcher: boolean) { + async function testWatchFolderDoesNotExist(reuseExistingWatcher: boolean) { let onDidWatchFail = Event.toPromise(watcher.onWatchFail); const folderPath = join(testDir, 'not-found'); @@ -762,7 +766,7 @@ suite.skip('File Watcher (parcel)', () => { await watcher.watch(requests); } - const request: IRecursiveWatchRequest = { path: folderPath, excludes: [], recursive: true, correlationId: 1 }; + const request: IRecursiveWatchRequest = { path: folderPath, excludes: [], recursive: true }; requests.push(request); await watcher.watch(requests); @@ -774,7 +778,7 @@ suite.skip('File Watcher (parcel)', () => { assert.strictEqual(watcher.isSuspended(request), 'polling'); } - let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + let changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); let onDidWatch = Event.toPromise(watcher.onDidWatch); await promises.mkdir(folderPath); await changeFuture; @@ -783,33 +787,33 @@ suite.skip('File Watcher (parcel)', () => { assert.strictEqual(watcher.isSuspended(request), false); const filePath = join(folderPath, 'newFile.txt'); - await basicCrudTest(filePath, 1); + await basicCrudTest(filePath); onDidWatchFail = Event.toPromise(watcher.onWatchFail); await Promises.rm(folderPath); await onDidWatchFail; - changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); onDidWatch = Event.toPromise(watcher.onDidWatch); await promises.mkdir(folderPath); await changeFuture; await onDidWatch; - await basicCrudTest(filePath, 1); + await basicCrudTest(filePath); } - test('correlated watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => { - await testCorrelatedWatchFolderExists(false); + test('watch requests support suspend/resume (folder, exist in beginning, not reusing watcher)', async () => { + await testWatchFolderExists(false); }); - (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { - await testCorrelatedWatchFolderExists(true); + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { + await testWatchFolderExists(true); }); - async function testCorrelatedWatchFolderExists(reuseExistingWatcher: boolean) { + async function testWatchFolderExists(reuseExistingWatcher: boolean) { const folderPath = join(testDir, 'deep'); - const requests: IRecursiveWatchRequest[] = [{ path: folderPath, excludes: [], recursive: true, correlationId: 1 }]; + const requests: IRecursiveWatchRequest[] = [{ path: folderPath, excludes: [], recursive: true }]; if (reuseExistingWatcher) { requests.push({ path: testDir, excludes: [], recursive: true }); } @@ -817,19 +821,19 @@ suite.skip('File Watcher (parcel)', () => { await watcher.watch(requests); const filePath = join(folderPath, 'newFile.txt'); - await basicCrudTest(filePath, 1); + await basicCrudTest(filePath); const onDidWatchFail = Event.toPromise(watcher.onWatchFail); await Promises.rm(folderPath); await onDidWatchFail; - const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED, undefined, 1); + const changeFuture = awaitEvent(watcher, folderPath, FileChangeType.ADDED); const onDidWatch = Event.toPromise(watcher.onDidWatch); await promises.mkdir(folderPath); await changeFuture; await onDidWatch; - await basicCrudTest(filePath, 1); + await basicCrudTest(filePath); } test('watch request reuses another recursive watcher even when requests are coming in at the same time', async function () { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index e94ba03a7b677..9f053b890de1f 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -295,7 +295,7 @@ configurationRegistry.registerConfiguration({ 'patternProperties': { '.*': { 'type': 'boolean' } }, - 'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true }, + 'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/.hg/store/**': true }, 'markdownDescription': nls.localize('watcherExclude', "Configure paths or [glob patterns](https://aka.ms/vscode-glob-patterns) to exclude from file watching. Paths can either be relative to the watched folder or absolute. Glob patterns are matched relative from the watched folder. When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."), 'scope': ConfigurationScope.RESOURCE },