import { c as _c } from "react-compiler-runtime"; import { resolve as resolvePath } from 'path'; import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { useRegisterOverlay } from '../context/overlayContext.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { Text } from '../ink.js'; import { logEvent } from '../services/analytics/index.js'; import { getCwd } from '../utils/cwd.js'; import { openFileInExternalEditor } from '../utils/editor.js'; import { truncatePathMiddle, truncateToWidth } from '../utils/format.js'; import { highlightMatch } from '../utils/highlightMatch.js'; import { relativePath } from '../utils/permissions/filesystem.js'; import { readFileInRange } from '../utils/readFileInRange.js'; import { ripGrepStream } from '../utils/ripgrep.js'; import { FuzzyPicker } from './design-system/FuzzyPicker.js'; import { LoadingState } from './design-system/LoadingState.js'; type Props = { onDone: () => void; onInsert: (text: string) => void; }; type Match = { file: string; line: number; text: string; }; const VISIBLE_RESULTS = 12; const DEBOUNCE_MS = 100; const PREVIEW_CONTEXT_LINES = 4; // rg -m is per-file; we also cap the parsed array to keep memory bounded. const MAX_MATCHES_PER_FILE = 10; const MAX_TOTAL_MATCHES = 500; /** * Global Search dialog (ctrl+shift+f / cmd+shift+f). * Debounced ripgrep search across the workspace. */ export function GlobalSearchDialog(t0) { const $ = _c(40); const { onDone, onInsert } = t0; useRegisterOverlay("global-search"); const { columns, rows } = useTerminalSize(); const previewOnRight = columns >= 140; const visibleResults = Math.min(VISIBLE_RESULTS, Math.max(4, rows - 14)); let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t1 = []; $[0] = t1; } else { t1 = $[0]; } const [matches, setMatches] = useState(t1); const [truncated, setTruncated] = useState(false); const [isSearching, setIsSearching] = useState(false); const [query, setQuery] = useState(""); const [focused, setFocused] = useState(undefined); const [preview, setPreview] = useState(null); const abortRef = useRef(null); const timeoutRef = useRef(null); let t2; let t3; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { t2 = () => () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } abortRef.current?.abort(); }; t3 = []; $[1] = t2; $[2] = t3; } else { t2 = $[1]; t3 = $[2]; } useEffect(t2, t3); let t4; let t5; if ($[3] !== focused) { t4 = () => { if (!focused) { setPreview(null); return; } const controller = new AbortController(); const absolute = resolvePath(getCwd(), focused.file); const start = Math.max(0, focused.line - PREVIEW_CONTEXT_LINES - 1); readFileInRange(absolute, start, PREVIEW_CONTEXT_LINES * 2 + 1, undefined, controller.signal).then(r => { if (controller.signal.aborted) { return; } setPreview({ file: focused.file, line: focused.line, content: r.content }); }).catch(() => { if (controller.signal.aborted) { return; } setPreview({ file: focused.file, line: focused.line, content: "(preview unavailable)" }); }); return () => controller.abort(); }; t5 = [focused]; $[3] = focused; $[4] = t4; $[5] = t5; } else { t4 = $[4]; t5 = $[5]; } useEffect(t4, t5); let t6; if ($[6] === Symbol.for("react.memo_cache_sentinel")) { t6 = q => { setQuery(q); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } abortRef.current?.abort(); if (!q.trim()) { setMatches(_temp); setIsSearching(false); setTruncated(false); return; } const controller_0 = new AbortController(); abortRef.current = controller_0; setIsSearching(true); setTruncated(false); const queryLower = q.toLowerCase(); setMatches(m_0 => { const filtered = m_0.filter(match => match.text.toLowerCase().includes(queryLower)); return filtered.length === m_0.length ? m_0 : filtered; }); timeoutRef.current = setTimeout(_temp4, DEBOUNCE_MS, q, controller_0, setMatches, setTruncated, setIsSearching); }; $[6] = t6; } else { t6 = $[6]; } const handleQueryChange = t6; const listWidth = previewOnRight ? Math.floor((columns - 10) * 0.5) : columns - 8; const maxPathWidth = Math.max(20, Math.floor(listWidth * 0.4)); const maxTextWidth = Math.max(20, listWidth - maxPathWidth - 4); const previewWidth = previewOnRight ? Math.max(40, columns - listWidth - 14) : columns - 6; let t7; if ($[7] !== matches.length || $[8] !== onDone) { t7 = m_3 => { const opened = openFileInExternalEditor(resolvePath(getCwd(), m_3.file), m_3.line); logEvent("tengu_global_search_select", { result_count: matches.length, opened_editor: opened }); onDone(); }; $[7] = matches.length; $[8] = onDone; $[9] = t7; } else { t7 = $[9]; } const handleOpen = t7; let t8; if ($[10] !== matches.length || $[11] !== onDone || $[12] !== onInsert) { t8 = (m_4, mention) => { onInsert(mention ? `@${m_4.file}#L${m_4.line} ` : `${m_4.file}:${m_4.line} `); logEvent("tengu_global_search_insert", { result_count: matches.length, mention }); onDone(); }; $[10] = matches.length; $[11] = onDone; $[12] = onInsert; $[13] = t8; } else { t8 = $[13]; } const handleInsert = t8; const matchLabel = matches.length > 0 ? `${matches.length}${truncated ? "+" : ""} matches${isSearching ? "\u2026" : ""}` : " "; const t9 = previewOnRight ? "right" : "bottom"; let t10; if ($[14] !== handleInsert) { t10 = { action: "mention", handler: m_5 => handleInsert(m_5, true) }; $[14] = handleInsert; $[15] = t10; } else { t10 = $[15]; } let t11; if ($[16] !== handleInsert) { t11 = { action: "insert path", handler: m_6 => handleInsert(m_6, false) }; $[16] = handleInsert; $[17] = t11; } else { t11 = $[17]; } let t12; if ($[18] !== isSearching) { t12 = q_0 => isSearching ? "Searching\u2026" : q_0 ? "No matches" : "Type to search\u2026"; $[18] = isSearching; $[19] = t12; } else { t12 = $[19]; } let t13; if ($[20] !== maxPathWidth || $[21] !== maxTextWidth || $[22] !== query) { t13 = (m_7, isFocused) => {truncatePathMiddle(m_7.file, maxPathWidth)}:{m_7.line}{" "}{highlightMatch(truncateToWidth(m_7.text.trimStart(), maxTextWidth), query)}; $[20] = maxPathWidth; $[21] = maxTextWidth; $[22] = query; $[23] = t13; } else { t13 = $[23]; } let t14; if ($[24] !== preview || $[25] !== previewWidth || $[26] !== query) { t14 = m_8 => preview?.file === m_8.file && preview.line === m_8.line ? <>{truncatePathMiddle(m_8.file, previewWidth)}:{m_8.line}{preview.content.split("\n").map((line_0, i) => {highlightMatch(truncateToWidth(line_0, previewWidth), query)})} : ; $[24] = preview; $[25] = previewWidth; $[26] = query; $[27] = t14; } else { t14 = $[27]; } let t15; if ($[28] !== handleOpen || $[29] !== matchLabel || $[30] !== matches || $[31] !== onDone || $[32] !== t10 || $[33] !== t11 || $[34] !== t12 || $[35] !== t13 || $[36] !== t14 || $[37] !== t9 || $[38] !== visibleResults) { t15 = ; $[28] = handleOpen; $[29] = matchLabel; $[30] = matches; $[31] = onDone; $[32] = t10; $[33] = t11; $[34] = t12; $[35] = t13; $[36] = t14; $[37] = t9; $[38] = visibleResults; $[39] = t15; } else { t15 = $[39]; } return t15; } function _temp4(query_0, controller_1, setMatches_0, setTruncated_0, setIsSearching_0) { const cwd = getCwd(); let collected = 0; ripGrepStream(["-n", "--no-heading", "-i", "-m", String(MAX_MATCHES_PER_FILE), "-F", "-e", query_0], cwd, controller_1.signal, lines => { if (controller_1.signal.aborted) { return; } const parsed = []; for (const line of lines) { const m_1 = parseRipgrepLine(line); if (!m_1) { continue; } const rel = relativePath(cwd, m_1.file); parsed.push({ ...m_1, file: rel.startsWith("..") ? m_1.file : rel }); } if (!parsed.length) { return; } collected = collected + parsed.length; collected; setMatches_0(prev => { const seen = new Set(prev.map(matchKey)); const fresh = parsed.filter(p => !seen.has(matchKey(p))); if (!fresh.length) { return prev; } const next = prev.concat(fresh); return next.length > MAX_TOTAL_MATCHES ? next.slice(0, MAX_TOTAL_MATCHES) : next; }); if (collected >= MAX_TOTAL_MATCHES) { controller_1.abort(); setTruncated_0(true); setIsSearching_0(false); } }).catch(_temp2).finally(() => { if (controller_1.signal.aborted) { return; } if (collected === 0) { setMatches_0(_temp3); } setIsSearching_0(false); }); } function _temp3(m_2) { return m_2.length ? [] : m_2; } function _temp2() {} function _temp(m) { return m.length ? [] : m; } function matchKey(m: Match): string { return `${m.file}:${m.line}`; } /** * Parse a ripgrep -n --no-heading output line: "path:line:text". * Windows paths may contain a drive letter ("C:\..."), so a simple split on * the first colon would mangle the path — use a regex that captures up to * the first :: instead. * @internal exported for testing */ export function parseRipgrepLine(line: string): Match | null { const m = /^(.*?):(\d+):(.*)$/.exec(line); if (!m) return null; const [, file, lineStr, text] = m; const lineNum = Number(lineStr); if (!file || !Number.isFinite(lineNum)) return null; return { file, line: lineNum, text: text ?? '' }; }