import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { KEY_SEP } from "@/lib/platform"; import type { EditorPaneHandle } from "@/modules/editor"; import { usePreferencesStore } from "@/modules/settings/preferences"; import { getBindingTokens, SHORTCUTS } from "@/modules/shortcuts/shortcuts"; import { Cancel01Icon, Search01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import type { SearchAddon } from "@xterm/addon-search"; import { AnimatePresence, motion } from "motion/react"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react"; const TERM_DECORATIONS = { matchBackground: "#515c6a", activeMatchBackground: "#d18616", matchOverviewRuler: "#d18616", activeMatchColorOverviewRuler: "#d18616", }; export type SearchTarget = | { kind: "terminal"; addon: SearchAddon; focus: () => void } | { kind: "editor"; handle: EditorPaneHandle; focus: () => void } | null; export type SearchInlineHandle = { focus: () => void }; type Props = { target: SearchTarget; /** When true, collapse to an icon-only button until the user opens it. */ compact?: boolean; }; export const SearchInline = forwardRef( function SearchInline({ target, compact }, ref) { const [q, setQ] = useState(""); // In compact mode the field is hidden behind an icon until activated. // In normal mode the field is always present. const [openInCompact, setOpenInCompact] = useState(false); const inputRef = useRef(null); const pendingFocusRef = useRef(false); const setInputRef = useCallback((el: HTMLInputElement | null) => { inputRef.current = el; if (!el || !pendingFocusRef.current) return; pendingFocusRef.current = false; el.focus(); }, []); const userShortcuts = usePreferencesStore((s) => s.shortcuts); const shortcutText = useMemo(() => { const s = SHORTCUTS.find((s) => s.id === "search.focus"); if (!s) return ""; const bindings = userShortcuts["search.focus"] || s.defaultBindings; if (!bindings || bindings.length === 0) return ""; const tokens = getBindingTokens(bindings[0]); return tokens.join(KEY_SEP); }, [userShortcuts]); const placeholder = useMemo(() => { return shortcutText ? `Search (${shortcutText})` : "Search"; }, [shortcutText]); const tooltipTitle = useMemo(() => { return shortcutText ? `Search (${shortcutText})` : "Search"; }, [shortcutText]); const expanded = !compact || openInCompact; const focus = useCallback(() => { pendingFocusRef.current = true; if (compact) setOpenInCompact(true); else inputRef.current?.focus(); if (inputRef.current) pendingFocusRef.current = false; }, [compact]); useImperativeHandle(ref, () => ({ focus }), [focus]); const clearTarget = useCallback(() => { if (!target) return; if (target.kind === "terminal") target.addon.clearDecorations(); else target.handle.clearQuery(); }, [target]); const restoreTargetFocus = useCallback(() => { if (!target) return; target.focus(); }, [target]); // Target switched (terminal ↔ editor) or removed → drop highlights. useEffect(() => clearTarget, [clearTarget]); const applyIncremental = (next: string) => { if (!target) return; if (target.kind === "terminal") { if (next) { target.addon.findNext(next, { incremental: true, decorations: TERM_DECORATIONS, }); } else { target.addon.clearDecorations(); } } else { target.handle.setQuery(next); } }; const findDirection = (forward: boolean) => { if (!target || !q) return; if (target.kind === "terminal") { const opts = { decorations: TERM_DECORATIONS }; if (forward) target.addon.findNext(q, opts); else target.addon.findPrevious(q, opts); } else { if (forward) target.handle.findNext(); else target.handle.findPrevious(); } }; return ( {expanded ? ( { const next = e.target.value; setQ(next); applyIncremental(next); }} onBlur={() => { if (compact && !q) setOpenInCompact(false); }} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); findDirection(!e.shiftKey); } else if (e.key === "Escape") { e.preventDefault(); clearTarget(); setQ(""); if (compact) { setOpenInCompact(false); } restoreTargetFocus(); } }} /> {q && ( )} ) : ( )} ); }, );