Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased

- 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

Big-workspace release: scanning is now bounded, filters are enforced everywhere, and defaults got a refresh.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ They can be set in user preferences (`ctrl+,` or `cmd+,`) or workspace settings
"**/objd/**"
],

// 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,

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@
"type": "string"
}
},
"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",
"default": false,
Expand Down
51 changes: 47 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import {
} from "vscode";
import { getClosestMatches } from "./closest-match";
import { shouldAddToCache } from "./glob-match";
import { buildExcludeGlob, normalizeIncludeGlob } from "./globs";
import {
buildExcludeGlob,
collectExcludeGlobs,
normalizeIncludeGlob,
} from "./globs";
import { partitionByRecency, recordRecentPath } from "./recent-paths";
import { replaceSelections } from "./replace-selections";
import { isTruncated, resolveMaxResults } from "./search-limit";
Expand Down Expand Up @@ -105,7 +109,7 @@ class RelativePath {
shouldAddToCache(
filePath.slice(workspacePrefix.length),
normalizeIncludeGlob(includeGlob),
this._configuration.get("ignore")
this.buildIgnoreGlobs()
)
) {
this._fileNames.push(filePath);
Expand Down Expand Up @@ -167,7 +171,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")
);
Expand Down Expand Up @@ -220,6 +224,34 @@ class RelativePath {
this.updateFiles(skipOpen);
}

// The globs excluded from both the findFiles scan and the file-creation
// 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 filesExclude = workspace
.getConfiguration("files")
.get<Record<string, unknown>>("exclude");
const globs = [...ignore, ...collectExcludeGlobs(filesExclude)];

if (this._configuration.get("respectSearchExclude")) {
const searchExclude = workspace
.getConfiguration("search")
.get<Record<string, unknown>>("exclude");
globs.push(...collectExcludeGlobs(searchExclude));
}

return globs;
}

// Compares the ignore property of _configuration to lastConfig
private ignoreWasUpdated(
currentIgnore: Array<string>,
Expand All @@ -240,6 +272,13 @@ class RelativePath {
const lastConfig = this._configuration;
this._configuration = workspace.getConfiguration("relativePath");

// 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
if (
Expand All @@ -249,7 +288,11 @@ class RelativePath {
) ||
this._configuration.maxFilesCached !==
lastConfig.maxFilesCached ||
this._configuration.includeGlob !== lastConfig.includeGlob
this._configuration.includeGlob !== lastConfig.includeGlob ||
this._configuration.respectSearchExclude !==
lastConfig.respectSearchExclude ||
e.affectsConfiguration("files.exclude") ||
(respectSearch && e.affectsConfiguration("search.exclude"))
) {
this.updateFiles(true);
}
Expand Down
44 changes: 43 additions & 1 deletion src/globs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,49 @@ 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(",")}}`;
}

// 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<string, unknown> | undefined
): string[] {
if (!exclude) {
return [];
}

const globs: string[] = [];
for (const glob of Object.keys(exclude)) {
if (exclude[glob] !== true) {
continue;
}

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
// 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("/**")) {
globs.push(`${glob}/**`);
}
}
return globs;
}

function escapeBraceSegment(pattern: string): string {
Expand Down
57 changes: 55 additions & 2 deletions test/globs.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as assert from "node:assert/strict";
import { test } from "node:test";

import { buildExcludeGlob, normalizeIncludeGlob } from "../src/globs";
import {
buildExcludeGlob,
collectExcludeGlobs,
normalizeIncludeGlob,
} from "../src/globs";

test("normalizes leading slash from include glob for RelativePattern", () => {
assert.equal(normalizeIncludeGlob("/**/*.*"), "**/*.*");
Expand All @@ -19,3 +22,53 @@ 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 nothing from an undefined or empty exclude map", () => {
assert.deepEqual(collectExcludeGlobs(undefined), []);
assert.deepEqual(collectExcludeGlobs({}), []);
});

test("collects each enabled glob with a folder-descendant form", () => {
assert.deepEqual(
collectExcludeGlobs({ "**/.git": true, "**/.DS_Store": true }),
["**/.git", "**/.git/**", "**/.DS_Store", "**/.DS_Store/**"]
);
});

test("honors only globs explicitly enabled with true", () => {
assert.deepEqual(
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(collectExcludeGlobs({ "**/node_modules/**": true }), [
"**/node_modules/**",
]);
});

test("keeps file-name patterns and adds a harmless descendant form", () => {
assert.deepEqual(collectExcludeGlobs({ "**/*.pyc": true }), [
"**/*.pyc",
"**/*.pyc/**",
]);
});