mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-20 00:32:31 -07:00
fix(cli): improve shell completion for dotfiles, spaces, and quotes
This commit is contained in:
@@ -245,6 +245,65 @@ describe('useShellCompletion utilities', () => {
|
||||
expect(labels).toContain('.hidden');
|
||||
});
|
||||
|
||||
it('should show dotfiles in the current directory when query is exactly "."', async () => {
|
||||
const structure: FileSystemStructure = {
|
||||
'.hidden': '',
|
||||
'.bashrc': '',
|
||||
visible: '',
|
||||
};
|
||||
tmpDir = await createTmpDir(structure);
|
||||
|
||||
const results = await resolvePathCompletions('.', tmpDir);
|
||||
const labels = results.map((s) => s.label);
|
||||
expect(labels).toContain('.hidden');
|
||||
expect(labels).toContain('.bashrc');
|
||||
expect(labels).not.toContain('visible');
|
||||
});
|
||||
|
||||
it('should handle dotfile completions within a subdirectory', async () => {
|
||||
const structure: FileSystemStructure = {
|
||||
subdir: {
|
||||
'.secret': '',
|
||||
'public.txt': '',
|
||||
},
|
||||
};
|
||||
tmpDir = await createTmpDir(structure);
|
||||
|
||||
const results = await resolvePathCompletions('subdir/.', tmpDir);
|
||||
const labels = results.map((s) => s.label);
|
||||
expect(labels).toContain('.secret');
|
||||
expect(labels).not.toContain('public.txt');
|
||||
});
|
||||
|
||||
it('should strip leading quotes to resolve inner directory contents', async () => {
|
||||
const structure: FileSystemStructure = {
|
||||
src: {
|
||||
'index.ts': '',
|
||||
},
|
||||
};
|
||||
tmpDir = await createTmpDir(structure);
|
||||
|
||||
const results = await resolvePathCompletions('"src/', tmpDir);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].label).toBe('index.ts');
|
||||
|
||||
const resultsSingleQuote = await resolvePathCompletions("'src/", tmpDir);
|
||||
expect(resultsSingleQuote).toHaveLength(1);
|
||||
expect(resultsSingleQuote[0].label).toBe('index.ts');
|
||||
});
|
||||
|
||||
it('should properly escape resolutions with spaces inside stripped quote queries', async () => {
|
||||
const structure: FileSystemStructure = {
|
||||
'Folder With Spaces': {},
|
||||
};
|
||||
tmpDir = await createTmpDir(structure);
|
||||
|
||||
const results = await resolvePathCompletions('"Fo', tmpDir);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].label).toBe('Folder With Spaces/');
|
||||
expect(results[0].value).toBe(escapeShellPath('Folder With Spaces/'));
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent directory', async () => {
|
||||
const results = await resolvePathCompletions(
|
||||
'/nonexistent/path/foo',
|
||||
|
||||
@@ -252,22 +252,30 @@ export async function resolvePathCompletions(
|
||||
): Promise<Suggestion[]> {
|
||||
if (partial == null) return [];
|
||||
|
||||
const [expandedPartial, didExpandTilde] = expandTilde(partial);
|
||||
// Input Sanitization
|
||||
let strippedPartial = partial;
|
||||
if (strippedPartial.startsWith('"') || strippedPartial.startsWith("'")) {
|
||||
strippedPartial = strippedPartial.slice(1);
|
||||
}
|
||||
if (strippedPartial.endsWith('"') || strippedPartial.endsWith("'")) {
|
||||
strippedPartial = strippedPartial.slice(0, -1);
|
||||
}
|
||||
|
||||
// Determine the directory to list and the prefix to match
|
||||
const resolvedPath = path.isAbsolute(expandedPartial)
|
||||
? expandedPartial
|
||||
: path.resolve(cwd, expandedPartial);
|
||||
// Normalize separators \ to /
|
||||
const normalizedPartial = strippedPartial.replace(/\\/g, '/');
|
||||
|
||||
// If the partial ends with a separator, list that directory directly.
|
||||
// Otherwise, list the parent and filter by the basename prefix.
|
||||
const [expandedPartial, didExpandTilde] = expandTilde(normalizedPartial);
|
||||
|
||||
// Directory Detection
|
||||
const endsWithSep =
|
||||
partial.endsWith('/') || partial.endsWith(path.sep) || partial === '';
|
||||
const dirToRead = endsWithSep ? resolvedPath : path.dirname(resolvedPath);
|
||||
const prefix = endsWithSep ? '' : path.basename(resolvedPath);
|
||||
normalizedPartial.endsWith('/') || normalizedPartial === '';
|
||||
const dirToRead = endsWithSep
|
||||
? path.resolve(cwd, expandedPartial)
|
||||
: path.resolve(cwd, path.dirname(expandedPartial));
|
||||
|
||||
const prefix = endsWithSep ? '' : path.basename(expandedPartial);
|
||||
const prefixLower = prefix.toLowerCase();
|
||||
|
||||
// Determine whether to show dotfiles
|
||||
const showDotfiles = prefix.startsWith('.');
|
||||
|
||||
let entries: Array<import('node:fs').Dirent>;
|
||||
@@ -294,30 +302,30 @@ export async function resolvePathCompletions(
|
||||
if (!name.toLowerCase().startsWith(prefixLower)) continue;
|
||||
|
||||
const isDir = entry.isDirectory();
|
||||
const displayName = isDir ? name + path.sep : name;
|
||||
const displayName = isDir ? name + '/' : name;
|
||||
|
||||
// Build the completion value relative to what the user typed
|
||||
let completionValue: string;
|
||||
if (endsWithSep) {
|
||||
completionValue = partial + displayName;
|
||||
completionValue = normalizedPartial + displayName;
|
||||
} else {
|
||||
// Replace the basename portion
|
||||
const parentPart = partial.slice(
|
||||
const parentPart = normalizedPartial.slice(
|
||||
0,
|
||||
partial.length - path.basename(partial).length,
|
||||
normalizedPartial.length - path.basename(normalizedPartial).length,
|
||||
);
|
||||
completionValue = parentPart + displayName;
|
||||
}
|
||||
|
||||
// Restore tilde if we expanded it
|
||||
if (didExpandTilde) {
|
||||
const homeDir = os.homedir();
|
||||
const homeDir = os.homedir().replace(/\\/g, '/');
|
||||
if (completionValue.startsWith(homeDir)) {
|
||||
completionValue = '~' + completionValue.slice(homeDir.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Escape special characters in the completion value
|
||||
// Output formatting: Escape special characters in the completion value
|
||||
// Since normalizedPartial stripped quotes, we escape the value directly.
|
||||
const escapedValue = escapeShellPath(completionValue);
|
||||
|
||||
suggestions.push({
|
||||
|
||||
Reference in New Issue
Block a user