fix(scratch): add visible language search field

This commit is contained in:
l0ng-ai
2026-10-31 21:21:50 +08:00
parent 2c9a6470ff
commit d6f6525ca5
3 changed files with 44 additions and 29 deletions
@@ -11,23 +11,17 @@
.header {
display: flex;
align-items: center;
gap: 8px;
height: var(--h-row);
padding: 0 10px;
font-size: var(--fs-small);
color: var(--fg-subtle);
border-bottom: 1px solid var(--border-popup);
margin-bottom: 4px;
}
.title {
flex: 1;
}
.filter {
font-family: var(--font-mono);
color: var(--fg-strong);
background: var(--accent-soft);
border-radius: var(--radius-small);
padding: 0 4px;
.search {
margin: 0 8px 4px;
border-bottom: 1px solid var(--border-popup);
}
.list {
max-height: 360px;
+27 -15
View File
@@ -1,12 +1,12 @@
import { useMemo, useState, type KeyboardEvent } from "react";
import { useMemo, useRef, useState, type KeyboardEvent } from "react";
import clsx from "clsx";
import { HighlightedText, Icon, List, Popup, fileIconName, listNavigate, T } from "@/ui";
import { HighlightedText, Icon, List, Popup, SearchField, fileIconName, listNavigate, T } from "@/ui";
import { chooserRows, type LanguageRow, type ScratchLanguage } from "./model";
import { closeChooser, rememberLanguage, useScratch } from "./store";
import { createScratch } from "./scratches";
import styles from "./ScratchChooser.module.css";
/** Overlay: IDEA's "New Scratch File" popup — languages, speed search, recent first. */
/** Overlay: IDEA's "New Scratch File" popup — searchable languages, recent first. */
export function ScratchChooserHost() {
const languages = useScratch((s) => s.languages);
const seq = useScratch((s) => s.seq);
@@ -16,6 +16,7 @@ export function ScratchChooserHost() {
function ScratchChooser({ languages }: { languages: readonly ScratchLanguage[] }) {
const recent = useScratch((s) => s.recent);
const inputRef = useRef<HTMLInputElement>(null);
const [filter, setFilter] = useState("");
const [active, setActive] = useState(0);
const rows = useMemo(() => chooserRows(languages, recent, filter), [languages, recent, filter]);
@@ -29,22 +30,19 @@ function ScratchChooser({ languages }: { languages: readonly ScratchLanguage[] }
void createScratch(row.lang);
};
const onKeyDown = (e: KeyboardEvent) => {
const next = listNavigate(e.key, { items: rows, active: current, wrap: true });
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing || e.metaKey || e.ctrlKey || e.altKey) return;
const next = listNavigate(e.key === "Home" || e.key === "End" ? "" : e.key, {
items: rows,
active: current,
wrap: true,
});
if (next !== null) {
e.preventDefault();
setActive(next);
} else if (e.key === "Enter") {
e.preventDefault();
pick(rows[current]);
} else if (e.key === "Backspace") {
e.preventDefault();
setFilter((f) => f.slice(0, -1));
setActive(0);
} else if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
setFilter((f) => f + e.key);
setActive(0);
}
};
@@ -58,13 +56,27 @@ function ScratchChooser({ languages }: { languages: readonly ScratchLanguage[] }
aria-label="New Scratch File"
className={styles.popup}
>
<div className={styles.body} tabIndex={0} data-autofocus onKeyDown={onKeyDown}>
<div className={styles.body}>
<div className={styles.header}>
<span className={styles.title}>
<T>New Scratch File</T>
</span>
{filter && <span className={styles.filter}>{filter}</span>}
</div>
<SearchField
ref={inputRef}
variant="bare"
data-autofocus
value={filter}
placeholder="Filter languages"
aria-label="Filter languages"
className={styles.search}
onChange={(value) => {
setFilter(value);
setActive(0);
inputRef.current?.focus();
}}
onKeyDown={onKeyDown}
/>
<List
items={rows}
getKey={(r) => r.lang.id}
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { getAction } from "@/platform/actions";
import { onIdeEvent } from "@/platform/ipc";
import { useProjectViewRoots } from "@/workbench/contributions";
@@ -103,12 +104,20 @@ describe("creating scratch files", () => {
await action.run({ ctx: {} as never, arg: undefined });
});
render(<ScratchChooserHost />);
const dialog = screen.getByRole("dialog", { name: "New Scratch File" });
const body = dialog.querySelector<HTMLElement>("[tabindex]")!;
for (const ch of "pyt") fireEvent.keyDown(body, { key: ch });
expect(dialog.textContent).toContain("pyt");
const user = userEvent.setup();
const input = screen.getByRole("textbox", { name: "Filter languages" });
expect(input).toHaveFocus();
await user.paste("java");
expect(input).toHaveValue("java");
expect(screen.queryByText("Python")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Clear" }));
expect(input).toHaveFocus();
expect(input).toHaveValue("");
await user.type(input, "pyton");
await user.keyboard("{ArrowLeft}{ArrowLeft}h");
expect(input).toHaveValue("python");
await act(async () => {
fireEvent.keyDown(body, { key: "Enter" });
fireEvent.keyDown(input, { key: "Enter" });
});
await vi.waitFor(() => expect(opened).toEqual(["/data/scratches/scratch.py"]));
expect(useScratch.getState().languages).toBeNull();