diff --git a/src/features/scratch/ScratchChooser.module.css b/src/features/scratch/ScratchChooser.module.css index 4f2945e1..6222bf36 100644 --- a/src/features/scratch/ScratchChooser.module.css +++ b/src/features/scratch/ScratchChooser.module.css @@ -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; diff --git a/src/features/scratch/ScratchChooser.tsx b/src/features/scratch/ScratchChooser.tsx index 58ab7b2b..857cc0bf 100644 --- a/src/features/scratch/ScratchChooser.tsx +++ b/src/features/scratch/ScratchChooser.tsx @@ -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(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) => { + 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} > -
+
New Scratch File - {filter && {filter}}
+ { + setFilter(value); + setActive(0); + inputRef.current?.focus(); + }} + onKeyDown={onKeyDown} + /> r.lang.id} diff --git a/src/features/scratch/__tests__/scratch.test.tsx b/src/features/scratch/__tests__/scratch.test.tsx index 5ba67b65..016a487d 100644 --- a/src/features/scratch/__tests__/scratch.test.tsx +++ b/src/features/scratch/__tests__/scratch.test.tsx @@ -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(); - const dialog = screen.getByRole("dialog", { name: "New Scratch File" }); - const body = dialog.querySelector("[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();