From 09fe726382d3a0f709f497a347b1d931844f6674 Mon Sep 17 00:00:00 2001 From: dawsbot <3408480+dawsbot@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:27:01 -0600 Subject: [PATCH 1/3] feat: respect built-in files.exclude / search.exclude (#31) Add a `relativePath.useBuiltInExcludes` setting so the picker can also honor VS Code's built-in `files.exclude` and/or `search.exclude`, letting users avoid duplicating those globs in `relativePath.ignore`. - collectBuiltInExcludes() reads the enabled globs (value === true) from either/both settings and, for folder-style globs, adds the `/**` descendant form so the file-creation watcher matches contents the same way findFiles prunes them. - The merged list feeds both the findFiles scan and the watcher. - The config watcher rescans when the mode changes, or when files.exclude / search.exclude change while the mode is active. - buildExcludeGlob now dedupes and emits a single glob bare (a one-item brace is not alternation). Defaults to "none", so existing behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++ README.md | 5 +++ package.json | 17 +++++++++ src/extension.ts | 50 ++++++++++++++++++++++-- src/globs.ts | 59 ++++++++++++++++++++++++++++- test/globs.test.ts | 94 +++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 222 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9688dd3..730777a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- New `relativePath.useBuiltInExcludes` setting lets the picker also honor VS Code's built-in `files.exclude` and/or `search.exclude`, so you no longer have to duplicate those globs in `relativePath.ignore`. Options: `none` (default), `files`, `search`, `both`. (#31) + ## 1.7.0 Big-workspace release: scanning is now bounded, filters are enforced everywhere, and defaults got a refresh. diff --git a/README.md b/README.md index 6e7ca23..fc25f84 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,11 @@ They can be set in user preferences (`ctrl+,` or `cmd+,`) or workspace settings "**/objd/**" ], +// Also hide files matched by VS Code's built-in `files.exclude` and/or +// `search.exclude` settings, so you don't have to duplicate them in +// `relativePath.ignore`. One of: "none" (default), "files", "search", "both". +"relativePath.useBuiltInExcludes": "none", + // Excludes the extension from the relative path url (Useful for systemjs imports). "relativePath.removeExtension": false, diff --git a/package.json b/package.json index 9d3608d..d250183 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,23 @@ "type": "string" } }, + "relativePath.useBuiltInExcludes": { + "type": "string", + "enum": [ + "none", + "files", + "search", + "both" + ], + "enumDescriptions": [ + "Use only relativePath.ignore.", + "Also hide files matched by the built-in files.exclude setting.", + "Also hide files matched by the built-in search.exclude setting.", + "Also hide files matched by either files.exclude or search.exclude." + ], + "default": "none", + "description": "Also hide files matched by VS Code's built-in 'files.exclude' and/or 'search.exclude' settings, so you don't have to duplicate them in 'relativePath.ignore'." + }, "relativePath.removeExtension": { "type": "boolean", "default": false, diff --git a/src/extension.ts b/src/extension.ts index 66805f2..480bc59 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,7 +17,12 @@ import { } from "vscode"; import { getClosestMatches } from "./closest-match"; import { shouldAddToCache } from "./glob-match"; -import { buildExcludeGlob, normalizeIncludeGlob } from "./globs"; +import { + buildExcludeGlob, + BuiltInExcludeMode, + collectBuiltInExcludes, + normalizeIncludeGlob, +} from "./globs"; import { partitionByRecency, recordRecentPath } from "./recent-paths"; import { replaceSelections } from "./replace-selections"; import { isTruncated, resolveMaxResults } from "./search-limit"; @@ -105,7 +110,7 @@ class RelativePath { shouldAddToCache( filePath.slice(workspacePrefix.length), normalizeIncludeGlob(includeGlob), - this._configuration.get("ignore") + this.buildIgnoreGlobs() ) ) { this._fileNames.push(filePath); @@ -167,7 +172,7 @@ class RelativePath { this._workspacePath, normalizeIncludeGlob(includeGlob) ); - const exclude = buildExcludeGlob(this._configuration.get("ignore")); + const exclude = buildExcludeGlob(this.buildIgnoreGlobs()); const maxResults = resolveMaxResults( this._configuration.get("maxFilesCached") ); @@ -220,6 +225,32 @@ class RelativePath { this.updateFiles(skipOpen); } + // The globs excluded from both the findFiles scan and the file-creation + // watcher: the user's `relativePath.ignore` plus, when opted in via + // `relativePath.useBuiltInExcludes`, VS Code's own `files.exclude` and/or + // `search.exclude` so those don't have to be duplicated (issue #31). + private buildIgnoreGlobs(): string[] { + const ignore: string[] = this._configuration.get("ignore") ?? []; + const mode = + this._configuration.get("useBuiltInExcludes"); + + if (!mode || mode === "none") { + return ignore; + } + + const filesExclude = workspace + .getConfiguration("files") + .get>("exclude"); + const searchExclude = workspace + .getConfiguration("search") + .get>("exclude"); + + return [ + ...ignore, + ...collectBuiltInExcludes(mode, filesExclude, searchExclude), + ]; + } + // Compares the ignore property of _configuration to lastConfig private ignoreWasUpdated( currentIgnore: Array, @@ -240,6 +271,12 @@ class RelativePath { const lastConfig = this._configuration; this._configuration = workspace.getConfiguration("relativePath"); + // When built-in excludes are active, VS Code's own `files.exclude` + // and `search.exclude` also shape our cache, so a change to either + // must trigger a rescan. + const builtInMode = this._configuration.get("useBuiltInExcludes"); + const builtInActive = builtInMode && builtInMode !== "none"; + // Handle updates to the properties that shape the cached file // list if there are any if ( @@ -249,7 +286,12 @@ class RelativePath { ) || this._configuration.maxFilesCached !== lastConfig.maxFilesCached || - this._configuration.includeGlob !== lastConfig.includeGlob + this._configuration.includeGlob !== lastConfig.includeGlob || + this._configuration.useBuiltInExcludes !== + lastConfig.useBuiltInExcludes || + (builtInActive && + (e.affectsConfiguration("files.exclude") || + e.affectsConfiguration("search.exclude"))) ) { this.updateFiles(true); } diff --git a/src/globs.ts b/src/globs.ts index 9540f6d..46bed59 100644 --- a/src/globs.ts +++ b/src/globs.ts @@ -7,7 +7,64 @@ export function buildExcludeGlob(ignore: string[] | undefined): string | null { return null; } - return `{${ignore.map(escapeBraceSegment).join(",")}}`; + const unique = [...new Set(ignore)]; + // A single-item brace (`{a}`) is not alternation, so the glob engine would + // read it as the literal characters `{a}`. Emit the bare pattern instead + // so its own `{a,b}` groups keep working. + if (unique.length === 1) { + return unique[0]; + } + + return `{${unique.map(escapeBraceSegment).join(",")}}`; +} + +export type BuiltInExcludeMode = "none" | "files" | "search" | "both"; + +// Collect the globs from VS Code's built-in `files.exclude` / `search.exclude` +// settings so they can be merged into the exclude passed to findFiles. Both +// settings are stored as a map of glob -> value, where the value is `true`, +// `false`, or a `{ when }` sibling clause. We honor only the globs explicitly +// enabled with `true`: `false` (a user re-including a path) and `when` clauses +// (which need sibling files we can't cheaply resolve here) are skipped. +export function collectBuiltInExcludes( + mode: BuiltInExcludeMode | undefined, + filesExclude: Record | undefined, + searchExclude: Record | undefined +): string[] { + const globs: string[] = []; + if (mode === "files" || mode === "both") { + addEnabledGlobs(filesExclude, globs); + } + if (mode === "search" || mode === "both") { + addEnabledGlobs(searchExclude, globs); + } + return globs; +} + +function addEnabledGlobs( + exclude: Record | undefined, + into: string[] +): void { + if (!exclude) { + return; + } + + for (const glob of Object.keys(exclude)) { + if (exclude[glob] !== true) { + continue; + } + + into.push(glob); + // The built-in settings write folders as `**/node_modules` (no trailing + // `/**`). findFiles prunes that folder's contents, but our file-creation + // watcher matches file paths directly, so it also needs the descendant + // form to keep files created later inside an excluded folder out of the + // cache. Appending `/**` to a file-name pattern is harmless: it simply + // never matches. + if (!glob.endsWith("/**")) { + into.push(`${glob}/**`); + } + } } function escapeBraceSegment(pattern: string): string { diff --git a/test/globs.test.ts b/test/globs.test.ts index 9b3ac22..a6fd2f0 100644 --- a/test/globs.test.ts +++ b/test/globs.test.ts @@ -1,7 +1,10 @@ import * as assert from "node:assert/strict"; import { test } from "node:test"; - -import { buildExcludeGlob, normalizeIncludeGlob } from "../src/globs"; +import { + buildExcludeGlob, + collectBuiltInExcludes, + normalizeIncludeGlob, +} from "../src/globs"; test("normalizes leading slash from include glob for RelativePattern", () => { assert.equal(normalizeIncludeGlob("/**/*.*"), "**/*.*"); @@ -19,3 +22,90 @@ test("combines ignore globs without changing comma or brace literals", () => { "{**/node_modules/**,**/fixtures/\\{a\\,b\\}/**}" ); }); + +test("emits a single ignore glob bare so its own {a,b} groups keep working", () => { + assert.equal( + buildExcludeGlob(["**/fixtures/{a,b}/**"]), + "**/fixtures/{a,b}/**" + ); +}); + +test("dedupes repeated ignore globs", () => { + assert.equal( + buildExcludeGlob(["**/node_modules/**", "**/node_modules/**"]), + "**/node_modules/**" + ); +}); + +test("collects no built-in excludes when the mode is none or unset", () => { + const files = { "**/.git": true }; + const search = { "**/node_modules": true }; + assert.deepEqual(collectBuiltInExcludes("none", files, search), []); + assert.deepEqual(collectBuiltInExcludes(undefined, files, search), []); +}); + +test("collects only files.exclude globs in files mode", () => { + assert.deepEqual( + collectBuiltInExcludes( + "files", + { "**/.git": true, "**/.DS_Store": true }, + { "**/node_modules": true } + ), + ["**/.git", "**/.git/**", "**/.DS_Store", "**/.DS_Store/**"] + ); +}); + +test("collects only search.exclude globs in search mode", () => { + assert.deepEqual( + collectBuiltInExcludes( + "search", + { "**/.git": true }, + { "**/dist": true } + ), + ["**/dist", "**/dist/**"] + ); +}); + +test("collects both sources in both mode", () => { + assert.deepEqual( + collectBuiltInExcludes( + "both", + { "**/.git": true }, + { "**/dist": true } + ), + ["**/.git", "**/.git/**", "**/dist", "**/dist/**"] + ); +}); + +test("honors only globs explicitly enabled with true", () => { + assert.deepEqual( + collectBuiltInExcludes( + "files", + { + "**/.git": true, + "**/kept": false, + "**/when-clause": { when: "$(basename).ts" }, + }, + undefined + ), + ["**/.git", "**/.git/**"] + ); +}); + +test("does not append a descendant form to globs already ending in /**", () => { + assert.deepEqual( + collectBuiltInExcludes( + "files", + { "**/node_modules/**": true }, + undefined + ), + ["**/node_modules/**"] + ); +}); + +test("keeps file-name patterns and adds a harmless descendant form", () => { + assert.deepEqual( + collectBuiltInExcludes("files", { "**/*.pyc": true }, undefined), + ["**/*.pyc", "**/*.pyc/**"] + ); +}); From 9c6570c79d378f5f6f590276a16401804f7ce40d Mon Sep 17 00:00:00 2001 From: dawsbot <3408480+dawsbot@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:41:10 -0600 Subject: [PATCH 2/3] feat: default useBuiltInExcludes to "both" Honor VS Code's built-in files.exclude and search.exclude out of the box, so users get sensible excludes without duplicating them into relativePath.ignore. Set the new setting to "none" to restore the prior behavior of using only relativePath.ignore. This changes default behavior: files under files.exclude / search.exclude are now hidden from the picker unless opted out. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- README.md | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 730777a..0c307f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Unreleased -- New `relativePath.useBuiltInExcludes` setting lets the picker also honor VS Code's built-in `files.exclude` and/or `search.exclude`, so you no longer have to duplicate those globs in `relativePath.ignore`. Options: `none` (default), `files`, `search`, `both`. (#31) +- The picker now also honors VS Code's built-in `files.exclude` and `search.exclude` by default, so you no longer have to duplicate those globs in `relativePath.ignore`. Tune this with the new `relativePath.useBuiltInExcludes` setting: `both` (default), `files`, `search`, or `none` to restore the previous behavior of using only `relativePath.ignore`. (#31) ## 1.7.0 diff --git a/README.md b/README.md index fc25f84..9e6b449 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ They can be set in user preferences (`ctrl+,` or `cmd+,`) or workspace settings // Also hide files matched by VS Code's built-in `files.exclude` and/or // `search.exclude` settings, so you don't have to duplicate them in -// `relativePath.ignore`. One of: "none" (default), "files", "search", "both". -"relativePath.useBuiltInExcludes": "none", +// `relativePath.ignore`. One of: "both" (default), "files", "search", "none". +"relativePath.useBuiltInExcludes": "both", // Excludes the extension from the relative path url (Useful for systemjs imports). "relativePath.removeExtension": false, diff --git a/package.json b/package.json index d250183..aee31a3 100644 --- a/package.json +++ b/package.json @@ -95,8 +95,8 @@ "Also hide files matched by the built-in search.exclude setting.", "Also hide files matched by either files.exclude or search.exclude." ], - "default": "none", - "description": "Also hide files matched by VS Code's built-in 'files.exclude' and/or 'search.exclude' settings, so you don't have to duplicate them in 'relativePath.ignore'." + "default": "both", + "description": "Also hide files matched by VS Code's built-in 'files.exclude' and/or 'search.exclude' settings, so you don't have to duplicate them in 'relativePath.ignore'. Set to 'none' to use only 'relativePath.ignore'." }, "relativePath.removeExtension": { "type": "boolean", From 3168e500f2f3b93e51c8b8f63c522ad9fe2f1993 Mon Sep 17 00:00:00 2001 From: dawsbot <3408480+dawsbot@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:58:34 -0600 Subject: [PATCH 3/3] refactor: replace useBuiltInExcludes enum with respectSearchExclude boolean The four-value enum over-promised. VS Code's findFiles always applies files.exclude to the scan (short of a null exclude, which would also drop our own ignore globs), so the "none" and "files" modes were misleading: files.exclude can't be turned off for the scan, and adding it there is a no-op. The only genuine user choice is whether to also honor search.exclude, which findFiles never applies on its own. - New boolean `relativePath.respectSearchExclude` (default true) gates search.exclude. - files.exclude is now always merged into the ignore globs so the file creation watcher stays consistent with the scan VS Code already prunes. - collectBuiltInExcludes(mode, files, search) becomes the single-source collectExcludeGlobs(exclude). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- README.md | 9 +++--- package.json | 20 +++---------- src/extension.ts | 57 ++++++++++++++++++------------------ src/globs.ts | 43 +++++++++------------------ test/globs.test.ts | 73 ++++++++++++---------------------------------- 6 files changed, 71 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c307f2..ce95d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Unreleased -- The picker now also honors VS Code's built-in `files.exclude` and `search.exclude` by default, so you no longer have to duplicate those globs in `relativePath.ignore`. Tune this with the new `relativePath.useBuiltInExcludes` setting: `both` (default), `files`, `search`, or `none` to restore the previous behavior of using only `relativePath.ignore`. (#31) +- The picker now also honors VS Code's built-in `search.exclude` by default, so you no longer have to duplicate those globs in `relativePath.ignore`. (`files.exclude` was already applied to the file scan by VS Code; it is now also applied to files created after the scan, keeping the cache consistent.) Opt out with the new `relativePath.respectSearchExclude: false`. (#31) ## 1.7.0 diff --git a/README.md b/README.md index 9e6b449..726e9f3 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,11 @@ They can be set in user preferences (`ctrl+,` or `cmd+,`) or workspace settings "**/objd/**" ], -// Also hide files matched by VS Code's built-in `files.exclude` and/or -// `search.exclude` settings, so you don't have to duplicate them in -// `relativePath.ignore`. One of: "both" (default), "files", "search", "none". -"relativePath.useBuiltInExcludes": "both", +// Also hide files matched by VS Code's built-in `search.exclude`, so you don't +// have to duplicate those globs in `relativePath.ignore`. (`files.exclude` is +// always respected -- VS Code applies it to the file scan regardless.) Set to +// false to use only `relativePath.ignore` plus `files.exclude`. +"relativePath.respectSearchExclude": true, // Excludes the extension from the relative path url (Useful for systemjs imports). "relativePath.removeExtension": false, diff --git a/package.json b/package.json index aee31a3..5518a74 100644 --- a/package.json +++ b/package.json @@ -81,22 +81,10 @@ "type": "string" } }, - "relativePath.useBuiltInExcludes": { - "type": "string", - "enum": [ - "none", - "files", - "search", - "both" - ], - "enumDescriptions": [ - "Use only relativePath.ignore.", - "Also hide files matched by the built-in files.exclude setting.", - "Also hide files matched by the built-in search.exclude setting.", - "Also hide files matched by either files.exclude or search.exclude." - ], - "default": "both", - "description": "Also hide files matched by VS Code's built-in 'files.exclude' and/or 'search.exclude' settings, so you don't have to duplicate them in 'relativePath.ignore'. Set to 'none' to use only 'relativePath.ignore'." + "relativePath.respectSearchExclude": { + "type": "boolean", + "default": true, + "description": "Also hide files matched by VS Code's built-in 'search.exclude' setting, so you don't have to duplicate those globs in 'relativePath.ignore'. 'files.exclude' is always respected (VS Code applies it to the file scan regardless). Set to false to use only 'relativePath.ignore' plus 'files.exclude'." }, "relativePath.removeExtension": { "type": "boolean", diff --git a/src/extension.ts b/src/extension.ts index 480bc59..5c49285 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,8 +19,7 @@ import { getClosestMatches } from "./closest-match"; import { shouldAddToCache } from "./glob-match"; import { buildExcludeGlob, - BuiltInExcludeMode, - collectBuiltInExcludes, + collectExcludeGlobs, normalizeIncludeGlob, } from "./globs"; import { partitionByRecency, recordRecentPath } from "./recent-paths"; @@ -226,29 +225,31 @@ class RelativePath { } // The globs excluded from both the findFiles scan and the file-creation - // watcher: the user's `relativePath.ignore` plus, when opted in via - // `relativePath.useBuiltInExcludes`, VS Code's own `files.exclude` and/or - // `search.exclude` so those don't have to be duplicated (issue #31). + // watcher: the user's `relativePath.ignore` plus VS Code's own built-in + // excludes so those don't have to be duplicated (issue #31). + // + // `files.exclude` is always merged in: findFiles applies it to the scan no + // matter what we pass (short of a `null` exclude, which would also drop our + // own ignore globs), so we add it to keep the creation watcher consistent + // with that. `search.exclude` is opt-in via + // `relativePath.respectSearchExclude` (default true), because findFiles + // never applies it on its own. private buildIgnoreGlobs(): string[] { const ignore: string[] = this._configuration.get("ignore") ?? []; - const mode = - this._configuration.get("useBuiltInExcludes"); - - if (!mode || mode === "none") { - return ignore; - } const filesExclude = workspace .getConfiguration("files") .get>("exclude"); - const searchExclude = workspace - .getConfiguration("search") - .get>("exclude"); + const globs = [...ignore, ...collectExcludeGlobs(filesExclude)]; - return [ - ...ignore, - ...collectBuiltInExcludes(mode, filesExclude, searchExclude), - ]; + if (this._configuration.get("respectSearchExclude")) { + const searchExclude = workspace + .getConfiguration("search") + .get>("exclude"); + globs.push(...collectExcludeGlobs(searchExclude)); + } + + return globs; } // Compares the ignore property of _configuration to lastConfig @@ -271,11 +272,12 @@ class RelativePath { const lastConfig = this._configuration; this._configuration = workspace.getConfiguration("relativePath"); - // When built-in excludes are active, VS Code's own `files.exclude` - // and `search.exclude` also shape our cache, so a change to either - // must trigger a rescan. - const builtInMode = this._configuration.get("useBuiltInExcludes"); - const builtInActive = builtInMode && builtInMode !== "none"; + // VS Code's own `files.exclude` always shapes our cache, and + // `search.exclude` does too while it's respected, so a change to + // either must trigger a rescan. + const respectSearch = this._configuration.get( + "respectSearchExclude" + ); // Handle updates to the properties that shape the cached file // list if there are any @@ -287,11 +289,10 @@ class RelativePath { this._configuration.maxFilesCached !== lastConfig.maxFilesCached || this._configuration.includeGlob !== lastConfig.includeGlob || - this._configuration.useBuiltInExcludes !== - lastConfig.useBuiltInExcludes || - (builtInActive && - (e.affectsConfiguration("files.exclude") || - e.affectsConfiguration("search.exclude"))) + this._configuration.respectSearchExclude !== + lastConfig.respectSearchExclude || + e.affectsConfiguration("files.exclude") || + (respectSearch && e.affectsConfiguration("search.exclude")) ) { this.updateFiles(true); } diff --git a/src/globs.ts b/src/globs.ts index 46bed59..0ac89b7 100644 --- a/src/globs.ts +++ b/src/globs.ts @@ -18,43 +18,27 @@ export function buildExcludeGlob(ignore: string[] | undefined): string | null { return `{${unique.map(escapeBraceSegment).join(",")}}`; } -export type BuiltInExcludeMode = "none" | "files" | "search" | "both"; - -// Collect the globs from VS Code's built-in `files.exclude` / `search.exclude` -// settings so they can be merged into the exclude passed to findFiles. Both -// settings are stored as a map of glob -> value, where the value is `true`, -// `false`, or a `{ when }` sibling clause. We honor only the globs explicitly -// enabled with `true`: `false` (a user re-including a path) and `when` clauses -// (which need sibling files we can't cheaply resolve here) are skipped. -export function collectBuiltInExcludes( - mode: BuiltInExcludeMode | undefined, - filesExclude: Record | undefined, - searchExclude: Record | undefined +// Collect the enabled globs from one of VS Code's built-in exclude settings +// (`files.exclude` or `search.exclude`) so they can be merged into the exclude +// passed to findFiles and the file-creation watcher. Each setting is a map of +// glob -> value, where the value is `true`, `false`, or a `{ when }` sibling +// clause. We honor only the globs explicitly enabled with `true`: `false` (a +// user re-including a path) and `when` clauses (which need sibling files we +// can't cheaply resolve here) are skipped. +export function collectExcludeGlobs( + exclude: Record | undefined ): string[] { - const globs: string[] = []; - if (mode === "files" || mode === "both") { - addEnabledGlobs(filesExclude, globs); - } - if (mode === "search" || mode === "both") { - addEnabledGlobs(searchExclude, globs); - } - return globs; -} - -function addEnabledGlobs( - exclude: Record | undefined, - into: string[] -): void { if (!exclude) { - return; + return []; } + const globs: string[] = []; for (const glob of Object.keys(exclude)) { if (exclude[glob] !== true) { continue; } - into.push(glob); + globs.push(glob); // The built-in settings write folders as `**/node_modules` (no trailing // `/**`). findFiles prunes that folder's contents, but our file-creation // watcher matches file paths directly, so it also needs the descendant @@ -62,9 +46,10 @@ function addEnabledGlobs( // cache. Appending `/**` to a file-name pattern is harmless: it simply // never matches. if (!glob.endsWith("/**")) { - into.push(`${glob}/**`); + globs.push(`${glob}/**`); } } + return globs; } function escapeBraceSegment(pattern: string): string { diff --git a/test/globs.test.ts b/test/globs.test.ts index a6fd2f0..f9218cb 100644 --- a/test/globs.test.ts +++ b/test/globs.test.ts @@ -2,7 +2,7 @@ import * as assert from "node:assert/strict"; import { test } from "node:test"; import { buildExcludeGlob, - collectBuiltInExcludes, + collectExcludeGlobs, normalizeIncludeGlob, } from "../src/globs"; @@ -37,75 +37,38 @@ test("dedupes repeated ignore globs", () => { ); }); -test("collects no built-in excludes when the mode is none or unset", () => { - const files = { "**/.git": true }; - const search = { "**/node_modules": true }; - assert.deepEqual(collectBuiltInExcludes("none", files, search), []); - assert.deepEqual(collectBuiltInExcludes(undefined, files, search), []); +test("collects nothing from an undefined or empty exclude map", () => { + assert.deepEqual(collectExcludeGlobs(undefined), []); + assert.deepEqual(collectExcludeGlobs({}), []); }); -test("collects only files.exclude globs in files mode", () => { +test("collects each enabled glob with a folder-descendant form", () => { assert.deepEqual( - collectBuiltInExcludes( - "files", - { "**/.git": true, "**/.DS_Store": true }, - { "**/node_modules": true } - ), + collectExcludeGlobs({ "**/.git": true, "**/.DS_Store": true }), ["**/.git", "**/.git/**", "**/.DS_Store", "**/.DS_Store/**"] ); }); -test("collects only search.exclude globs in search mode", () => { - assert.deepEqual( - collectBuiltInExcludes( - "search", - { "**/.git": true }, - { "**/dist": true } - ), - ["**/dist", "**/dist/**"] - ); -}); - -test("collects both sources in both mode", () => { - assert.deepEqual( - collectBuiltInExcludes( - "both", - { "**/.git": true }, - { "**/dist": true } - ), - ["**/.git", "**/.git/**", "**/dist", "**/dist/**"] - ); -}); - test("honors only globs explicitly enabled with true", () => { assert.deepEqual( - collectBuiltInExcludes( - "files", - { - "**/.git": true, - "**/kept": false, - "**/when-clause": { when: "$(basename).ts" }, - }, - undefined - ), + collectExcludeGlobs({ + "**/.git": true, + "**/kept": false, + "**/when-clause": { when: "$(basename).ts" }, + }), ["**/.git", "**/.git/**"] ); }); test("does not append a descendant form to globs already ending in /**", () => { - assert.deepEqual( - collectBuiltInExcludes( - "files", - { "**/node_modules/**": true }, - undefined - ), - ["**/node_modules/**"] - ); + assert.deepEqual(collectExcludeGlobs({ "**/node_modules/**": true }), [ + "**/node_modules/**", + ]); }); test("keeps file-name patterns and adds a harmless descendant form", () => { - assert.deepEqual( - collectBuiltInExcludes("files", { "**/*.pyc": true }, undefined), - ["**/*.pyc", "**/*.pyc/**"] - ); + assert.deepEqual(collectExcludeGlobs({ "**/*.pyc": true }), [ + "**/*.pyc", + "**/*.pyc/**", + ]); });