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'// Constantsconst MAX_SHELL_COMPLETIONS = 15const SHELL_COMPLETION_TIMEOUT_MS = 1000const COMMAND_OPERATORS = ['|', '||', '&&', ';'] as constexport type ShellCompletionType = 'command' | 'variable' | 'file'type InputContext = {prefix: stringcompletionType: 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-quoteconst varMatch = beforeCursor.match(/\$[a-zA-Z_][a-zA-Z0-9_]*$/)if (varMatch) {return { prefix: varMatch[0], completionType: 'variable' }}// Parse with shell-quoteconst parseResult = tryParseShellCommand(beforeCursor)if (!parseResult.success) {// Fallback to simple parsingconst 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 tokenconst lastToken = findLastStringToken(parseResult.tokens)if (!lastToken) {// No string token found - check if after operatorconst lastParsedToken = parseResult.tokens[parseResult.tokens.length - 1]const completionType =lastParsedToken && isCommandOperator(lastParsedToken)? 'command': 'command' // Default to command at startreturn { prefix: '', completionType }}// If there's a trailing space, the user is starting a new argumentif (beforeCursor.endsWith('')) {// After first token (command) with space = file argument expectedreturn { prefix: '', completionType: 'file' }}// Determine completion type from contextconst baseType = getCompletionTypeFromPrefix(lastToken.token)// If it's clearly a file or variable based on prefix, use that typeif (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 argumentreturn { prefix: lastToken.token, completionType }}/*** Generate bash completion command using compgen*/function getBashCompletionCommand(prefix: string,completionType: ShellCompletionType,): string {if (completionType === 'variable') {// Variable completion - remove $ prefixconst 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 newlinesreturn `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 completionreturn `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 filteringconst 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 filteringreturn `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: stringif (shellType === 'bash') {command = getBashCompletionCommand(prefix, completionType)} else if (shellType === 'zsh') {command = getZshCompletionCommand(prefix, completionType)} else {// Unsupported shell typereturn []}const shellCommand = await Shell.exec(command, abortSignal, 'bash', {timeout: SHELL_COMPLETION_TIMEOUT_MS,})const result = await shellCommand.resultreturn 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 changesreturn completions.map(suggestion => ({...suggestion,metadata: {...(suggestion.metadata as { completionType: ShellCompletionType }),inputSnapshot: input,},}))} catch (error) {logForDebugging(`Shell completion failed: ${error}`)return [] // Silent fail}}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。