fix(cli): improve shell completion for dotfiles, spaces, and quotes

This commit is contained in:
MD. MOHIBUR RAHMAN
2026-02-24 03:41:33 +06:00
parent 555b95eb09
commit 926afab788
2 changed files with 85 additions and 18 deletions
@@ -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',
+26 -18
View File
@@ -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({