Download src/utils/suggestions/directoryCompletion.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 7.1 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/suggestions/directoryCompletion.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/suggestions/directoryCompletion.ts
-
curl -L -o directoryCompletion.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/suggestions/directoryCompletion.ts
7.1 kB
| import { LRUCache } from 'lru-cache' | |
| import { basename, dirname, join, sep } from 'path' | |
| import type { SuggestionItem } from 'src/components/PromptInput/PromptInputFooterSuggestions.js' | |
| import { getCwd } from 'src/utils/cwd.js' | |
| import { getFsImplementation } from 'src/utils/fsOperations.js' | |
| import { logError } from 'src/utils/log.js' | |
| import { expandPath } from 'src/utils/path.js' | |
| // Types | |
| export type DirectoryEntry = { | |
| name: string | |
| path: string | |
| type: 'directory' | |
| } | |
| export type PathEntry = { | |
| name: string | |
| path: string | |
| type: 'directory' | 'file' | |
| } | |
| export type CompletionOptions = { | |
| basePath?: string | |
| maxResults?: number | |
| } | |
| export type PathCompletionOptions = CompletionOptions & { | |
| includeFiles?: boolean | |
| includeHidden?: boolean | |
| } | |
| type ParsedPath = { | |
| directory: string | |
| prefix: string | |
| } | |
| // Cache configuration | |
| const CACHE_SIZE = 500 | |
| const CACHE_TTL = 5 * 60 * 1000 // 5 minutes | |
| // Initialize LRU cache for directory scans | |
| const directoryCache = new LRUCache<string, DirectoryEntry[]>({ | |
| max: CACHE_SIZE, | |
| ttl: CACHE_TTL, | |
| }) | |
| // Initialize LRU cache for path scans (files and directories) | |
| const pathCache = new LRUCache<string, PathEntry[]>({ | |
| max: CACHE_SIZE, | |
| ttl: CACHE_TTL, | |
| }) | |
| /** | |
| * Parses a partial path into directory and prefix components | |
| */ | |
| export function parsePartialPath( | |
| partialPath: string, | |
| basePath?: string, | |
| ): ParsedPath { | |
| // Handle empty input | |
| if (!partialPath) { | |
| const directory = basePath || getCwd() | |
| return { directory, prefix: '' } | |
| } | |
| const resolved = expandPath(partialPath, basePath) | |
| // If path ends with separator, treat as directory with no prefix | |
| // Handle both forward slash and platform-specific separator | |
| if (partialPath.endsWith('/') || partialPath.endsWith(sep)) { | |
| return { directory: resolved, prefix: '' } | |
| } | |
| // Split into directory and prefix | |
| const directory = dirname(resolved) | |
| const prefix = basename(partialPath) | |
| return { directory, prefix } | |
| } | |
| /** | |
| * Scans a directory and returns subdirectories | |
| * Uses LRU cache to avoid repeated filesystem calls | |
| */ | |
| export async function scanDirectory( | |
| dirPath: string, | |
| ): Promise<DirectoryEntry[]> { | |
| // Check cache first | |
| const cached = directoryCache.get(dirPath) | |
| if (cached) { | |
| return cached | |
| } | |
| try { | |
| // Read directory contents | |
| const fs = getFsImplementation() | |
| const entries = await fs.readdir(dirPath) | |
| // Filter for directories only, exclude hidden directories | |
| const directories = entries | |
| .filter(entry => entry.isDirectory() && !entry.name.startsWith('.')) | |
| .map(entry => ({ | |
| name: entry.name, | |
| path: join(dirPath, entry.name), | |
| type: 'directory' as const, | |
| })) | |
| .slice(0, 100) // Limit results for MVP | |
| // Cache the results | |
| directoryCache.set(dirPath, directories) | |
| return directories | |
| } catch (error) { | |
| logError(error) | |
| return [] | |
| } | |
| } | |
| /** | |
| * Main function to get directory completion suggestions | |
| */ | |
| export async function getDirectoryCompletions( | |
| partialPath: string, | |
| options: CompletionOptions = {}, | |
| ): Promise<SuggestionItem[]> { | |
| const { basePath = getCwd(), maxResults = 10 } = options | |
| const { directory, prefix } = parsePartialPath(partialPath, basePath) | |
| const entries = await scanDirectory(directory) | |
| const prefixLower = prefix.toLowerCase() | |
| const matches = entries | |
| .filter(entry => entry.name.toLowerCase().startsWith(prefixLower)) | |
| .slice(0, maxResults) | |
| return matches.map(entry => ({ | |
| id: entry.path, | |
| displayText: entry.name + '/', | |
| description: 'directory', | |
| metadata: { type: 'directory' as const }, | |
| })) | |
| } | |
| /** | |
| * Clears the directory cache | |
| */ | |
| export function clearDirectoryCache(): void { | |
| directoryCache.clear() | |
| } | |
| /** | |
| * Checks if a string looks like a path (starts with path-like prefixes) | |
| */ | |
| export function isPathLikeToken(token: string): boolean { | |
| return ( | |
| token.startsWith('~/') || | |
| token.startsWith('/') || | |
| token.startsWith('./') || | |
| token.startsWith('../') || | |
| token === '~' || | |
| token === '.' || | |
| token === '..' | |
| ) | |
| } | |
| /** | |
| * Scans a directory and returns both files and subdirectories | |
| * Uses LRU cache to avoid repeated filesystem calls | |
| */ | |
| export async function scanDirectoryForPaths( | |
| dirPath: string, | |
| includeHidden = false, | |
| ): Promise<PathEntry[]> { | |
| const cacheKey = `${dirPath}:${includeHidden}` | |
| const cached = pathCache.get(cacheKey) | |
| if (cached) { | |
| return cached | |
| } | |
| try { | |
| const fs = getFsImplementation() | |
| const entries = await fs.readdir(dirPath) | |
| const paths = entries | |
| .filter(entry => includeHidden || !entry.name.startsWith('.')) | |
| .map(entry => ({ | |
| name: entry.name, | |
| path: join(dirPath, entry.name), | |
| type: entry.isDirectory() ? ('directory' as const) : ('file' as const), | |
| })) | |
| .sort((a, b) => { | |
| // Sort directories first, then alphabetically | |
| if (a.type === 'directory' && b.type !== 'directory') return -1 | |
| if (a.type !== 'directory' && b.type === 'directory') return 1 | |
| return a.name.localeCompare(b.name) | |
| }) | |
| .slice(0, 100) | |
| pathCache.set(cacheKey, paths) | |
| return paths | |
| } catch (error) { | |
| logError(error) | |
| return [] | |
| } | |
| } | |
| /** | |
| * Get path completion suggestions for files and directories | |
| */ | |
| export async function getPathCompletions( | |
| partialPath: string, | |
| options: PathCompletionOptions = {}, | |
| ): Promise<SuggestionItem[]> { | |
| const { | |
| basePath = getCwd(), | |
| maxResults = 10, | |
| includeFiles = true, | |
| includeHidden = false, | |
| } = options | |
| const { directory, prefix } = parsePartialPath(partialPath, basePath) | |
| const entries = await scanDirectoryForPaths(directory, includeHidden) | |
| const prefixLower = prefix.toLowerCase() | |
| const matches = entries | |
| .filter(entry => { | |
| if (!includeFiles && entry.type === 'file') return false | |
| return entry.name.toLowerCase().startsWith(prefixLower) | |
| }) | |
| .slice(0, maxResults) | |
| // Construct relative path based on original partialPath | |
| // e.g., if partialPath is "src/c", directory portion is "src/" | |
| // Strip leading "./" since it's just used for cwd search | |
| // Handle both forward slash and platform separator for Windows compatibility | |
| const hasSeparator = partialPath.includes('/') || partialPath.includes(sep) | |
| let dirPortion = '' | |
| if (hasSeparator) { | |
| // Find the last separator (either / or platform-specific) | |
| const lastSlash = partialPath.lastIndexOf('/') | |
| const lastSep = partialPath.lastIndexOf(sep) | |
| const lastSeparatorPos = Math.max(lastSlash, lastSep) | |
| dirPortion = partialPath.substring(0, lastSeparatorPos + 1) | |
| } | |
| if (dirPortion.startsWith('./') || dirPortion.startsWith('.' + sep)) { | |
| dirPortion = dirPortion.slice(2) | |
| } | |
| return matches.map(entry => { | |
| const fullPath = dirPortion + entry.name | |
| return { | |
| id: fullPath, | |
| displayText: entry.type === 'directory' ? fullPath + '/' : fullPath, | |
| metadata: { type: entry.type }, | |
| } | |
| }) | |
| } | |
| /** | |
| * Clears both directory and path caches | |
| */ | |
| export function clearPathCache(): void { | |
| directoryCache.clear() | |
| pathCache.clear() | |
| } | |