diff options
| author | Sandy Tao <[email protected]> | 2025-07-31 16:07:12 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-07-31 23:07:12 +0000 |
| commit | 32809a7be795b974506b893a179091a83b285b7b (patch) | |
| tree | a81fb93f01ba2d5a412f971e45d51fc55b1b5a0a /packages/cli/src/ui/hooks/useCompletion.ts | |
| parent | 37a3f1e6b61b74b124a0573c408447aa00a62467 (diff) | |
feat(cli): Improve @ autocompletion for mid-sentence edits (#5321)
Diffstat (limited to 'packages/cli/src/ui/hooks/useCompletion.ts')
| -rw-r--r-- | packages/cli/src/ui/hooks/useCompletion.ts | 78 |
1 files changed, 49 insertions, 29 deletions
diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts index 4b106c1b..77b0ded4 100644 --- a/packages/cli/src/ui/hooks/useCompletion.ts +++ b/packages/cli/src/ui/hooks/useCompletion.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useState, useEffect, useCallback, useMemo } from 'react'; +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import * as fs from 'fs/promises'; import * as path from 'path'; import { glob } from 'glob'; @@ -22,7 +22,10 @@ import { Suggestion, } from '../components/SuggestionsDisplay.js'; import { CommandContext, SlashCommand } from '../commands/types.js'; -import { TextBuffer } from '../components/shared/text-buffer.js'; +import { + logicalPosToOffset, + TextBuffer, +} from '../components/shared/text-buffer.js'; import { isSlashCommand } from '../utils/commandUtils.js'; import { toCodePoints } from '../utils/textUtils.js'; @@ -57,6 +60,11 @@ export function useCompletion( const [isLoadingSuggestions, setIsLoadingSuggestions] = useState<boolean>(false); const [isPerfectMatch, setIsPerfectMatch] = useState<boolean>(false); + const completionStart = useRef(-1); + const completionEnd = useRef(-1); + + const cursorRow = buffer.cursor[0]; + const cursorCol = buffer.cursor[1]; const resetCompletionState = useCallback(() => { setSuggestions([]); @@ -127,17 +135,15 @@ export function useCompletion( }, [suggestions.length]); // Check if cursor is after @ or / without unescaped spaces - const isActive = useMemo(() => { + const commandIndex = useMemo(() => { if (isSlashCommand(buffer.text.trim())) { - return true; + return 0; } // For other completions like '@', we search backwards from the cursor. - const [row, col] = buffer.cursor; - const currentLine = buffer.lines[row] || ''; - const codePoints = toCodePoints(currentLine); - for (let i = col - 1; i >= 0; i--) { + const codePoints = toCodePoints(buffer.lines[cursorRow] || ''); + for (let i = cursorCol - 1; i >= 0; i--) { const char = codePoints[i]; if (char === ' ') { @@ -147,19 +153,19 @@ export function useCompletion( backslashCount++; } if (backslashCount % 2 === 0) { - return false; // Inactive on unescaped space. + return -1; // Inactive on unescaped space. } } else if (char === '@') { // Active if we find an '@' before any unescaped space. - return true; + return i; } } - return false; - }, [buffer.text, buffer.cursor, buffer.lines]); + return -1; + }, [buffer.text, cursorRow, cursorCol, buffer.lines]); useEffect(() => { - if (!isActive) { + if (commandIndex === -1) { resetCompletionState(); return; } @@ -311,14 +317,29 @@ export function useCompletion( } // Handle At Command Completion - const atIndex = buffer.text.lastIndexOf('@'); - if (atIndex === -1) { - resetCompletionState(); - return; + const currentLine = buffer.lines[cursorRow] || ''; + const codePoints = toCodePoints(currentLine); + + completionEnd.current = 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) { + completionEnd.current = i; + break; + } + } } - const partialPath = buffer.text.substring(atIndex + 1); + const pathStart = commandIndex + 1; + const partialPath = currentLine.substring(pathStart, completionEnd.current); const lastSlashIndex = partialPath.lastIndexOf('/'); + completionStart.current = + lastSlashIndex === -1 ? pathStart : pathStart + lastSlashIndex + 1; const baseDirRelative = lastSlashIndex === -1 ? '.' @@ -601,9 +622,12 @@ export function useCompletion( }; }, [ buffer.text, + cursorRow, + cursorCol, + buffer.lines, dirs, cwd, - isActive, + commandIndex, resetCompletionState, slashCommands, commandContext, @@ -669,23 +693,19 @@ export function useCompletion( buffer.setText(newValue); } else { - const atIndex = query.lastIndexOf('@'); - if (atIndex === -1) return; - const pathPart = query.substring(atIndex + 1); - const lastSlashIndexInPath = pathPart.lastIndexOf('/'); - let autoCompleteStartIndex = atIndex + 1; - if (lastSlashIndexInPath !== -1) { - autoCompleteStartIndex += lastSlashIndexInPath + 1; + if (completionStart.current === -1 || completionEnd.current === -1) { + return; } + buffer.replaceRangeByOffset( - autoCompleteStartIndex, - buffer.text.length, + logicalPosToOffset(buffer.lines, cursorRow, completionStart.current), + logicalPosToOffset(buffer.lines, cursorRow, completionEnd.current), suggestion, ); } resetCompletionState(); }, - [resetCompletionState, buffer, suggestions, slashCommands], + [cursorRow, resetCompletionState, buffer, suggestions, slashCommands], ); return { |
