mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
Hard-coded UI text, titles and aria-labels in about 110 components (Git panels, hierarchy, pull requests, dialogs, settings, welcome) now go through tx; spliced fragments became whole sentences (fileCount, noneFoundLabel, hierarchyTitle) and pull request states use keyed messages. 225 zh entries added, stale fragment entries removed.
Also: tx used as a tag without interpolations (tx`No checks`) now looks the text up exactly instead of as a template, so it is translated like tx("No checks"); the text-catalog test checks such tags as plain literals. Check and test counts reuse the existing "{count} passed" / "{failed} failing" entries.
Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Co-authored-by: jaw52 <[email protected]>
168 lines
5.4 KiB
TypeScript
168 lines
5.4 KiB
TypeScript
/** ⌘F in the terminal: search the scrollback (xterm addon-search) with Match Case / Words / Regex. */
|
|
import { useEffect, useRef, useState, type KeyboardEvent } from "react";
|
|
import clsx from "clsx";
|
|
import { create } from "zustand";
|
|
import { IconButton, SearchField } from "@/ui";
|
|
import type { TerminalSearchOptions, TerminalSearchResult } from "./session";
|
|
import { activeSession } from "./store";
|
|
import styles from "./Terminal.module.css";
|
|
import { hasPrimaryModifier } from "@/platform/keymap";
|
|
import { tx } from "@/platform/i18n";
|
|
|
|
export const useTerminalFind = create<{ open: boolean; seq: number; query: string; options: TerminalSearchOptions }>(
|
|
() => ({
|
|
open: false,
|
|
seq: 0,
|
|
query: "",
|
|
options: { caseSensitive: false, regex: false, wholeWord: false },
|
|
}),
|
|
);
|
|
|
|
/** Opens (or refocuses) the find bar, seeding it with the terminal selection. */
|
|
export function openTerminalFind() {
|
|
// While open, the terminal selection is the current match: keep the query.
|
|
const selection = useTerminalFind.getState().open ? "" : activeSession()?.term.getSelection().trim();
|
|
useTerminalFind.setState((s) => ({
|
|
open: true,
|
|
seq: s.seq + 1,
|
|
query: selection && !selection.includes("\n") ? selection : s.query,
|
|
}));
|
|
}
|
|
|
|
export function closeTerminalFind() {
|
|
if (!useTerminalFind.getState().open) return;
|
|
useTerminalFind.setState({ open: false });
|
|
const session = activeSession();
|
|
session?.clearSearch();
|
|
session?.focus();
|
|
}
|
|
|
|
/** "3/12", "999+" style counter text. */
|
|
export function matchCounter(r: TerminalSearchResult | null, query: string): string {
|
|
if (!query || !r) return "";
|
|
if (r.count === 0) return "0 results";
|
|
return r.index >= 0 ? `${r.index + 1}/${r.count}` : `${r.count}+`;
|
|
}
|
|
|
|
export function TerminalFindBar({ sessionKey }: { sessionKey: string | null }) {
|
|
const { open, seq, query, options } = useTerminalFind();
|
|
const input = useRef<HTMLInputElement>(null);
|
|
const [result, setResult] = useState<TerminalSearchResult | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
input.current?.focus();
|
|
input.current?.select();
|
|
}, [open, seq]);
|
|
|
|
// Re-run the search on query / option / session changes.
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const session = activeSession();
|
|
if (!session) return;
|
|
const off = session.onSearchResults(setResult);
|
|
// Results arrive through the listener (clearing reports 0 results).
|
|
if (query) session.find(query, options, "next", true);
|
|
else session.clearSearch();
|
|
return () => {
|
|
off();
|
|
session.clearSearch();
|
|
};
|
|
}, [open, query, options, sessionKey]);
|
|
|
|
if (!open) return null;
|
|
|
|
const find = (direction: "next" | "previous") => activeSession()?.find(query, options, direction);
|
|
const toggle = (k: keyof TerminalSearchOptions) =>
|
|
useTerminalFind.setState((s) => ({ options: { ...s.options, [k]: !s.options[k] } }));
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeTerminalFind();
|
|
} else if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
find(e.shiftKey ? "previous" : "next");
|
|
} else if (e.key === "F3" || (hasPrimaryModifier(e) && e.key.toLowerCase() === "g")) {
|
|
e.preventDefault();
|
|
find(e.shiftKey ? "previous" : "next");
|
|
} else if (e.altKey && !e.metaKey && !e.ctrlKey) {
|
|
const k =
|
|
e.code === "KeyC" ? "caseSensitive" : e.code === "KeyW" ? "wholeWord" : e.code === "KeyX" ? "regex" : null;
|
|
if (k) {
|
|
e.preventDefault();
|
|
toggle(k);
|
|
}
|
|
}
|
|
};
|
|
const counter = matchCounter(result, query);
|
|
const none = !!query && result?.count === 0;
|
|
|
|
return (
|
|
<div
|
|
className={styles.find}
|
|
role="search"
|
|
aria-label={tx("Find in terminal")}
|
|
data-terminal-find=""
|
|
onKeyDown={onKeyDown}
|
|
>
|
|
<SearchField
|
|
ref={input}
|
|
className={styles.findField}
|
|
value={query}
|
|
onChange={(q) => useTerminalFind.setState({ query: q })}
|
|
placeholder="Find in terminal"
|
|
aria-label="Find in terminal"
|
|
invalid={none}
|
|
toggles={[
|
|
{
|
|
id: "case",
|
|
icon: "case",
|
|
title: "Match Case",
|
|
shortcut: "⌥C",
|
|
pressed: options.caseSensitive,
|
|
onToggle: () => toggle("caseSensitive"),
|
|
},
|
|
{
|
|
id: "word",
|
|
icon: "word",
|
|
title: "Words",
|
|
shortcut: "⌥W",
|
|
pressed: options.wholeWord,
|
|
onToggle: () => toggle("wholeWord"),
|
|
},
|
|
{
|
|
id: "regex",
|
|
icon: "regex",
|
|
title: "Regex",
|
|
shortcut: "⌥X",
|
|
pressed: options.regex,
|
|
onToggle: () => toggle("regex"),
|
|
},
|
|
]}
|
|
/>
|
|
<span className={clsx(styles.findCount, none && styles.findCountNone)} aria-live="polite">
|
|
{counter}
|
|
</span>
|
|
<IconButton
|
|
icon="arrow-up"
|
|
title="Previous Occurrence"
|
|
shortcut="⇧↵"
|
|
size="small"
|
|
disabled={!query}
|
|
onClick={() => find("previous")}
|
|
/>
|
|
<IconButton
|
|
icon="arrow-down"
|
|
title="Next Occurrence"
|
|
shortcut="↵"
|
|
size="small"
|
|
disabled={!query}
|
|
onClick={() => find("next")}
|
|
/>
|
|
<span className={styles.findSpacer} />
|
|
<IconButton icon="close" title="Close" shortcut="⎋" size="small" onClick={closeTerminalFind} />
|
|
</div>
|
|
);
|
|
}
|