diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index a5bea3367cc0f..d2e955468a21a 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -441,6 +441,9 @@ export interface IWatchOptions { /** * A set of glob patterns or paths to exclude from watching. + * Paths can be relative or absolute and when relative are + * resolved against the watched folder. Glob patterns are + * always matched relative to the watched folder. */ excludes: string[]; @@ -448,6 +451,9 @@ export interface IWatchOptions { * An optional set of glob patterns or paths to include for * watching. If not provided, all paths are considered for * events. + * Paths can be relative or absolute and when relative are + * resolved against the watched folder. Glob patterns are + * always matched relative to the watched folder. */ includes?: Array; } diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index ff21eee96e007..6300880a913e2 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -25,11 +25,6 @@ interface IWatchRequest { /** * A set of glob patterns or paths to exclude from watching. - * - * Paths or basic glob patterns that are relative will be - * resolved to an absolute path using the currently opened - * workspace. Complex glob patterns must match on absolute - * paths via leading or trailing `**`. */ excludes: string[]; @@ -37,11 +32,6 @@ interface IWatchRequest { * An optional set of glob patterns or paths to include for * watching. If not provided, all paths are considered for * events. - * - * Paths or basic glob patterns that are relative will be - * resolved to an absolute path using the currently opened - * workspace. Complex glob patterns must match on absolute - * paths via leading or trailing `**`. */ includes?: Array; } diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 260e6d39331ce..84ecc1532115f 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -10,14 +10,13 @@ import { DeferredPromise, RunOnceScheduler, RunOnceWorker, ThrottledWorker } fro import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; -import { isEqualOrParent, randomPath } from 'vs/base/common/extpath'; +import { randomPath } from 'vs/base/common/extpath'; import { GLOBSTAR, ParsedPattern, patternsEquals } from 'vs/base/common/glob'; import { Disposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { normalizeNFC } from 'vs/base/common/normalization'; -import { dirname, isAbsolute, join, normalize, sep } from 'vs/base/common/path'; +import { dirname, normalize } from 'vs/base/common/path'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { rtrim } from 'vs/base/common/strings'; 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'; @@ -68,18 +67,6 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { ] ); - private static readonly GLOB_MARKERS = { - Star: '*', - GlobStar: '**', - GlobStarPosix: '**/**', - GlobStarWindows: '**\\**', - GlobStarPathStartPosix: '**/', - GlobStarPathEndPosix: '/**', - StarPathEndPosix: '/*', - GlobStarPathStartWindows: '**\\', - GlobStarPathEndWindows: '\\**' - }; - private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events'; private readonly _onDidChangeFile = this._register(new Emitter()); @@ -184,98 +171,6 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } } - protected toExcludePaths(path: string, excludes: string[] | undefined): string[] | undefined { - if (!Array.isArray(excludes)) { - return undefined; - } - - const excludePaths = new Set(); - - // Parcel watcher currently does not support glob patterns - // for native exclusions. As long as that is the case, try - // to convert exclude patterns into absolute paths that the - // watcher supports natively to reduce the overhead at the - // level of the file watcher as much as possible. - // Refs: https://github.com/parcel-bundler/watcher/issues/64 - for (const exclude of excludes) { - const isGlob = exclude.includes(ParcelWatcher.GLOB_MARKERS.Star); - - // Glob pattern: check for typical patterns and convert - let normalizedExclude: string | undefined = undefined; - if (isGlob) { - - // Examples: **, **/**, **\** - if ( - exclude === ParcelWatcher.GLOB_MARKERS.GlobStar || - exclude === ParcelWatcher.GLOB_MARKERS.GlobStarPosix || - exclude === ParcelWatcher.GLOB_MARKERS.GlobStarWindows - ) { - normalizedExclude = path; - } - - // Examples: - // - **/node_modules/** - // - **/.git/objects/** - // - **/build-folder - // - output/** - else { - const startsWithGlobStar = exclude.startsWith(ParcelWatcher.GLOB_MARKERS.GlobStarPathStartPosix) || exclude.startsWith(ParcelWatcher.GLOB_MARKERS.GlobStarPathStartWindows); - const endsWithGlobStar = exclude.endsWith(ParcelWatcher.GLOB_MARKERS.GlobStarPathEndPosix) || exclude.endsWith(ParcelWatcher.GLOB_MARKERS.GlobStarPathEndWindows); - if (startsWithGlobStar || endsWithGlobStar) { - if (startsWithGlobStar && endsWithGlobStar) { - normalizedExclude = exclude.substring(ParcelWatcher.GLOB_MARKERS.GlobStarPathStartPosix.length, exclude.length - ParcelWatcher.GLOB_MARKERS.GlobStarPathEndPosix.length); - } else if (startsWithGlobStar) { - normalizedExclude = exclude.substring(ParcelWatcher.GLOB_MARKERS.GlobStarPathStartPosix.length); - } else { - normalizedExclude = exclude.substring(0, exclude.length - ParcelWatcher.GLOB_MARKERS.GlobStarPathEndPosix.length); - } - } - - // Support even more glob patterns on Linux where we know - // that each folder requires a file handle to watch. - // Examples: - // - node_modules/* (full form: **/node_modules/*/**) - if (isLinux && normalizedExclude) { - const endsWithStar = normalizedExclude?.endsWith(ParcelWatcher.GLOB_MARKERS.StarPathEndPosix); - if (endsWithStar) { - normalizedExclude = normalizedExclude.substring(0, normalizedExclude.length - ParcelWatcher.GLOB_MARKERS.StarPathEndPosix.length); - } - } - } - } - - // Not a glob pattern, take as is - else { - normalizedExclude = exclude; - } - - if (!normalizedExclude || normalizedExclude.includes(ParcelWatcher.GLOB_MARKERS.Star)) { - continue; // skip for parcel (will be applied later by our glob matching) - } - - // Absolute path: normalize to watched path and - // exclude if not a parent of it otherwise. - if (isAbsolute(normalizedExclude)) { - if (!isEqualOrParent(normalizedExclude, path, !isLinux)) { - continue; // exclude points to path outside of watched folder, ignore - } - - // convert to relative path to ensure we - // get the correct path casing going forward - normalizedExclude = normalizedExclude.substr(path.length); - } - - // Finally take as relative path joined to watched path - excludePaths.add(rtrim(join(path, normalizedExclude), sep)); - } - - if (excludePaths.size > 0) { - return Array.from(excludePaths); - } - - return undefined; - } - private startPolling(request: IRecursiveWatchRequest, pollingInterval: number, restarts = 0): void { const cts = new CancellationTokenSource(); @@ -305,13 +200,10 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); - // Warm up exclude/include patterns for usage - const excludePatterns = parseWatcherPatterns(request.path, request.excludes); + // Warm up include patterns for usage const includePatterns = request.includes ? parseWatcherPatterns(request.path, request.includes) : undefined; - const ignore = this.toExcludePaths(realPath, watcher.request.excludes); - - this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}' and native excludes '${ignore?.join(', ')}'`); + this.trace(`Started watching: '${realPath}' with polling interval '${pollingInterval}'`); let counter = 0; @@ -324,18 +216,18 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // We already ran before, check for events since if (counter > 1) { - const parcelEvents = await parcelWatcher.getEventsSince(realPath, snapshotFile, { ignore, backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); + const parcelEvents = await parcelWatcher.getEventsSince(realPath, snapshotFile, { ignore: request.excludes, backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); if (cts.token.isCancellationRequested) { return; } // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, excludePatterns, includePatterns, realPathDiffers, realPathLength); + this.onParcelEvents(parcelEvents, watcher, includePatterns, realPathDiffers, realPathLength); } // Store a snapshot of files to the snapshot file - await parcelWatcher.writeSnapshot(realPath, snapshotFile, { ignore, backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); + await parcelWatcher.writeSnapshot(realPath, snapshotFile, { ignore: request.excludes, backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); // Signal we are ready now when the first snapshot was written if (counter === 1) { @@ -379,11 +271,9 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { // Path checks for symbolic links / wrong casing const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); - // Warm up exclude/include patterns for usage - const excludePatterns = parseWatcherPatterns(request.path, request.excludes); + // Warm up include patterns for usage const includePatterns = request.includes ? parseWatcherPatterns(request.path, request.includes) : undefined; - const ignore = this.toExcludePaths(realPath, watcher.request.excludes); parcelWatcher.subscribe(realPath, (error, parcelEvents) => { if (watcher.token.isCancellationRequested) { return; // return early when disposed @@ -398,12 +288,12 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } // Handle & emit events - this.onParcelEvents(parcelEvents, watcher, excludePatterns, includePatterns, realPathDiffers, realPathLength); + this.onParcelEvents(parcelEvents, watcher, includePatterns, realPathDiffers, realPathLength); }, { backend: ParcelWatcher.PARCEL_WATCHER_BACKEND, - ignore + ignore: watcher.request.excludes }).then(parcelWatcher => { - this.trace(`Started watching: '${realPath}' with backend '${ParcelWatcher.PARCEL_WATCHER_BACKEND}' and native excludes '${ignore?.join(', ')}'`); + this.trace(`Started watching: '${realPath}' with backend '${ParcelWatcher.PARCEL_WATCHER_BACKEND}'`); instance.complete(parcelWatcher); }).catch(error => { @@ -413,18 +303,18 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { }); } - private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: IParcelWatcherInstance, excludes: ParsedPattern[], includes: ParsedPattern[] | undefined, realPathDiffers: boolean, realPathLength: number): void { + private onParcelEvents(parcelEvents: parcelWatcher.Event[], watcher: IParcelWatcherInstance, includes: ParsedPattern[] | undefined, realPathDiffers: boolean, realPathLength: number): void { if (parcelEvents.length === 0) { return; } // Normalize events: handle NFC normalization and symlinks // It is important to do this before checking for includes - // and excludes to check on the original path. + // to check on the original path. this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength); - // Check for excludes - const includedEvents = this.handleExcludeIncludes(parcelEvents, excludes, includes); + // Check for includes + const includedEvents = this.handleIncludes(parcelEvents, includes); // Add to event aggregator for later processing for (const includedEvent of includedEvents) { @@ -432,7 +322,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } } - private handleExcludeIncludes(parcelEvents: parcelWatcher.Event[], excludes: ParsedPattern[], includes: ParsedPattern[] | undefined): IDiskFileChange[] { + private handleIncludes(parcelEvents: parcelWatcher.Event[], includes: ParsedPattern[] | undefined): IDiskFileChange[] { const events: IDiskFileChange[] = []; for (const { path, type: parcelEventType } of parcelEvents) { @@ -441,12 +331,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { this.trace(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`); } - // Add to buffer unless excluded or not included (not if explicitly disabled) - if (excludes.some(exclude => exclude(path))) { - if (this.verboseLogging) { - this.trace(` >> ignored (excluded) ${path}`); - } - } else if (includes && includes.length > 0 && !includes.some(include => include(path))) { + // Apply include filter if any + if (includes && includes.length > 0 && !includes.some(include => include(path))) { if (this.verboseLogging) { this.trace(` >> ignored (not included) ${path}`); } diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index c3e1028ae2f52..43e451c109c96 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { realpathSync } from 'fs'; import { tmpdir } from 'os'; import { timeout } from 'vs/base/common/async'; -import { dirname, join, sep } from 'vs/base/common/path'; +import { 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, getPathFromAmdModule, getRandomTestPath } from 'vs/base/test/node/testUtils'; @@ -46,10 +46,6 @@ import { ltrim } from 'vs/base/common/strings'; await watcher.ready; } } - - testToExcludePaths(path: string, excludes: string[] | undefined): string[] | undefined { - return super.toExcludePaths(path, excludes); - } } let testDir: string; @@ -465,6 +461,32 @@ import { ltrim } from 'vs/base/common/strings'; return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); }); + test('excludes are supported (path)', async function () { + return testExcludes([join(realpathSync(testDir), 'deep')]); + }); + + test('excludes are supported (glob)', function () { + return testExcludes(['deep/**']); + }); + + async function testExcludes(excludes: string[]) { + await watcher.watch([{ path: testDir, excludes, recursive: true }]); + + // New file (*.txt) + const newTextFilePath = join(testDir, 'deep', 'newFile.txt'); + const changeFuture = awaitEvent(watcher, newTextFilePath, FileChangeType.ADDED); + await Promises.writeFile(newTextFilePath, 'Hello World'); + + const res = await Promise.any([ + timeout(500).then(() => true), + changeFuture.then(() => false) + ]); + + if (!res) { + assert.fail('Unexpected change event'); + } + } + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); @@ -561,115 +583,4 @@ import { ltrim } from 'vs/base/common/strings'; test('should ignore when everything excluded', () => { assert.deepStrictEqual(watcher.testNormalizePaths(['/foo/bar', '/bar'], ['**', 'something']), []); }); - - test('excludes are converted to absolute paths', () => { - - // undefined / empty - - assert.strictEqual(watcher.testToExcludePaths(testDir, undefined), undefined); - assert.strictEqual(watcher.testToExcludePaths(testDir, []), undefined); - - // absolute paths - - let excludes = watcher.testToExcludePaths(testDir, [testDir]); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], testDir); - - excludes = watcher.testToExcludePaths(testDir, [`${testDir}${sep}`, join(testDir, 'foo', 'bar'), `${join(testDir, 'other', 'deep')}${sep}`]); - assert.strictEqual(excludes?.length, 3); - assert.strictEqual(excludes[0], testDir); - assert.strictEqual(excludes[1], join(testDir, 'foo', 'bar')); - assert.strictEqual(excludes[2], join(testDir, 'other', 'deep')); - - // wrong casing is normalized for root - if (!isLinux) { - excludes = watcher.testToExcludePaths(testDir, [join(testDir.toUpperCase(), 'node_modules', '**')]); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, 'node_modules')); - } - - // exclude ignored if not parent of watched dir - excludes = watcher.testToExcludePaths(testDir, [join(dirname(testDir), 'node_modules', '**')]); - assert.strictEqual(excludes, undefined); - - // relative paths - - excludes = watcher.testToExcludePaths(testDir, ['.']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], testDir); - - excludes = watcher.testToExcludePaths(testDir, ['foo', `bar${sep}`, join('foo', 'bar'), `${join('other', 'deep')}${sep}`]); - assert.strictEqual(excludes?.length, 4); - assert.strictEqual(excludes[0], join(testDir, 'foo')); - assert.strictEqual(excludes[1], join(testDir, 'bar')); - assert.strictEqual(excludes[2], join(testDir, 'foo', 'bar')); - assert.strictEqual(excludes[3], join(testDir, 'other', 'deep')); - - // simple globs (relative) - - excludes = watcher.testToExcludePaths(testDir, ['**']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], testDir); - - excludes = watcher.testToExcludePaths(testDir, ['**/**']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], testDir); - - excludes = watcher.testToExcludePaths(testDir, ['**\\**']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], testDir); - - excludes = watcher.testToExcludePaths(testDir, ['**/node_modules/**']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, 'node_modules')); - - excludes = watcher.testToExcludePaths(testDir, ['**/.git/objects/**']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, '.git', 'objects')); - - excludes = watcher.testToExcludePaths(testDir, ['**/node_modules']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, 'node_modules')); - - excludes = watcher.testToExcludePaths(testDir, ['**/.git/objects']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, '.git', 'objects')); - - excludes = watcher.testToExcludePaths(testDir, ['node_modules/**']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, 'node_modules')); - - excludes = watcher.testToExcludePaths(testDir, ['.git/objects/**']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, '.git', 'objects')); - - // simple globs (absolute) - - excludes = watcher.testToExcludePaths(testDir, [join(testDir, 'node_modules', '**')]); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, 'node_modules')); - - // Linux: more restrictive glob treatment - if (isLinux) { - excludes = watcher.testToExcludePaths(testDir, ['**/node_modules/*/**']); - assert.strictEqual(excludes?.length, 1); - assert.strictEqual(excludes[0], join(testDir, 'node_modules')); - } - - // unsupported globs - - else { - excludes = watcher.testToExcludePaths(testDir, ['**/node_modules/*/**']); - assert.strictEqual(excludes, undefined); - } - - excludes = watcher.testToExcludePaths(testDir, ['**/*.js']); - assert.strictEqual(excludes, undefined); - - excludes = watcher.testToExcludePaths(testDir, ['*.js']); - assert.strictEqual(excludes, undefined); - - excludes = watcher.testToExcludePaths(testDir, ['*']); - assert.strictEqual(excludes, undefined); - }); }); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 99f977f4e971b..ffd761948639e 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -248,7 +248,7 @@ configurationRegistry.registerConfiguration({ 'files.watcherExclude': { 'type': 'object', 'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true }, - 'markdownDescription': nls.localize('watcherExclude', "Configure paths or glob patterns to exclude from file watching. Paths or basic glob patterns that are relative (for example `build/output` or `*.js`) will be resolved to an absolute path using the currently opened workspace. Complex glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). 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)."), + 'markdownDescription': nls.localize('watcherExclude', "Configure paths or 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 }, 'files.watcherInclude': {