feat: Multi-Directory Workspace Support (part1: add --include-directories option) (#4605)

Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
Yuki Okita
2025-07-31 05:38:20 +09:00
committed by GitHub
parent 8fabce2c04
commit cb6a2161fe
44 changed files with 1913 additions and 253 deletions

View File

@@ -43,6 +43,7 @@ export interface UseCompletionReturn {
export function useCompletion(
buffer: TextBuffer,
dirs: readonly string[],
cwd: string,
slashCommands: readonly SlashCommand[],
commandContext: CommandContext,
@@ -328,8 +329,6 @@ export function useCompletion(
: partialPath.substring(lastSlashIndex + 1),
);
const baseDirAbsolute = path.resolve(cwd, baseDirRelative);
let isMounted = true;
const findFilesRecursively = async (
@@ -358,7 +357,7 @@ export function useCompletion(
const entryPathRelative = path.join(currentRelativePath, entry.name);
const entryPathFromRoot = path.relative(
cwd,
startDir,
path.join(startDir, entry.name),
);
@@ -417,29 +416,31 @@ export function useCompletion(
respectGitIgnore?: boolean;
respectGeminiIgnore?: boolean;
},
searchDir: string,
maxResults = 50,
): Promise<Suggestion[]> => {
const globPattern = `**/${searchPrefix}*`;
const files = await glob(globPattern, {
cwd,
cwd: searchDir,
dot: searchPrefix.startsWith('.'),
nocase: true,
});
const suggestions: Suggestion[] = files
.map((file: string) => ({
label: file,
value: escapePath(file),
}))
.filter((s) => {
.filter((file) => {
if (fileDiscoveryService) {
return !fileDiscoveryService.shouldIgnoreFile(
s.label,
filterOptions,
); // relative path
return !fileDiscoveryService.shouldIgnoreFile(file, filterOptions);
}
return true;
})
.map((file: string) => {
const absolutePath = path.resolve(searchDir, file);
const label = path.relative(cwd, absolutePath);
return {
label,
value: escapePath(label),
};
})
.slice(0, maxResults);
return suggestions;
@@ -456,63 +457,78 @@ export function useCompletion(
config?.getFileFilteringOptions() ?? DEFAULT_FILE_FILTERING_OPTIONS;
try {
// If there's no slash, or it's the root, do a recursive search from cwd
if (
partialPath.indexOf('/') === -1 &&
prefix &&
enableRecursiveSearch
) {
if (fileDiscoveryService) {
fetchedSuggestions = await findFilesWithGlob(
prefix,
fileDiscoveryService,
filterOptions,
);
// If there's no slash, or it's the root, do a recursive search from workspace directories
for (const dir of dirs) {
let fetchedSuggestionsPerDir: Suggestion[] = [];
if (
partialPath.indexOf('/') === -1 &&
prefix &&
enableRecursiveSearch
) {
if (fileDiscoveryService) {
fetchedSuggestionsPerDir = await findFilesWithGlob(
prefix,
fileDiscoveryService,
filterOptions,
dir,
);
} else {
fetchedSuggestionsPerDir = await findFilesRecursively(
dir,
prefix,
null,
filterOptions,
);
}
} else {
fetchedSuggestions = await findFilesRecursively(
cwd,
prefix,
null,
filterOptions,
);
}
} else {
// Original behavior: list files in the specific directory
const lowerPrefix = prefix.toLowerCase();
const entries = await fs.readdir(baseDirAbsolute, {
withFileTypes: true,
});
// Original behavior: list files in the specific directory
const lowerPrefix = prefix.toLowerCase();
const baseDirAbsolute = path.resolve(dir, baseDirRelative);
const entries = await fs.readdir(baseDirAbsolute, {
withFileTypes: true,
});
// Filter entries using git-aware filtering
const filteredEntries = [];
for (const entry of entries) {
// Conditionally ignore dotfiles
if (!prefix.startsWith('.') && entry.name.startsWith('.')) {
continue;
}
if (!entry.name.toLowerCase().startsWith(lowerPrefix)) continue;
// Filter entries using git-aware filtering
const filteredEntries = [];
for (const entry of entries) {
// Conditionally ignore dotfiles
if (!prefix.startsWith('.') && entry.name.startsWith('.')) {
continue;
}
if (!entry.name.toLowerCase().startsWith(lowerPrefix)) continue;
const relativePath = path.relative(
cwd,
path.join(baseDirAbsolute, entry.name),
);
if (
fileDiscoveryService &&
fileDiscoveryService.shouldIgnoreFile(relativePath, filterOptions)
) {
continue;
const relativePath = path.relative(
dir,
path.join(baseDirAbsolute, entry.name),
);
if (
fileDiscoveryService &&
fileDiscoveryService.shouldIgnoreFile(
relativePath,
filterOptions,
)
) {
continue;
}
filteredEntries.push(entry);
}
filteredEntries.push(entry);
fetchedSuggestionsPerDir = filteredEntries.map((entry) => {
const absolutePath = path.resolve(baseDirAbsolute, entry.name);
const label =
cwd === dir ? entry.name : path.relative(cwd, absolutePath);
const suggestionLabel = entry.isDirectory() ? label + '/' : label;
return {
label: suggestionLabel,
value: escapePath(suggestionLabel),
};
});
}
fetchedSuggestions = filteredEntries.map((entry) => {
const label = entry.isDirectory() ? entry.name + '/' : entry.name;
return {
label,
value: escapePath(label), // Value for completion should be just the name part
};
});
fetchedSuggestions = [
...fetchedSuggestions,
...fetchedSuggestionsPerDir,
];
}
// Like glob, we always return forwardslashes, even in windows.
@@ -585,6 +601,7 @@ export function useCompletion(
};
}, [
buffer.text,
dirs,
cwd,
isActive,
resetCompletionState,