Download packages/cli/src/ui/hooks/useCommandCompletion.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/hooks/useCommandCompletion.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/hooks/useCommandCompletion.tsx
-
curl -L -o useCommandCompletion.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/hooks/useCommandCompletion.tsx
14 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import type React from 'react'; | |
| import { useCallback, useMemo, useEffect, useState } from 'react'; | |
| import type { Suggestion } from '../components/SuggestionsDisplay.js'; | |
| import type { CommandContext, SlashCommand } from '../commands/types.js'; | |
| import type { TextBuffer } from '../components/shared/text-buffer.js'; | |
| import { logicalPosToOffset } from '../components/shared/text-buffer.js'; | |
| import { toCodePoints } from '../utils/textUtils.js'; | |
| import { isSlashCommand } from '../utils/commandUtils.js'; | |
| import { useAtCompletion } from './useAtCompletion.js'; | |
| import { useSlashCompletion } from './useSlashCompletion.js'; | |
| import { useShellCompletion } from './useShellCompletion.js'; | |
| import { | |
| usePromptCompletion, | |
| PROMPT_COMPLETION_MIN_LENGTH, | |
| type PromptCompletion, | |
| } from './usePromptCompletion.js'; | |
| import type { Config } from '@google/gemini-cli-core'; | |
| import { useCompletion } from './useCompletion.js'; | |
| export enum CompletionMode { | |
| IDLE = 'IDLE', | |
| AT = 'AT', | |
| SLASH = 'SLASH', | |
| PROMPT = 'PROMPT', | |
| SHELL = 'SHELL', | |
| } | |
| export interface UseCommandCompletionReturn { | |
| suggestions: Suggestion[]; | |
| activeSuggestionIndex: number; | |
| visibleStartIndex: number; | |
| showSuggestions: boolean; | |
| isLoadingSuggestions: boolean; | |
| isPerfectMatch: boolean; | |
| forceShowShellSuggestions: boolean; | |
| setForceShowShellSuggestions: (value: boolean) => void; | |
| isShellSuggestionsVisible: boolean; | |
| setActiveSuggestionIndex: React.Dispatch<React.SetStateAction<number>>; | |
| resetCompletionState: () => void; | |
| navigateUp: () => void; | |
| navigateDown: () => void; | |
| handleAutocomplete: (indexToUse: number) => void; | |
| promptCompletion: PromptCompletion; | |
| getCommandFromSuggestion: ( | |
| suggestion: Suggestion, | |
| ) => SlashCommand | undefined; | |
| slashCompletionRange: { | |
| completionStart: number; | |
| completionEnd: number; | |
| getCommandFromSuggestion: ( | |
| suggestion: Suggestion, | |
| ) => SlashCommand | undefined; | |
| isArgumentCompletion: boolean; | |
| leafCommand: SlashCommand | null; | |
| }; | |
| getCompletedText: (suggestion: Suggestion) => string | null; | |
| completionMode: CompletionMode; | |
| } | |
| export interface UseCommandCompletionOptions { | |
| buffer: TextBuffer; | |
| cwd: string; | |
| slashCommands: readonly SlashCommand[]; | |
| commandContext: CommandContext; | |
| reverseSearchActive?: boolean; | |
| shellModeActive: boolean; | |
| config?: Config; | |
| active: boolean; | |
| } | |
| export function useCommandCompletion({ | |
| buffer, | |
| cwd, | |
| slashCommands, | |
| commandContext, | |
| reverseSearchActive = false, | |
| shellModeActive, | |
| config, | |
| active, | |
| }: UseCommandCompletionOptions): UseCommandCompletionReturn { | |
| const [forceShowShellSuggestions, setForceShowShellSuggestions] = | |
| useState(false); | |
| const { | |
| suggestions, | |
| activeSuggestionIndex, | |
| visibleStartIndex, | |
| isLoadingSuggestions, | |
| isPerfectMatch, | |
| setSuggestions, | |
| setActiveSuggestionIndex, | |
| setIsLoadingSuggestions, | |
| setIsPerfectMatch, | |
| setVisibleStartIndex, | |
| resetCompletionState: baseResetCompletionState, | |
| navigateUp, | |
| navigateDown, | |
| } = useCompletion(); | |
| const resetCompletionState = useCallback(() => { | |
| baseResetCompletionState(); | |
| setForceShowShellSuggestions(false); | |
| }, [baseResetCompletionState]); | |
| const cursorRow = buffer.cursor[0]; | |
| const cursorCol = buffer.cursor[1]; | |
| const { | |
| completionMode, | |
| query: memoQuery, | |
| completionStart, | |
| completionEnd, | |
| } = useMemo(() => { | |
| const currentLine = buffer.lines[cursorRow] || ''; | |
| const codePoints = toCodePoints(currentLine); | |
| if (shellModeActive) { | |
| return { | |
| completionMode: | |
| currentLine.trim().length === 0 | |
| ? CompletionMode.IDLE | |
| : CompletionMode.SHELL, | |
| query: '', | |
| completionStart: -1, | |
| completionEnd: -1, | |
| }; | |
| } | |
| // FIRST: Check for @ completion (scan backwards from cursor) | |
| // This must happen before slash command check so that `/cmd @file` | |
| // triggers file completion, not just slash command completion. | |
| for (let i = cursorCol - 1; i >= 0; i--) { | |
| const char = codePoints[i]; | |
| if (char === ' ') { | |
| let backslashCount = 0; | |
| for (let j = i - 1; j >= 0 && codePoints[j] === '\\'; j--) { | |
| backslashCount++; | |
| } | |
| if (backslashCount % 2 === 0) { | |
| break; | |
| } | |
| } else if (char === '@') { | |
| let end = codePoints.length; | |
| for (let i = cursorCol; i < codePoints.length; i++) { | |
| if (codePoints[i] === ' ') { | |
| let backslashCount = 0; | |
| for (let j = i - 1; j >= 0 && codePoints[j] === '\\'; j--) { | |
| backslashCount++; | |
| } | |
| if (backslashCount % 2 === 0) { | |
| end = i; | |
| break; | |
| } | |
| } | |
| } | |
| const pathStart = i + 1; | |
| const partialPath = currentLine.substring(pathStart, end); | |
| return { | |
| completionMode: CompletionMode.AT, | |
| query: partialPath, | |
| completionStart: pathStart, | |
| completionEnd: end, | |
| }; | |
| } | |
| } | |
| // THEN: Check for slash command (only if no @ completion is active) | |
| if (cursorRow === 0 && isSlashCommand(currentLine.trim())) { | |
| return { | |
| completionMode: CompletionMode.SLASH, | |
| query: currentLine, | |
| completionStart: 0, | |
| completionEnd: currentLine.length, | |
| }; | |
| } | |
| // Check for prompt completion - only if enabled | |
| const trimmedText = buffer.text.trim(); | |
| const isPromptCompletionEnabled = false; | |
| if ( | |
| isPromptCompletionEnabled && | |
| trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH && | |
| !isSlashCommand(trimmedText) && | |
| !trimmedText.includes('@') | |
| ) { | |
| return { | |
| completionMode: CompletionMode.PROMPT, | |
| query: trimmedText, | |
| completionStart: 0, | |
| completionEnd: trimmedText.length, | |
| }; | |
| } | |
| return { | |
| completionMode: CompletionMode.IDLE, | |
| query: null, | |
| completionStart: -1, | |
| completionEnd: -1, | |
| }; | |
| }, [cursorRow, cursorCol, buffer.lines, buffer.text, shellModeActive]); | |
| useAtCompletion({ | |
| enabled: active && completionMode === CompletionMode.AT, | |
| pattern: memoQuery || '', | |
| config, | |
| cwd, | |
| setSuggestions, | |
| setIsLoadingSuggestions, | |
| }); | |
| const slashCompletionRange = useSlashCompletion({ | |
| enabled: | |
| active && completionMode === CompletionMode.SLASH && !shellModeActive, | |
| query: memoQuery, | |
| slashCommands, | |
| commandContext, | |
| setSuggestions, | |
| setIsLoadingSuggestions, | |
| setIsPerfectMatch, | |
| }); | |
| const shellCompletionRange = useShellCompletion({ | |
| enabled: active && completionMode === CompletionMode.SHELL, | |
| line: buffer.lines[cursorRow] || '', | |
| cursorCol, | |
| cwd, | |
| setSuggestions, | |
| setIsLoadingSuggestions, | |
| }); | |
| const query = | |
| completionMode === CompletionMode.SHELL | |
| ? shellCompletionRange.query | |
| : memoQuery; | |
| const basePromptCompletion = usePromptCompletion({ | |
| buffer, | |
| }); | |
| const isShellSuggestionsVisible = | |
| completionMode !== CompletionMode.SHELL || forceShowShellSuggestions; | |
| const promptCompletion = useMemo(() => { | |
| if ( | |
| completionMode === CompletionMode.SHELL && | |
| suggestions.length === 1 && | |
| query != null && | |
| shellCompletionRange.completionStart === shellCompletionRange.activeStart | |
| ) { | |
| const suggestion = suggestions[0]; | |
| const textToInsertBase = suggestion.value; | |
| if ( | |
| textToInsertBase.startsWith(query) && | |
| textToInsertBase.length > query.length | |
| ) { | |
| const currentLine = buffer.lines[cursorRow] || ''; | |
| const start = shellCompletionRange.completionStart; | |
| const end = shellCompletionRange.completionEnd; | |
| let textToInsert = textToInsertBase; | |
| const charAfterCompletion = currentLine[end]; | |
| if ( | |
| charAfterCompletion !== ' ' && | |
| !textToInsert.endsWith('/') && | |
| !textToInsert.endsWith('\\') | |
| ) { | |
| textToInsert += ' '; | |
| } | |
| const newText = | |
| currentLine.substring(0, start) + | |
| textToInsert + | |
| currentLine.substring(end); | |
| return { | |
| text: newText, | |
| isActive: true, | |
| isLoading: false, | |
| accept: () => { | |
| buffer.replaceRangeByOffset( | |
| logicalPosToOffset(buffer.lines, cursorRow, start), | |
| logicalPosToOffset(buffer.lines, cursorRow, end), | |
| textToInsert, | |
| ); | |
| }, | |
| clear: () => {}, | |
| markSelected: () => {}, | |
| }; | |
| } | |
| } | |
| return basePromptCompletion; | |
| }, [ | |
| completionMode, | |
| suggestions, | |
| query, | |
| basePromptCompletion, | |
| buffer, | |
| cursorRow, | |
| shellCompletionRange, | |
| ]); | |
| useEffect(() => { | |
| setActiveSuggestionIndex(suggestions.length > 0 ? 0 : -1); | |
| setVisibleStartIndex(0); | |
| // Generic perfect match detection for non-slash modes or as a fallback | |
| if (completionMode !== CompletionMode.SLASH) { | |
| if (suggestions.length > 0) { | |
| const firstSuggestion = suggestions[0]; | |
| setIsPerfectMatch(firstSuggestion.value === query); | |
| } else { | |
| setIsPerfectMatch(false); | |
| } | |
| } | |
| }, [ | |
| suggestions, | |
| setActiveSuggestionIndex, | |
| setVisibleStartIndex, | |
| completionMode, | |
| query, | |
| setIsPerfectMatch, | |
| ]); | |
| useEffect(() => { | |
| if ( | |
| !active || | |
| completionMode === CompletionMode.IDLE || | |
| reverseSearchActive | |
| ) { | |
| resetCompletionState(); | |
| } | |
| }, [active, completionMode, reverseSearchActive, resetCompletionState]); | |
| const showSuggestions = | |
| active && | |
| completionMode !== CompletionMode.IDLE && | |
| !reverseSearchActive && | |
| isShellSuggestionsVisible && | |
| (isLoadingSuggestions || suggestions.length > 0); | |
| /** | |
| * Gets the completed text by replacing the completion range with the suggestion value. | |
| * This is the core string replacement logic used by both autocomplete and auto-execute. | |
| * | |
| * @param suggestion The suggestion to apply | |
| * @returns The completed text with the suggestion applied, or null if invalid | |
| */ | |
| const getCompletedText = useCallback( | |
| (suggestion: Suggestion): string | null => { | |
| const currentLine = buffer.lines[cursorRow] || ''; | |
| let start = completionStart; | |
| let end = completionEnd; | |
| if (completionMode === CompletionMode.SLASH) { | |
| start = slashCompletionRange.completionStart; | |
| end = slashCompletionRange.completionEnd; | |
| } else if (completionMode === CompletionMode.SHELL) { | |
| start = shellCompletionRange.completionStart; | |
| end = shellCompletionRange.completionEnd; | |
| } | |
| if (start === -1 || end === -1) { | |
| return null; | |
| } | |
| // Apply space padding for slash commands (needed for subcommands like "/chat list") | |
| let suggestionText = suggestion.insertValue ?? suggestion.value; | |
| if (completionMode === CompletionMode.SLASH) { | |
| // Add leading space if completing a subcommand (cursor is after parent command with no space) | |
| if (start === end && start > 1 && currentLine[start - 1] !== ' ') { | |
| suggestionText = ' ' + suggestionText; | |
| } | |
| } | |
| // Build the completed text with proper spacing | |
| return ( | |
| currentLine.substring(0, start) + | |
| suggestionText + | |
| currentLine.substring(end) | |
| ); | |
| }, | |
| [ | |
| cursorRow, | |
| buffer.lines, | |
| completionMode, | |
| completionStart, | |
| completionEnd, | |
| slashCompletionRange, | |
| shellCompletionRange, | |
| ], | |
| ); | |
| const handleAutocomplete = useCallback( | |
| (indexToUse: number) => { | |
| if (indexToUse < 0 || indexToUse >= suggestions.length) { | |
| return; | |
| } | |
| const suggestion = suggestions[indexToUse]; | |
| const completedText = getCompletedText(suggestion); | |
| if (completedText === null) { | |
| return; | |
| } | |
| let start = completionStart; | |
| let end = completionEnd; | |
| if (completionMode === CompletionMode.SLASH) { | |
| start = slashCompletionRange.completionStart; | |
| end = slashCompletionRange.completionEnd; | |
| } else if (completionMode === CompletionMode.SHELL) { | |
| start = shellCompletionRange.completionStart; | |
| end = shellCompletionRange.completionEnd; | |
| } | |
| // Add space padding for Tab completion (auto-execute gets padding from getCompletedText) | |
| let suggestionText = suggestion.insertValue ?? suggestion.value; | |
| if (completionMode === CompletionMode.SLASH) { | |
| if ( | |
| start === end && | |
| start > 1 && | |
| (buffer.lines[cursorRow] || '')[start - 1] !== ' ' | |
| ) { | |
| suggestionText = ' ' + suggestionText; | |
| } | |
| } | |
| const lineCodePoints = toCodePoints(buffer.lines[cursorRow] || ''); | |
| const charAfterCompletion = lineCodePoints[end]; | |
| const shouldAddSpace = true; | |
| if ( | |
| charAfterCompletion !== ' ' && | |
| !suggestionText.endsWith('/') && | |
| !suggestionText.endsWith('\\') && | |
| shouldAddSpace | |
| ) { | |
| suggestionText += ' '; | |
| } | |
| buffer.replaceRangeByOffset( | |
| logicalPosToOffset(buffer.lines, cursorRow, start), | |
| logicalPosToOffset(buffer.lines, cursorRow, end), | |
| suggestionText, | |
| ); | |
| }, | |
| [ | |
| cursorRow, | |
| buffer, | |
| suggestions, | |
| completionMode, | |
| completionStart, | |
| completionEnd, | |
| slashCompletionRange, | |
| shellCompletionRange, | |
| getCompletedText, | |
| ], | |
| ); | |
| return { | |
| suggestions, | |
| activeSuggestionIndex, | |
| visibleStartIndex, | |
| showSuggestions, | |
| isLoadingSuggestions, | |
| isPerfectMatch, | |
| forceShowShellSuggestions, | |
| setForceShowShellSuggestions, | |
| isShellSuggestionsVisible, | |
| setActiveSuggestionIndex, | |
| resetCompletionState, | |
| navigateUp, | |
| navigateDown, | |
| handleAutocomplete, | |
| promptCompletion, | |
| getCommandFromSuggestion: slashCompletionRange.getCommandFromSuggestion, | |
| slashCompletionRange, | |
| getCompletedText, | |
| completionMode, | |
| }; | |
| } | |