/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useState, useEffect, useCallback } from 'react'; import * as fs from 'fs/promises'; import * as path from 'path'; import { isNodeError } from '@gemini-code/server'; const MAX_SUGGESTIONS_TO_SHOW = 8; export interface UseCompletionReturn { suggestions: string[]; activeSuggestionIndex: number; visibleStartIndex: number; showSuggestions: boolean; isLoadingSuggestions: boolean; setActiveSuggestionIndex: React.Dispatch>; setShowSuggestions: React.Dispatch>; resetCompletionState: () => void; navigateUp: () => void; navigateDown: () => void; } export function useCompletion( query: string, cwd: string, isActive: boolean, ): UseCompletionReturn { const [suggestions, setSuggestions] = useState([]); const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(-1); const [visibleStartIndex, setVisibleStartIndex] = useState(0); const [showSuggestions, setShowSuggestions] = useState(false); const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); const resetCompletionState = useCallback(() => { setSuggestions([]); setActiveSuggestionIndex(-1); setVisibleStartIndex(0); setShowSuggestions(false); setIsLoadingSuggestions(false); }, []); // --- Navigation Logic --- const navigateUp = useCallback(() => { if (suggestions.length === 0) return; setActiveSuggestionIndex((prevIndex) => { const newIndex = prevIndex <= 0 ? suggestions.length - 1 : prevIndex - 1; // Adjust visible window if needed (scrolling up) if (newIndex < visibleStartIndex) { setVisibleStartIndex(newIndex); } else if ( newIndex === suggestions.length - 1 && suggestions.length > MAX_SUGGESTIONS_TO_SHOW ) { // Handle wrapping from first to last item setVisibleStartIndex( Math.max(0, suggestions.length - MAX_SUGGESTIONS_TO_SHOW), ); } return newIndex; }); }, [suggestions.length, visibleStartIndex]); const navigateDown = useCallback(() => { if (suggestions.length === 0) return; setActiveSuggestionIndex((prevIndex) => { const newIndex = prevIndex >= suggestions.length - 1 ? 0 : prevIndex + 1; // Adjust visible window if needed (scrolling down) if (newIndex >= visibleStartIndex + MAX_SUGGESTIONS_TO_SHOW) { setVisibleStartIndex(visibleStartIndex + 1); } else if ( newIndex === 0 && suggestions.length > MAX_SUGGESTIONS_TO_SHOW ) { // Handle wrapping from last to first item setVisibleStartIndex(0); } return newIndex; }); }, [suggestions.length, visibleStartIndex]); // --- End Navigation Logic --- useEffect(() => { if (!isActive) { resetCompletionState(); return; } const atIndex = query.lastIndexOf('@'); if (atIndex === -1) { resetCompletionState(); return; } const partialPath = query.substring(atIndex + 1); const lastSlashIndex = partialPath.lastIndexOf('/'); const baseDirRelative = lastSlashIndex === -1 ? '.' : partialPath.substring(0, lastSlashIndex + 1); const prefix = lastSlashIndex === -1 ? partialPath : partialPath.substring(lastSlashIndex + 1); const baseDirAbsolute = path.resolve(cwd, baseDirRelative); let isMounted = true; const fetchSuggestions = async () => { setIsLoadingSuggestions(true); try { const entries = await fs.readdir(baseDirAbsolute, { withFileTypes: true, }); const filteredSuggestions = entries .filter((entry) => entry.name.startsWith(prefix)) .map((entry) => (entry.isDirectory() ? entry.name + '/' : entry.name)) .sort((a, b) => { // Sort directories first, then alphabetically const aIsDir = a.endsWith('/'); const bIsDir = b.endsWith('/'); if (aIsDir && !bIsDir) return -1; if (!aIsDir && bIsDir) return 1; return a.localeCompare(b); }); if (isMounted) { setSuggestions(filteredSuggestions); setShowSuggestions(filteredSuggestions.length > 0); setActiveSuggestionIndex(-1); // Reset selection on new suggestions setVisibleStartIndex(0); // Reset scroll on new suggestions } } catch (error) { if (isNodeError(error) && error.code === 'ENOENT') { // Directory doesn't exist, likely mid-typing, clear suggestions if (isMounted) { setSuggestions([]); setShowSuggestions(false); } } else { console.error( `Error fetching completion suggestions for ${baseDirAbsolute}:`, error, ); if (isMounted) { resetCompletionState(); } } } if (isMounted) { setIsLoadingSuggestions(false); } }; // Debounce the fetch slightly const debounceTimeout = setTimeout(fetchSuggestions, 100); return () => { isMounted = false; clearTimeout(debounceTimeout); // Don't reset loading state here, let the next effect handle it or resetCompletionState }; }, [query, cwd, isActive, resetCompletionState]); return { suggestions, activeSuggestionIndex, visibleStartIndex, showSuggestions, isLoadingSuggestions, setActiveSuggestionIndex, setShowSuggestions, resetCompletionState, navigateUp, navigateDown, }; }