| |
| |
| |
| |
| |
|
|
| 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); |
|
|
| |
| if ( |
| isHomeExpansion && |
| !partialPath.includes('/') && |
| !partialPath.includes(path.sep) |
| ) { |
| searchDir = homedir(); |
| filter = partialPath.substring(1); |
| } |
| } |
|
|
| |
| 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 }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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); |
|
|
| |
| if (matches.length >= MAX_SUGGESTIONS * MATCH_BUFFER_MULTIPLIER) { |
| break; |
| } |
| } |
| } |
| } finally { |
| await dir.close().catch(() => {}); |
| } |
|
|
| |
| 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[]; |
| } |
|
|
| |
| |
| |
| |
| 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 }; |
| } |
|
|