Inline base64 source maps had been checked into tracked src files. This strips those comments from the repository without changing runtime behavior or adding ongoing guardrails, per the requested one-time cleanup scope. Constraint: Keep this change limited to tracked source cleanup only Rejected: Add CI/source verification guard | user requested one-time cleanup only Confidence: high Scope-risk: narrow Reversibility: clean Directive: If these directives reappear, fix the producing transform instead of reintroducing repo-side cleanup code Tested: rg -n "sourceMappingURL" ., bun run smoke, bun run verify:privacy, bun run test:provider, npm run test:provider-recommendation Not-tested: bun run typecheck (repository has many pre-existing unrelated failures) Co-authored-by: anandh8x <test@example.com>
343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
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) => <Text color={isFocused ? "suggestion" : undefined}><Text dimColor={true}>{truncatePathMiddle(m_7.file, maxPathWidth)}:{m_7.line}</Text>{" "}{highlightMatch(truncateToWidth(m_7.text.trimStart(), maxTextWidth), query)}</Text>;
|
|
$[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 ? <><Text dimColor={true}>{truncatePathMiddle(m_8.file, previewWidth)}:{m_8.line}</Text>{preview.content.split("\n").map((line_0, i) => <Text key={i}>{highlightMatch(truncateToWidth(line_0, previewWidth), query)}</Text>)}</> : <LoadingState message={"Loading\u2026"} dimColor={true} />;
|
|
$[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 = <FuzzyPicker title="Global Search" placeholder={"Type to search\u2026"} items={matches} getKey={matchKey} visibleCount={visibleResults} direction="up" previewPosition={t9} onQueryChange={handleQueryChange} onFocus={setFocused} onSelect={handleOpen} onTab={t10} onShiftTab={t11} onCancel={onDone} emptyMessage={t12} matchLabel={matchLabel} selectAction="open in editor" renderItem={t13} renderPreview={t14} />;
|
|
$[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 :<digits>: 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 ?? ''
|
|
};
|
|
}
|