import { useMemo, useState, type KeyboardEvent } from "react"; import clsx from "clsx"; import { HighlightedText, Icon, List, Popup, SearchField, fuzzyMatch, listNavigate, T, type PopupAnchor } from "@/ui"; import { useRunConfigTypes, runConfigTypes, type RunConfigTypeContribution } from "@/workbench/contributions"; import styles from "./Run.module.css"; export type TypeRow = | { kind: "group"; key: string; label: string } | { kind: "type"; key: string; type: RunConfigTypeContribution; indices: number[] | null; nested: boolean }; const ROW = 24; /** "Add New Configuration" rows: types by label, grouped ones under their group; filtered by `query`. */ export function typeRows(types: readonly RunConfigTypeContribution[], query: string): TypeRow[] { const q = query.trim(); const match = (t: RunConfigTypeContribution) => q ? (fuzzyMatch(t.label, q) ?? (t.group && fuzzyMatch(t.group, q) ? [] : null)) : null; const rows: TypeRow[] = []; const groups = new Map(); const entries: Array<{ label: string; group?: string; type?: RunConfigTypeContribution }> = []; for (const t of types) { if (!t.group) { entries.push({ label: t.label, type: t }); continue; } let g = groups.get(t.group); if (!g) { groups.set(t.group, (g = [])); entries.push({ label: t.group, group: t.group }); } g.push(t); } entries.sort((a, b) => a.label.localeCompare(b.label)); for (const e of entries) { if (e.type) { const indices = match(e.type); if (!q || indices) rows.push({ kind: "type", key: e.type.id, type: e.type, indices, nested: false }); continue; } const members = groups .get(e.group!)! .map((t) => ({ t, indices: match(t) })) .filter((x) => !q || x.indices); if (!members.length) continue; rows.push({ kind: "group", key: `group:${e.group}`, label: e.group! }); for (const { t, indices } of members) rows.push({ kind: "type", key: t.id, type: t, indices, nested: true }); } return rows; } /** IDEA's "Add New Configuration" popup: a searchable list of configuration types. */ export function AddConfigPopup({ anchor, onPick, onClose, }: { anchor: PopupAnchor; onPick: (type: RunConfigTypeContribution) => void; onClose: () => void; }) { const types = useRunConfigTypes((s) => s.types); const [query, setQuery] = useState(""); const rows = useMemo(() => typeRows(runConfigTypes(types), query), [types, query]); const isSection = (r: TypeRow) => r.kind === "group"; const first = Math.max( 0, rows.findIndex((r) => r.kind === "type"), ); const [active, setActive] = useState(first); const clamped = Math.min(active, rows.length - 1); const pick = (row: TypeRow | undefined) => { if (row?.kind !== "type") return; onClose(); onPick(row.type); }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); pick(rows[clamped]); return; } const next = listNavigate(e.key, { items: rows, active: clamped, isSection, wrap: true }); if (next !== null) { e.preventDefault(); setActive(next); } }; return (
Add New Configuration
{ setQuery(v); setActive(0); }} placeholder="Search types" aria-label="Search configuration types" data-autofocus="" onKeyDown={onKeyDown} variant="bare" />
aria-label="Configuration types" className={styles.chooserList} items={rows} getKey={(r) => r.key} isSection={isSection} renderSection={(r) => (r.kind === "group" ? r.label : null)} activeIndex={rows[clamped]?.kind === "type" ? clamped : first} onActiveChange={setActive} focusable={false} hoverSelects rowHeight={ROW} onActivate={(r) => pick(r)} empty={ Nothing found } renderItem={(r) => r.kind === "type" ? ( {r.indices?.length ? : r.type.label} ) : null } />
); }