Download src/utils/bash/shellCompletion.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 7.87 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/bash/shellCompletion.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/bash/shellCompletion.ts
-
curl -L -o shellCompletion.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/bash/shellCompletion.ts
7.87 kB
| import type { SuggestionItem } from 'src/components/PromptInput/PromptInputFooterSuggestions.js' | |
| import { | |
| type ParseEntry, | |
| quote, | |
| tryParseShellCommand, | |
| } from '../bash/shellQuote.js' | |
| import { logForDebugging } from '../debug.js' | |
| import { getShellType } from '../localInstaller.js' | |
| import * as Shell from '../Shell.js' | |
| // Constants | |
| const MAX_SHELL_COMPLETIONS = 15 | |
| const SHELL_COMPLETION_TIMEOUT_MS = 1000 | |
| const COMMAND_OPERATORS = ['|', '||', '&&', ';'] as const | |
| export type ShellCompletionType = 'command' | 'variable' | 'file' | |
| type InputContext = { | |
| prefix: string | |
| completionType: ShellCompletionType | |
| } | |
| /** | |
| * Check if a parsed token is a command operator (|, ||, &&, ;) | |
| */ | |
| function isCommandOperator(token: ParseEntry): boolean { | |
| return ( | |
| typeof token === 'object' && | |
| token !== null && | |
| 'op' in token && | |
| (COMMAND_OPERATORS as readonly string[]).includes(token.op as string) | |
| ) | |
| } | |
| /** | |
| * Determine completion type based solely on prefix characteristics | |
| */ | |
| function getCompletionTypeFromPrefix(prefix: string): ShellCompletionType { | |
| if (prefix.startsWith('$')) { | |
| return 'variable' | |
| } | |
| if ( | |
| prefix.includes('/') || | |
| prefix.startsWith('~') || | |
| prefix.startsWith('.') | |
| ) { | |
| return 'file' | |
| } | |
| return 'command' | |
| } | |
| /** | |
| * Find the last string token and its index in parsed tokens | |
| */ | |
| function findLastStringToken( | |
| tokens: ParseEntry[], | |
| ): { token: string; index: number } | null { | |
| const i = tokens.findLastIndex(t => typeof t === 'string') | |
| return i !== -1 ? { token: tokens[i] as string, index: i } : null | |
| } | |
| /** | |
| * Check if we're in a context that expects a new command | |
| * (at start of input or after a command operator) | |
| */ | |
| function isNewCommandContext( | |
| tokens: ParseEntry[], | |
| currentTokenIndex: number, | |
| ): boolean { | |
| if (currentTokenIndex === 0) { | |
| return true | |
| } | |
| const prevToken = tokens[currentTokenIndex - 1] | |
| return prevToken !== undefined && isCommandOperator(prevToken) | |
| } | |
| /** | |
| * Parse input to extract completion context | |
| */ | |
| function parseInputContext(input: string, cursorOffset: number): InputContext { | |
| const beforeCursor = input.slice(0, cursorOffset) | |
| // Check if it's a variable prefix, before expanding with shell-quote | |
| const varMatch = beforeCursor.match(/\$[a-zA-Z_][a-zA-Z0-9_]*$/) | |
| if (varMatch) { | |
| return { prefix: varMatch[0], completionType: 'variable' } | |
| } | |
| // Parse with shell-quote | |
| const parseResult = tryParseShellCommand(beforeCursor) | |
| if (!parseResult.success) { | |
| // Fallback to simple parsing | |
| const tokens = beforeCursor.split(/\s+/) | |
| const prefix = tokens[tokens.length - 1] || '' | |
| const isFirstToken = tokens.length === 1 && !beforeCursor.includes(' ') | |
| const completionType = isFirstToken | |
| ? 'command' | |
| : getCompletionTypeFromPrefix(prefix) | |
| return { prefix, completionType } | |
| } | |
| // Extract current token | |
| const lastToken = findLastStringToken(parseResult.tokens) | |
| if (!lastToken) { | |
| // No string token found - check if after operator | |
| const lastParsedToken = parseResult.tokens[parseResult.tokens.length - 1] | |
| const completionType = | |
| lastParsedToken && isCommandOperator(lastParsedToken) | |
| ? 'command' | |
| : 'command' // Default to command at start | |
| return { prefix: '', completionType } | |
| } | |
| // If there's a trailing space, the user is starting a new argument | |
| if (beforeCursor.endsWith(' ')) { | |
| // After first token (command) with space = file argument expected | |
| return { prefix: '', completionType: 'file' } | |
| } | |
| // Determine completion type from context | |
| const baseType = getCompletionTypeFromPrefix(lastToken.token) | |
| // If it's clearly a file or variable based on prefix, use that type | |
| if (baseType === 'variable' || baseType === 'file') { | |
| return { prefix: lastToken.token, completionType: baseType } | |
| } | |
| // For command-like tokens, check context: are we starting a new command? | |
| const completionType = isNewCommandContext( | |
| parseResult.tokens, | |
| lastToken.index, | |
| ) | |
| ? 'command' | |
| : 'file' // Not after operator = file argument | |
| return { prefix: lastToken.token, completionType } | |
| } | |
| /** | |
| * Generate bash completion command using compgen | |
| */ | |
| function getBashCompletionCommand( | |
| prefix: string, | |
| completionType: ShellCompletionType, | |
| ): string { | |
| if (completionType === 'variable') { | |
| // Variable completion - remove $ prefix | |
| const varName = prefix.slice(1) | |
| return `compgen -v ${quote([varName])} 2>/dev/null` | |
| } else if (completionType === 'file') { | |
| // File completion with trailing slash for directories and trailing space for files | |
| // Use 'while read' to prevent command injection from filenames containing newlines | |
| return `compgen -f ${quote([prefix])} 2>/dev/null | head -${MAX_SHELL_COMPLETIONS} | while IFS= read -r f; do [ -d "$f" ] && echo "$f/" || echo "$f "; done` | |
| } else { | |
| // Command completion | |
| return `compgen -c ${quote([prefix])} 2>/dev/null` | |
| } | |
| } | |
| /** | |
| * Generate zsh completion command using native zsh commands | |
| */ | |
| function getZshCompletionCommand( | |
| prefix: string, | |
| completionType: ShellCompletionType, | |
| ): string { | |
| if (completionType === 'variable') { | |
| // Variable completion - use zsh pattern matching for safe filtering | |
| const varName = prefix.slice(1) | |
| return `print -rl -- \${(k)parameters[(I)${quote([varName])}*]} 2>/dev/null` | |
| } else if (completionType === 'file') { | |
| // File completion with trailing slash for directories and trailing space for files | |
| // Note: zsh glob expansion is safe from command injection (unlike bash for-in loops) | |
| return `for f in ${quote([prefix])}*(N[1,${MAX_SHELL_COMPLETIONS}]); do [[ -d "$f" ]] && echo "$f/" || echo "$f "; done` | |
| } else { | |
| // Command completion - use zsh pattern matching for safe filtering | |
| return `print -rl -- \${(k)commands[(I)${quote([prefix])}*]} 2>/dev/null` | |
| } | |
| } | |
| /** | |
| * Get completions for the given shell type | |
| */ | |
| async function getCompletionsForShell( | |
| shellType: 'bash' | 'zsh', | |
| prefix: string, | |
| completionType: ShellCompletionType, | |
| abortSignal: AbortSignal, | |
| ): Promise<SuggestionItem[]> { | |
| let command: string | |
| if (shellType === 'bash') { | |
| command = getBashCompletionCommand(prefix, completionType) | |
| } else if (shellType === 'zsh') { | |
| command = getZshCompletionCommand(prefix, completionType) | |
| } else { | |
| // Unsupported shell type | |
| return [] | |
| } | |
| const shellCommand = await Shell.exec(command, abortSignal, 'bash', { | |
| timeout: SHELL_COMPLETION_TIMEOUT_MS, | |
| }) | |
| const result = await shellCommand.result | |
| return result.stdout | |
| .split('\n') | |
| .filter((line: string) => line.trim()) | |
| .slice(0, MAX_SHELL_COMPLETIONS) | |
| .map((text: string) => ({ | |
| id: text, | |
| displayText: text, | |
| description: undefined, | |
| metadata: { completionType }, | |
| })) | |
| } | |
| /** | |
| * Get shell completions for the given input | |
| * Supports bash and zsh shells (matches Shell.ts execution support) | |
| */ | |
| export async function getShellCompletions( | |
| input: string, | |
| cursorOffset: number, | |
| abortSignal: AbortSignal, | |
| ): Promise<SuggestionItem[]> { | |
| const shellType = getShellType() | |
| // Only support bash/zsh (matches Shell.ts execution support) | |
| if (shellType !== 'bash' && shellType !== 'zsh') { | |
| return [] | |
| } | |
| try { | |
| const { prefix, completionType } = parseInputContext(input, cursorOffset) | |
| if (!prefix) { | |
| return [] | |
| } | |
| const completions = await getCompletionsForShell( | |
| shellType, | |
| prefix, | |
| completionType, | |
| abortSignal, | |
| ) | |
| // Add inputSnapshot to all suggestions so we can detect when input changes | |
| return completions.map(suggestion => ({ | |
| ...suggestion, | |
| metadata: { | |
| ...(suggestion.metadata as { completionType: ShellCompletionType }), | |
| inputSnapshot: input, | |
| }, | |
| })) | |
| } catch (error) { | |
| logForDebugging(`Shell completion failed: ${error}`) | |
| return [] // Silent fail | |
| } | |
| } | |