Fix hidden workspace file suggestions (#1609)
This commit is contained in:
@@ -332,6 +332,82 @@ describe("searchWorkspaceEntries", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("suffix mode finds files under hidden workspace directories", async () => {
|
||||||
|
mkdirSync(path.join(workspaceDir, ".claude"), { recursive: true });
|
||||||
|
writeFileSync(path.join(workspaceDir, ".claude", "settings.local.json"), "{}");
|
||||||
|
|
||||||
|
const results = await searchWorkspaceEntries({
|
||||||
|
cwd: workspaceDir,
|
||||||
|
query: "settings.local.json",
|
||||||
|
limit: 20,
|
||||||
|
includeFiles: true,
|
||||||
|
includeDirectories: false,
|
||||||
|
matchMode: "suffix",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results).toEqual([{ path: ".claude/settings.local.json", kind: "file" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not suggest hidden directories even when includeDirectories is true", async () => {
|
||||||
|
mkdirSync(path.join(workspaceDir, ".claude"), { recursive: true });
|
||||||
|
writeFileSync(path.join(workspaceDir, ".claude", "settings.local.json"), "{}");
|
||||||
|
|
||||||
|
const results = await searchWorkspaceEntries({
|
||||||
|
cwd: workspaceDir,
|
||||||
|
query: "claude",
|
||||||
|
limit: 20,
|
||||||
|
includeFiles: true,
|
||||||
|
includeDirectories: true,
|
||||||
|
matchMode: "fuzzy",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.some((entry) => entry.path === ".claude" && entry.kind === "directory")).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(results).toContainEqual({
|
||||||
|
path: ".claude/settings.local.json",
|
||||||
|
kind: "file",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("path mode does not suggest hidden workspace directories", async () => {
|
||||||
|
mkdirSync(path.join(workspaceDir, ".claude"), { recursive: true });
|
||||||
|
writeFileSync(path.join(workspaceDir, ".claude", "settings.local.json"), "{}");
|
||||||
|
|
||||||
|
const results = await searchWorkspaceEntries({
|
||||||
|
cwd: workspaceDir,
|
||||||
|
query: "./",
|
||||||
|
limit: 20,
|
||||||
|
includeFiles: true,
|
||||||
|
includeDirectories: true,
|
||||||
|
matchMode: "fuzzy",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results).toContainEqual({
|
||||||
|
path: "README.md",
|
||||||
|
kind: "file",
|
||||||
|
});
|
||||||
|
expect(results.some((entry) => entry.path === ".claude" && entry.kind === "directory")).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not traverse .git while searching workspace files", async () => {
|
||||||
|
mkdirSync(path.join(workspaceDir, ".git", "objects", "ab"), { recursive: true });
|
||||||
|
writeFileSync(path.join(workspaceDir, ".git", "objects", "ab", "deadbeef"), "");
|
||||||
|
|
||||||
|
const results = await searchWorkspaceEntries({
|
||||||
|
cwd: workspaceDir,
|
||||||
|
query: "deadbeef",
|
||||||
|
limit: 20,
|
||||||
|
includeFiles: true,
|
||||||
|
includeDirectories: false,
|
||||||
|
matchMode: "suffix",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
// POSIX-only: creates and follows a symlink escape fixture.
|
// POSIX-only: creates and follows a symlink escape fixture.
|
||||||
it.skipIf(isWindows)(
|
it.skipIf(isWindows)(
|
||||||
"supports path-style queries and does not escape cwd through symlinks",
|
"supports path-style queries and does not escape cwd through symlinks",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const IGNORED_SUGGESTION_DIRECTORY_NAMES = new Set([
|
|||||||
"coverage",
|
"coverage",
|
||||||
"vendor",
|
"vendor",
|
||||||
"__pycache__",
|
"__pycache__",
|
||||||
|
".git",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export async function searchHomeDirectories(
|
export async function searchHomeDirectories(
|
||||||
@@ -318,6 +319,9 @@ async function searchWorkspaceWithinParentDirectory(input: {
|
|||||||
if (entry.kind === "file" && !input.includeFiles) {
|
if (entry.kind === "file" && !input.includeFiles) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isHiddenWorkspaceSuggestion(entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const rankedEntry = rankWorkspaceEntry({
|
const rankedEntry = rankWorkspaceEntry({
|
||||||
absolutePath: entry.absolutePath,
|
absolutePath: entry.absolutePath,
|
||||||
kind: entry.kind,
|
kind: entry.kind,
|
||||||
@@ -385,6 +389,10 @@ async function searchWorkspaceAcrossTree(input: {
|
|||||||
if (entry.kind === "directory" && !input.includeDirectories) {
|
if (entry.kind === "directory" && !input.includeDirectories) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Hidden directories are traversed, but not offered as suggestions.
|
||||||
|
if (isHiddenWorkspaceSuggestion(entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (entry.kind === "file" && !input.includeFiles) {
|
if (entry.kind === "file" && !input.includeFiles) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -867,10 +875,19 @@ async function listWorkspaceChildEntries(input: {
|
|||||||
const dirents = await readdir(input.directory, { withFileTypes: true }).catch(
|
const dirents = await readdir(input.directory, { withFileTypes: true }).catch(
|
||||||
() => [] as Dirent[],
|
() => [] as Dirent[],
|
||||||
);
|
);
|
||||||
const candidates = dirents.filter(
|
const candidates = dirents.filter((dirent) => {
|
||||||
(dirent) =>
|
if (isIgnoredSuggestionDirectoryName(dirent.name)) {
|
||||||
!isHiddenDirectoryName(dirent.name) && !isIgnoredSuggestionDirectoryName(dirent.name),
|
return false;
|
||||||
);
|
}
|
||||||
|
// Hidden directories must remain traversable so file links like
|
||||||
|
// `.claude/settings.local.json` can resolve, but hidden files (e.g.
|
||||||
|
// `.DS_Store`) should never be suggested.
|
||||||
|
if (dirent.isFile() && isHiddenDirectoryName(dirent.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const resolved = await Promise.all(
|
const resolved = await Promise.all(
|
||||||
candidates.map(async (dirent) => {
|
candidates.map(async (dirent) => {
|
||||||
const candidatePath = path.join(input.directory, dirent.name);
|
const candidatePath = path.join(input.directory, dirent.name);
|
||||||
@@ -960,6 +977,10 @@ function isHiddenDirectoryName(name: string): boolean {
|
|||||||
return name.startsWith(".");
|
return name.startsWith(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isHiddenWorkspaceSuggestion(entry: ChildWorkspaceEntry): boolean {
|
||||||
|
return isHiddenDirectoryName(entry.name);
|
||||||
|
}
|
||||||
|
|
||||||
function isIgnoredSuggestionDirectoryName(name: string): boolean {
|
function isIgnoredSuggestionDirectoryName(name: string): boolean {
|
||||||
return IGNORED_SUGGESTION_DIRECTORY_NAMES.has(name);
|
return IGNORED_SUGGESTION_DIRECTORY_NAMES.has(name);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user