From 9f20c5f95e43bccee21b1d89e33fbc3c61a70650 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Wed, 30 Apr 2025 08:31:32 -0700 Subject: Add @ command suggestions in the UI. (#219) --- packages/cli/src/ui/components/InputPrompt.tsx | 114 ++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 14 deletions(-) (limited to 'packages/cli/src/ui/components/InputPrompt.tsx') diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 40956647..67727fd2 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -4,28 +4,113 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { Text, Box, useInput, useFocus } from 'ink'; +import React, { useCallback } from 'react'; +import { Text, Box, useInput, useFocus, Key } from 'ink'; import TextInput from 'ink-text-input'; import { Colors } from '../colors.js'; interface InputPromptProps { + query: string; + setQuery: React.Dispatch>; + inputKey: number; + setInputKey: React.Dispatch>; onSubmit: (value: string) => void; + showSuggestions: boolean; + suggestions: string[]; + activeSuggestionIndex: number; + navigateUp: () => void; + navigateDown: () => void; + resetCompletion: () => void; } -export const InputPrompt: React.FC = ({ onSubmit }) => { - const [value, setValue] = React.useState(''); - +export const InputPrompt: React.FC = ({ + query, + setQuery, + inputKey, + setInputKey, + onSubmit, + showSuggestions, + suggestions, + activeSuggestionIndex, + navigateUp, + navigateDown, + resetCompletion, +}) => { const { isFocused } = useFocus({ autoFocus: true }); + const handleAutocomplete = useCallback(() => { + if ( + activeSuggestionIndex < 0 || + activeSuggestionIndex >= suggestions.length + ) { + return; + } + const selectedSuggestion = suggestions[activeSuggestionIndex]; + const atIndex = query.lastIndexOf('@'); + if (atIndex === -1) return; + + // Find the part of the query after the '@' + const pathPart = query.substring(atIndex + 1); + // Find the last slash within that part + const lastSlashIndexInPath = pathPart.lastIndexOf('/'); + + let base = ''; + if (lastSlashIndexInPath === -1) { + // No slash after '@', replace everything after '@' + base = query.substring(0, atIndex + 1); + } else { + // Slash found, keep everything up to and including the last slash + base = query.substring(0, atIndex + 1 + lastSlashIndexInPath + 1); + } + + const newValue = base + selectedSuggestion; + setQuery(newValue); + resetCompletion(); // Hide suggestions after selection + setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset + }, [ + query, + setQuery, + suggestions, + activeSuggestionIndex, + resetCompletion, + setInputKey, + ]); + useInput( - (input, key) => { - if (key.return) { - if (value.trim()) { - onSubmit(value); - setValue(''); + (input: string, key: Key) => { + let handled = false; + + if (showSuggestions) { + if (key.upArrow) { + navigateUp(); + handled = true; + } else if (key.downArrow) { + navigateDown(); + handled = true; + } else if ((key.tab || key.return) && activeSuggestionIndex >= 0) { + handleAutocomplete(); + handled = true; + } else if (key.escape) { + resetCompletion(); + handled = true; } } + + // Only submit on Enter if it wasn't handled above + if (!handled && key.return) { + if (query.trim()) { + onSubmit(query); + } + handled = true; + } + + if ( + handled && + showSuggestions && + (key.upArrow || key.downArrow || key.tab || key.escape || key.return) + ) { + // No explicit preventDefault needed, handled flag stops further processing + } }, { isActive: isFocused }, ); @@ -35,11 +120,12 @@ export const InputPrompt: React.FC = ({ onSubmit }) => { > { - /* Empty to prevent double submission */ + /* onSubmit is handled by useInput hook above */ }} /> -- cgit v1.2.3