diff --git a/src/features/search/__tests__/searchKeys.test.ts b/src/features/search/__tests__/searchKeys.test.ts new file mode 100644 index 000000000..daab78cb3 --- /dev/null +++ b/src/features/search/__tests__/searchKeys.test.ts @@ -0,0 +1,36 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { registerAction } from "@/platform/actions"; +import { setContext } from "@/platform/context"; +import { installKeyboardDispatcher, useKeymap } from "@/platform/keymap"; +import { claimSearchKeys } from "../everywhere/SearchEverywhere"; + +const disposers: Array<() => void> = []; +afterEach(() => { + disposers.splice(0).forEach((d) => d()); + document.body.innerHTML = ""; +}); + +describe("Search Everywhere's query field", () => { + it("keeps ⌥↵ and the navigation keys from the editor's actions behind the popup", () => { + const intentions = vi.fn(); + const extend = vi.fn(); + useKeymap.setState({ overrides: { "t.intentions": ["Alt+Enter"], "t.extend": ["Alt+ArrowUp"] } } as never); + setContext("textEditorActive", true); + const input = document.body.appendChild(document.createElement("input")); + const other = document.body.appendChild(document.createElement("input")); + disposers.push( + registerAction({ id: "t.intentions", title: "Show Context Actions", run: intentions }), + registerAction({ id: "t.extend", title: "Extend Selection", run: extend }), + installKeyboardDispatcher(window), + claimSearchKeys(() => input), + ); + const press = (target: HTMLElement, key: string) => + target.dispatchEvent(new KeyboardEvent("keydown", { key, altKey: true, bubbles: true, cancelable: true })); + press(input, "Enter"); + press(input, "ArrowUp"); + expect(intentions).not.toHaveBeenCalled(); + expect(extend).not.toHaveBeenCalled(); + press(other, "Enter"); + expect(intentions).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/features/search/everywhere/SearchEverywhere.tsx b/src/features/search/everywhere/SearchEverywhere.tsx index 650182818..b991625f4 100644 --- a/src/features/search/everywhere/SearchEverywhere.tsx +++ b/src/features/search/everywhere/SearchEverywhere.tsx @@ -4,7 +4,7 @@ import clsx from "clsx"; import { actionToggled, executeAction, executeActionFrom, getAction } from "@/platform/actions"; import type { ProjectFileSet } from "@/platform/ipc"; import { tx as translate, useTx } from "@/platform/i18n"; -import { hasPrimaryModifier, shortcutLabel } from "@/platform/keymap"; +import { hasPrimaryModifier, registerKeyClaim, shortcutLabel } from "@/platform/keymap"; import { Checkbox, Dialog, @@ -73,6 +73,13 @@ const SE_SCOPES: Array<{ value: SeScope; label: string }> = [ { value: "test", label: "Project Test Files" }, ]; +/** Keys the query field handles itself (`onKeyDown`), in every modifier combination. */ +const SEARCH_KEYS: ReadonlySet = new Set(["Enter", "Tab", "ArrowUp", "ArrowDown", "PageUp", "PageDown"]); + +/** Keeps the global keymap off the keys the query field handles while it has focus. */ +export function claimSearchKeys(input: () => HTMLElement | null): () => void { + return registerKeyClaim((e) => e.target === input() && SEARCH_KEYS.has(e.key)); +} const ROW = 24; const MAX_LIST = 672; const MIN_LIST = 10 * ROW; @@ -313,6 +320,10 @@ function SearchEverywhereDialog() { activateSelection(pending.side); }); + // The keys the input handles below are its own: with an editor open, the global keymap would + // otherwise run editor actions on them (⌥↵ Show Context Actions, ⌥↑ Extend Selection…). + useEffect(() => claimSearchKeys(() => inputRef.current), []); + /** IDEA's ⌥↵ on an action: record a shortcut for it (written to the active keymap). */ const assignShortcut = (id: string) => closeThen(() => void executeAction("settings.assignShortcut", id));