mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-08 04:10:35 -07:00
167 lines
4.2 KiB
TypeScript
167 lines
4.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import * as path from 'node:path';
|
|
import * as fs from 'node:fs';
|
|
import { opendir } from 'node:fs/promises';
|
|
import { homedir, type WorkspaceContext } from '@google/gemini-cli-core';
|
|
|
|
const MAX_SUGGESTIONS = 50;
|
|
const MATCH_BUFFER_MULTIPLIER = 3;
|
|
|
|
export function expandHomeDir(p: string): string {
|
|
if (!p) {
|
|
return '';
|
|
}
|
|
let expandedPath = p;
|
|
if (p.toLowerCase().startsWith('%userprofile%')) {
|
|
expandedPath = homedir() + p.substring('%userprofile%'.length);
|
|
} else if (p === '~' || p.startsWith('~/')) {
|
|
expandedPath = homedir() + p.substring(1);
|
|
}
|
|
return path.normalize(expandedPath);
|
|
}
|
|
|
|
interface ParsedPath {
|
|
searchDir: string;
|
|
filter: string;
|
|
isHomeExpansion: boolean;
|
|
resultPrefix: string;
|
|
}
|
|
|
|
function parsePartialPath(partialPath: string): ParsedPath {
|
|
const isHomeExpansion = partialPath.startsWith('~');
|
|
const expandedPath = expandHomeDir(partialPath || '.');
|
|
|
|
let searchDir: string;
|
|
let filter: string;
|
|
|
|
if (
|
|
partialPath === '' ||
|
|
partialPath.endsWith('/') ||
|
|
partialPath.endsWith(path.sep)
|
|
) {
|
|
searchDir = expandedPath;
|
|
filter = '';
|
|
} else {
|
|
searchDir = path.dirname(expandedPath);
|
|
filter = path.basename(expandedPath);
|
|
|
|
// Special case for ~ because path.dirname('~') can be '.'
|
|
if (
|
|
isHomeExpansion &&
|
|
!partialPath.includes('/') &&
|
|
!partialPath.includes(path.sep)
|
|
) {
|
|
searchDir = homedir();
|
|
filter = partialPath.substring(1);
|
|
}
|
|
}
|
|
|
|
// Calculate result prefix
|
|
let resultPrefix = '';
|
|
if (
|
|
partialPath === '' ||
|
|
partialPath.endsWith('/') ||
|
|
partialPath.endsWith(path.sep)
|
|
) {
|
|
resultPrefix = partialPath;
|
|
} else {
|
|
const lastSlashIndex = Math.max(
|
|
partialPath.lastIndexOf('/'),
|
|
partialPath.lastIndexOf(path.sep),
|
|
);
|
|
if (lastSlashIndex !== -1) {
|
|
resultPrefix = partialPath.substring(0, lastSlashIndex + 1);
|
|
} else if (isHomeExpansion) {
|
|
resultPrefix = `~${path.sep}`;
|
|
}
|
|
}
|
|
|
|
return { searchDir, filter, isHomeExpansion, resultPrefix };
|
|
}
|
|
|
|
/**
|
|
* Gets directory suggestions based on a partial path.
|
|
* Uses async iteration with fs.opendir for efficient handling of large directories.
|
|
*
|
|
* @param partialPath The partial path typed by the user.
|
|
* @returns A promise resolving to an array of directory path suggestions.
|
|
*/
|
|
export async function getDirectorySuggestions(
|
|
partialPath: string,
|
|
): Promise<string[]> {
|
|
try {
|
|
const { searchDir, filter, resultPrefix } = parsePartialPath(partialPath);
|
|
|
|
if (!fs.existsSync(searchDir) || !fs.statSync(searchDir).isDirectory()) {
|
|
return [];
|
|
}
|
|
|
|
const matches: string[] = [];
|
|
const filterLower = filter.toLowerCase();
|
|
const showHidden = filter.startsWith('.');
|
|
const dir = await opendir(searchDir);
|
|
|
|
try {
|
|
for await (const entry of dir) {
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
if (entry.name.startsWith('.') && !showHidden) {
|
|
continue;
|
|
}
|
|
|
|
if (entry.name.toLowerCase().startsWith(filterLower)) {
|
|
matches.push(entry.name);
|
|
|
|
// Early termination with buffer for sorting
|
|
if (matches.length >= MAX_SUGGESTIONS * MATCH_BUFFER_MULTIPLIER) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
await dir.close().catch(() => {});
|
|
}
|
|
|
|
// Use the separator style from user's input for consistency
|
|
const userSep = resultPrefix.includes('/') ? '/' : path.sep;
|
|
|
|
return matches
|
|
.sort()
|
|
.slice(0, MAX_SUGGESTIONS)
|
|
.map((name) => resultPrefix + name + userSep);
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export interface BatchAddResult {
|
|
added: string[];
|
|
errors: string[];
|
|
}
|
|
|
|
/**
|
|
* Helper to batch add directories to the workspace context.
|
|
* Handles expansion and error formatting.
|
|
*/
|
|
export function batchAddDirectories(
|
|
workspaceContext: WorkspaceContext,
|
|
paths: string[],
|
|
): BatchAddResult {
|
|
const result = workspaceContext.addDirectories(
|
|
paths.map((p) => expandHomeDir(p.trim())),
|
|
);
|
|
|
|
const errors: string[] = [];
|
|
for (const failure of result.failed) {
|
|
errors.push(`Error adding '${failure.path}': ${failure.error.message}`);
|
|
}
|
|
|
|
return { added: result.added, errors };
|
|
}
|