From fcc8583f36bc73a15662fa63e5c5ed7de703a92e Mon Sep 17 00:00:00 2001 From: Mr0Bread <55754540+Mr0Bread@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:15:07 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E2=8C=98=E2=8C=AB=20erases=20a=20tr?= =?UTF-8?q?ee's=20speed=20search=20query=20(#3226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While a tree's speed search has a query, the keymap dispatcher ran ⌫ shortcuts before the tree saw the key: in the Database view ⌘⌫ opened Drop or Remove Data Source. The shared Tree now claims ⌫ with any modifier while a query is active and edits the query like a text field: ⌘⌫ erases it, ⌥⌫ / Ctrl+⌫ erase the last word. Without a query the view's shortcuts run as before. Co-authored-by: Mr0Bread <55754540+Mr0Bread@users.noreply.github.com> --- src/ui/Tree.test.tsx | 26 ++++++++++++++++++++++++++ src/ui/Tree.tsx | 16 +++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/ui/Tree.test.tsx b/src/ui/Tree.test.tsx index c1282f642..b9ba814d8 100644 --- a/src/ui/Tree.test.tsx +++ b/src/ui/Tree.test.tsx @@ -3,6 +3,8 @@ import { createRef } from "react"; import { act, fireEvent, render, screen, within } from "@testing-library/react"; import { Tree, type TreeHandle } from "./Tree"; import { getContext } from "@/platform/context"; +import { registerAction } from "@/platform/actions"; +import { installKeyboardDispatcher, useKeymap } from "@/platform/keymap"; interface N { id: string; @@ -251,6 +253,30 @@ describe("Tree speed search", () => { expect(labels(tree)).toContain("App.tsx"); }); + it("⌘⌫ erases the query and ⌥⌫ its last word instead of running the view's ⌘⌫ action", () => { + const drop = vi.fn(); + useKeymap.setState({ preset: "intellij-macos", overrides: { "test.drop": ["Meta+Backspace", "Alt+Backspace"] } }); + const dispose = [registerAction({ id: "test.drop", title: "Drop", run: drop }), installKeyboardDispatcher()]; + try { + const { tree, lead, key } = setup({ defaultExpanded: ["src", "main"] }); + key("ArrowDown"); + for (const c of "index.t") key(c); + expect(lead()).toBe("index.ts"); + key("Backspace", { altKey: true }); + expect(screen.getByRole("status")).toHaveTextContent(/^index\.$/); + key("Backspace", { metaKey: true }); + expect(screen.queryByRole("status")).toBeNull(); + expect(drop).not.toHaveBeenCalled(); + // Without a query the shortcut reaches the view again. + key("Backspace", { metaKey: true }); + expect(drop).toHaveBeenCalledOnce(); + expect(tree).toHaveFocus(); + } finally { + dispose.forEach((d) => d()); + useKeymap.setState({ overrides: {} }); + } + }); + it("filter mode hides non-matching rows but keeps ancestors", () => { const { tree, key } = setup({ defaultExpanded: ["src", "main"], speedSearchMode: "filter" }); key("ArrowDown"); diff --git a/src/ui/Tree.tsx b/src/ui/Tree.tsx index 99b5bf3a1..2f337523e 100644 --- a/src/ui/Tree.tsx +++ b/src/ui/Tree.tsx @@ -31,6 +31,7 @@ import { acquirePopup } from "./popupState"; import styles from "./Tree.module.css"; import { T } from "./T"; import { setContext } from "@/platform/context"; +import { registerKeyClaim } from "@/platform/keymap"; import { useGivenTx } from "@/platform/i18n"; import { useTreeReorder, type TreeReorder } from "./treeReorder"; import { startPointerDrag, type DragData } from "./pointerDrag"; @@ -367,10 +368,18 @@ export function Tree(props: TreeProps) { pendingScrollToLead.current = null; }, [lead, indexOfKey, virtualizer]); - // Speed search owns Escape while active. + // Speed search owns Escape while active, and ⌫ with any modifier: the query is edited like a + // text field instead of the view's shortcuts running (⌘⌫ is Drop… in the Database view). useEffect(() => { if (!query) return; - return acquirePopup(); + const release = acquirePopup(); + const unclaim = registerKeyClaim( + (e) => e.key === "Backspace" && e.target instanceof Node && !!scrollRef.current?.contains(e.target), + ); + return () => { + release(); + unclaim(); + }; }, [query]); const setExpanded = (row: Row, open: boolean) => { @@ -524,7 +533,8 @@ export function Tree(props: TreeProps) { break; case "Backspace": if (!query) return; - setQuery(query.slice(0, -1)); + // ⌘⌫ erases the query, ⌥⌫ / Ctrl+⌫ its last word, as in a text field. + setQuery(e.metaKey ? "" : e.altKey || e.ctrlKey ? query.replace(/\w*\W*$/, "") : query.slice(0, -1)); break; case "ArrowDown": case "ArrowUp": {