mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
feat: Multi-Directory Workspace Support (part1: add --include-directories option) (#4605)
Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
@@ -61,6 +61,7 @@ export interface CliArgs {
|
||||
listExtensions: boolean | undefined;
|
||||
ideMode: boolean | undefined;
|
||||
proxy: string | undefined;
|
||||
includeDirectories: string[] | undefined;
|
||||
}
|
||||
|
||||
export async function parseArguments(): Promise<CliArgs> {
|
||||
@@ -199,6 +200,15 @@ export async function parseArguments(): Promise<CliArgs> {
|
||||
description:
|
||||
'Proxy for gemini client, like schema://user:password@host:port',
|
||||
})
|
||||
.option('include-directories', {
|
||||
type: 'array',
|
||||
string: true,
|
||||
description:
|
||||
'Additional directories to include in the workspace (comma-separated or multiple --include-directories)',
|
||||
coerce: (dirs: string[]) =>
|
||||
// Handle comma-separated values
|
||||
dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())),
|
||||
})
|
||||
.version(await getCliVersion()) // This will enable the --version flag based on package.json
|
||||
.alias('v', 'version')
|
||||
.help()
|
||||
@@ -366,6 +376,7 @@ export async function loadCliConfig(
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
sandbox: sandboxConfig,
|
||||
targetDir: process.cwd(),
|
||||
includeDirectories: argv.includeDirectories,
|
||||
debugMode,
|
||||
question: argv.promptInteractive || argv.prompt || '',
|
||||
fullContext: argv.allFiles || argv.all_files || false,
|
||||
|
||||
@@ -199,7 +199,7 @@ export async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
await start_sandbox(sandboxConfig, memoryArgs);
|
||||
await start_sandbox(sandboxConfig, memoryArgs, config);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Not in a sandbox and not entering one, so relaunch with additional
|
||||
|
||||
@@ -152,6 +152,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
getSessionId: vi.fn(() => 'test-session-id'),
|
||||
getUserTier: vi.fn().mockResolvedValue(undefined),
|
||||
getIdeMode: vi.fn(() => false),
|
||||
getWorkspaceContext: vi.fn(() => ({
|
||||
getDirectories: vi.fn(() => []),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -292,6 +295,13 @@ describe('App UI', () => {
|
||||
|
||||
// Ensure a theme is set so the theme dialog does not appear.
|
||||
mockSettings = createMockSettings({ workspace: { theme: 'Default' } });
|
||||
|
||||
// Ensure getWorkspaceContext is available if not added by the constructor
|
||||
if (!mockConfig.getWorkspaceContext) {
|
||||
mockConfig.getWorkspaceContext = vi.fn(() => ({
|
||||
getDirectories: vi.fn(() => ['/test/dir']),
|
||||
}));
|
||||
}
|
||||
vi.mocked(ideContext.getIdeContext).mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ describe('aboutCommand', () => {
|
||||
});
|
||||
|
||||
it('should call addItem with all version info', async () => {
|
||||
process.env.SANDBOX = '';
|
||||
if (!aboutCommand.action) {
|
||||
throw new Error('The about command must have an action.');
|
||||
}
|
||||
|
||||
@@ -172,6 +172,9 @@ describe('InputPrompt', () => {
|
||||
getProjectRoot: () => path.join('test', 'project'),
|
||||
getTargetDir: () => path.join('test', 'project', 'src'),
|
||||
getVimMode: () => false,
|
||||
getWorkspaceContext: () => ({
|
||||
getDirectories: () => ['/test/project/src'],
|
||||
}),
|
||||
} as unknown as Config,
|
||||
slashCommands: mockSlashCommands,
|
||||
commandContext: mockCommandContext,
|
||||
@@ -731,6 +734,7 @@ describe('InputPrompt', () => {
|
||||
// Verify useCompletion was called with correct signature
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -756,6 +760,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -781,6 +786,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -806,6 +812,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -831,6 +838,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -857,6 +865,7 @@ describe('InputPrompt', () => {
|
||||
// Verify useCompletion was called with the buffer
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -882,6 +891,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -908,6 +918,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -934,6 +945,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -960,6 +972,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -986,6 +999,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -1014,6 +1028,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -1040,6 +1055,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -1068,6 +1084,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
expect(mockedUseCompletion).toHaveBeenCalledWith(
|
||||
mockBuffer,
|
||||
['/test/project/src'],
|
||||
path.join('test', 'project', 'src'),
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
|
||||
@@ -60,8 +60,19 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}) => {
|
||||
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
|
||||
|
||||
const [dirs, setDirs] = useState<readonly string[]>(
|
||||
config.getWorkspaceContext().getDirectories(),
|
||||
);
|
||||
const dirsChanged = config.getWorkspaceContext().getDirectories();
|
||||
useEffect(() => {
|
||||
if (dirs.length !== dirsChanged.length) {
|
||||
setDirs(dirsChanged);
|
||||
}
|
||||
}, [dirs.length, dirsChanged]);
|
||||
|
||||
const completion = useCompletion(
|
||||
buffer,
|
||||
dirs,
|
||||
config.getTargetDir(),
|
||||
slashCommands,
|
||||
commandContext,
|
||||
|
||||
@@ -57,6 +57,10 @@ describe('handleAtCommand', () => {
|
||||
respectGeminiIgnore: true,
|
||||
}),
|
||||
getEnableRecursiveFileSearch: vi.fn(() => true),
|
||||
getWorkspaceContext: () => ({
|
||||
isPathWithinWorkspace: () => true,
|
||||
getDirectories: () => [testRootDir],
|
||||
}),
|
||||
} as unknown as Config;
|
||||
|
||||
const registry = new ToolRegistry(mockConfig);
|
||||
|
||||
@@ -188,6 +188,14 @@ export async function handleAtCommand({
|
||||
|
||||
// Check if path should be ignored based on filtering options
|
||||
|
||||
const workspaceContext = config.getWorkspaceContext();
|
||||
if (!workspaceContext.isPathWithinWorkspace(pathName)) {
|
||||
onDebugMessage(
|
||||
`Path ${pathName} is not in the workspace and will be skipped.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const gitIgnored =
|
||||
respectFileIgnore.respectGitIgnore &&
|
||||
fileDiscovery.shouldIgnoreFile(pathName, {
|
||||
@@ -215,90 +223,88 @@ export async function handleAtCommand({
|
||||
continue;
|
||||
}
|
||||
|
||||
let currentPathSpec = pathName;
|
||||
let resolvedSuccessfully = false;
|
||||
|
||||
try {
|
||||
const absolutePath = path.resolve(config.getTargetDir(), pathName);
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
currentPathSpec =
|
||||
pathName + (pathName.endsWith(path.sep) ? `**` : `/**`);
|
||||
onDebugMessage(
|
||||
`Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`,
|
||||
);
|
||||
} else {
|
||||
onDebugMessage(`Path ${pathName} resolved to file: ${absolutePath}`);
|
||||
}
|
||||
resolvedSuccessfully = true;
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
if (config.getEnableRecursiveFileSearch() && globTool) {
|
||||
for (const dir of config.getWorkspaceContext().getDirectories()) {
|
||||
let currentPathSpec = pathName;
|
||||
let resolvedSuccessfully = false;
|
||||
try {
|
||||
const absolutePath = path.resolve(dir, pathName);
|
||||
const stats = await fs.stat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
currentPathSpec =
|
||||
pathName + (pathName.endsWith(path.sep) ? `**` : `/**`);
|
||||
onDebugMessage(
|
||||
`Path ${pathName} not found directly, attempting glob search.`,
|
||||
`Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`,
|
||||
);
|
||||
try {
|
||||
const globResult = await globTool.execute(
|
||||
{
|
||||
pattern: `**/*${pathName}*`,
|
||||
path: config.getTargetDir(),
|
||||
},
|
||||
signal,
|
||||
} else {
|
||||
onDebugMessage(`Path ${pathName} resolved to file: ${absolutePath}`);
|
||||
}
|
||||
resolvedSuccessfully = true;
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
if (config.getEnableRecursiveFileSearch() && globTool) {
|
||||
onDebugMessage(
|
||||
`Path ${pathName} not found directly, attempting glob search.`,
|
||||
);
|
||||
if (
|
||||
globResult.llmContent &&
|
||||
typeof globResult.llmContent === 'string' &&
|
||||
!globResult.llmContent.startsWith('No files found') &&
|
||||
!globResult.llmContent.startsWith('Error:')
|
||||
) {
|
||||
const lines = globResult.llmContent.split('\n');
|
||||
if (lines.length > 1 && lines[1]) {
|
||||
const firstMatchAbsolute = lines[1].trim();
|
||||
currentPathSpec = path.relative(
|
||||
config.getTargetDir(),
|
||||
firstMatchAbsolute,
|
||||
);
|
||||
onDebugMessage(
|
||||
`Glob search for ${pathName} found ${firstMatchAbsolute}, using relative path: ${currentPathSpec}`,
|
||||
);
|
||||
resolvedSuccessfully = true;
|
||||
try {
|
||||
const globResult = await globTool.execute(
|
||||
{
|
||||
pattern: `**/*${pathName}*`,
|
||||
path: dir,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
if (
|
||||
globResult.llmContent &&
|
||||
typeof globResult.llmContent === 'string' &&
|
||||
!globResult.llmContent.startsWith('No files found') &&
|
||||
!globResult.llmContent.startsWith('Error:')
|
||||
) {
|
||||
const lines = globResult.llmContent.split('\n');
|
||||
if (lines.length > 1 && lines[1]) {
|
||||
const firstMatchAbsolute = lines[1].trim();
|
||||
currentPathSpec = path.relative(dir, firstMatchAbsolute);
|
||||
onDebugMessage(
|
||||
`Glob search for ${pathName} found ${firstMatchAbsolute}, using relative path: ${currentPathSpec}`,
|
||||
);
|
||||
resolvedSuccessfully = true;
|
||||
} else {
|
||||
onDebugMessage(
|
||||
`Glob search for '**/*${pathName}*' did not return a usable path. Path ${pathName} will be skipped.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onDebugMessage(
|
||||
`Glob search for '**/*${pathName}*' did not return a usable path. Path ${pathName} will be skipped.`,
|
||||
`Glob search for '**/*${pathName}*' found no files or an error. Path ${pathName} will be skipped.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
} catch (globError) {
|
||||
console.error(
|
||||
`Error during glob search for ${pathName}: ${getErrorMessage(globError)}`,
|
||||
);
|
||||
onDebugMessage(
|
||||
`Glob search for '**/*${pathName}*' found no files or an error. Path ${pathName} will be skipped.`,
|
||||
`Error during glob search for ${pathName}. Path ${pathName} will be skipped.`,
|
||||
);
|
||||
}
|
||||
} catch (globError) {
|
||||
console.error(
|
||||
`Error during glob search for ${pathName}: ${getErrorMessage(globError)}`,
|
||||
);
|
||||
} else {
|
||||
onDebugMessage(
|
||||
`Error during glob search for ${pathName}. Path ${pathName} will be skipped.`,
|
||||
`Glob tool not found. Path ${pathName} will be skipped.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`Error stating path ${pathName}: ${getErrorMessage(error)}`,
|
||||
);
|
||||
onDebugMessage(
|
||||
`Glob tool not found. Path ${pathName} will be skipped.`,
|
||||
`Error stating path ${pathName}. Path ${pathName} will be skipped.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`Error stating path ${pathName}: ${getErrorMessage(error)}`,
|
||||
);
|
||||
onDebugMessage(
|
||||
`Error stating path ${pathName}. Path ${pathName} will be skipped.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedSuccessfully) {
|
||||
pathSpecsToRead.push(currentPathSpec);
|
||||
atPathToResolvedSpecMap.set(originalAtPath, currentPathSpec);
|
||||
contentLabelsForDisplay.push(pathName);
|
||||
if (resolvedSuccessfully) {
|
||||
pathSpecsToRead.push(currentPathSpec);
|
||||
atPathToResolvedSpecMap.set(originalAtPath, currentPathSpec);
|
||||
contentLabelsForDisplay.push(pathName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('useCompletion', () => {
|
||||
|
||||
// A minimal mock is sufficient for these tests.
|
||||
const mockCommandContext = {} as CommandContext;
|
||||
let testDirs: string[];
|
||||
|
||||
async function createEmptyDir(...pathSegments: string[]) {
|
||||
const fullPath = path.join(testRootDir, ...pathSegments);
|
||||
@@ -51,8 +52,12 @@ describe('useCompletion', () => {
|
||||
testRootDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'completion-unit-test-'),
|
||||
);
|
||||
testDirs = [testRootDir];
|
||||
mockConfig = {
|
||||
getTargetDir: () => testRootDir,
|
||||
getWorkspaceContext: () => ({
|
||||
getDirectories: () => testDirs,
|
||||
}),
|
||||
getProjectRoot: () => testRootDir,
|
||||
getFileFilteringOptions: vi.fn(() => ({
|
||||
respectGitIgnore: true,
|
||||
@@ -79,6 +84,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest(''),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -108,6 +114,7 @@ describe('useCompletion', () => {
|
||||
const textBuffer = useTextBufferForTest(text);
|
||||
return useCompletion(
|
||||
textBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -138,6 +145,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/help'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -170,6 +178,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest(''),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -191,6 +200,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest(''),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -215,6 +225,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/h'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -242,6 +253,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/h'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -270,6 +282,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -315,6 +328,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/command'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
largeMockCommands,
|
||||
mockCommandContext,
|
||||
@@ -372,6 +386,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -394,6 +409,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/mem'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -417,6 +433,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/usag'), // part of the word "usage"
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -443,6 +460,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/clear'), // No trailing space
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -474,6 +492,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest(query),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
mockSlashCommands,
|
||||
mockCommandContext,
|
||||
@@ -494,6 +513,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/clear '),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -514,6 +534,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/unknown-command'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -547,6 +568,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/memory'), // Note: no trailing space
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -584,6 +606,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/memory'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -619,6 +642,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/memory a'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -650,6 +674,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/memory dothisnow'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -692,6 +717,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/chat resume my-ch'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -735,6 +761,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/chat resume '),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -769,6 +796,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('/chat resume '),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -796,6 +824,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@s'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -829,6 +858,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@src/comp'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -854,6 +884,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@.'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -885,6 +916,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@d'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -910,6 +942,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -944,6 +977,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -974,6 +1008,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@d'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -1007,6 +1042,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -1039,6 +1075,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
useTextBufferForTest('@t'),
|
||||
testDirs,
|
||||
testRootDir,
|
||||
[],
|
||||
mockCommandContext,
|
||||
@@ -1085,6 +1122,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
mockBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -1128,6 +1166,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
mockBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -1173,6 +1212,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
mockBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
@@ -1221,6 +1261,7 @@ describe('useCompletion', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCompletion(
|
||||
mockBuffer,
|
||||
testDirs,
|
||||
testRootDir,
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||
;; Allow writes to included directories from --include-directories
|
||||
(subpath (param "INCLUDE_DIR_0"))
|
||||
(subpath (param "INCLUDE_DIR_1"))
|
||||
(subpath (param "INCLUDE_DIR_2"))
|
||||
(subpath (param "INCLUDE_DIR_3"))
|
||||
(subpath (param "INCLUDE_DIR_4"))
|
||||
(literal "/dev/stdout")
|
||||
(literal "/dev/stderr")
|
||||
(literal "/dev/null")
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||
;; Allow writes to included directories from --include-directories
|
||||
(subpath (param "INCLUDE_DIR_0"))
|
||||
(subpath (param "INCLUDE_DIR_1"))
|
||||
(subpath (param "INCLUDE_DIR_2"))
|
||||
(subpath (param "INCLUDE_DIR_3"))
|
||||
(subpath (param "INCLUDE_DIR_4"))
|
||||
(literal "/dev/stdout")
|
||||
(literal "/dev/stderr")
|
||||
(literal "/dev/null")
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||
;; Allow writes to included directories from --include-directories
|
||||
(subpath (param "INCLUDE_DIR_0"))
|
||||
(subpath (param "INCLUDE_DIR_1"))
|
||||
(subpath (param "INCLUDE_DIR_2"))
|
||||
(subpath (param "INCLUDE_DIR_3"))
|
||||
(subpath (param "INCLUDE_DIR_4"))
|
||||
(literal "/dev/stdout")
|
||||
(literal "/dev/stderr")
|
||||
(literal "/dev/null")
|
||||
|
||||
@@ -71,6 +71,12 @@
|
||||
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||
;; Allow writes to included directories from --include-directories
|
||||
(subpath (param "INCLUDE_DIR_0"))
|
||||
(subpath (param "INCLUDE_DIR_1"))
|
||||
(subpath (param "INCLUDE_DIR_2"))
|
||||
(subpath (param "INCLUDE_DIR_3"))
|
||||
(subpath (param "INCLUDE_DIR_4"))
|
||||
(literal "/dev/stdout")
|
||||
(literal "/dev/stderr")
|
||||
(literal "/dev/null")
|
||||
|
||||
@@ -71,6 +71,12 @@
|
||||
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||
;; Allow writes to included directories from --include-directories
|
||||
(subpath (param "INCLUDE_DIR_0"))
|
||||
(subpath (param "INCLUDE_DIR_1"))
|
||||
(subpath (param "INCLUDE_DIR_2"))
|
||||
(subpath (param "INCLUDE_DIR_3"))
|
||||
(subpath (param "INCLUDE_DIR_4"))
|
||||
(literal "/dev/stdout")
|
||||
(literal "/dev/stderr")
|
||||
(literal "/dev/null")
|
||||
|
||||
@@ -71,6 +71,12 @@
|
||||
(subpath (string-append (param "HOME_DIR") "/.npm"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.cache"))
|
||||
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
|
||||
;; Allow writes to included directories from --include-directories
|
||||
(subpath (param "INCLUDE_DIR_0"))
|
||||
(subpath (param "INCLUDE_DIR_1"))
|
||||
(subpath (param "INCLUDE_DIR_2"))
|
||||
(subpath (param "INCLUDE_DIR_3"))
|
||||
(subpath (param "INCLUDE_DIR_4"))
|
||||
(literal "/dev/stdout")
|
||||
(literal "/dev/stderr")
|
||||
(literal "/dev/null")
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
SETTINGS_DIRECTORY_NAME,
|
||||
} from '../config/settings.js';
|
||||
import { promisify } from 'util';
|
||||
import { SandboxConfig } from '@google/gemini-cli-core';
|
||||
import { Config, SandboxConfig } from '@google/gemini-cli-core';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -183,6 +183,7 @@ function entrypoint(workdir: string): string[] {
|
||||
export async function start_sandbox(
|
||||
config: SandboxConfig,
|
||||
nodeArgs: string[] = [],
|
||||
cliConfig?: Config,
|
||||
) {
|
||||
if (config.command === 'sandbox-exec') {
|
||||
// disallow BUILD_SANDBOX
|
||||
@@ -223,6 +224,38 @@ export async function start_sandbox(
|
||||
`HOME_DIR=${fs.realpathSync(os.homedir())}`,
|
||||
'-D',
|
||||
`CACHE_DIR=${fs.realpathSync(execSync(`getconf DARWIN_USER_CACHE_DIR`).toString().trim())}`,
|
||||
];
|
||||
|
||||
// Add included directories from the workspace context
|
||||
// Always add 5 INCLUDE_DIR parameters to ensure .sb files can reference them
|
||||
const MAX_INCLUDE_DIRS = 5;
|
||||
const targetDir = fs.realpathSync(cliConfig?.getTargetDir() || '');
|
||||
const includedDirs: string[] = [];
|
||||
|
||||
if (cliConfig) {
|
||||
const workspaceContext = cliConfig.getWorkspaceContext();
|
||||
const directories = workspaceContext.getDirectories();
|
||||
|
||||
// Filter out TARGET_DIR
|
||||
for (const dir of directories) {
|
||||
const realDir = fs.realpathSync(dir);
|
||||
if (realDir !== targetDir) {
|
||||
includedDirs.push(realDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAX_INCLUDE_DIRS; i++) {
|
||||
let dirPath = '/dev/null'; // Default to a safe path that won't cause issues
|
||||
|
||||
if (i < includedDirs.length) {
|
||||
dirPath = includedDirs[i];
|
||||
}
|
||||
|
||||
args.push('-D', `INCLUDE_DIR_${i}=${dirPath}`);
|
||||
}
|
||||
|
||||
args.push(
|
||||
'-f',
|
||||
profileFile,
|
||||
'sh',
|
||||
@@ -232,7 +265,7 @@ export async function start_sandbox(
|
||||
`NODE_OPTIONS="${nodeOptions}"`,
|
||||
...process.argv.map((arg) => quote([arg])),
|
||||
].join(' '),
|
||||
];
|
||||
);
|
||||
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
|
||||
const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND;
|
||||
let proxyProcess: ChildProcess | undefined = undefined;
|
||||
|
||||
Reference in New Issue
Block a user