diff --git a/e2e/editor.spec.ts b/e2e/editor.spec.ts
index 6f6a79d5..9dc7b88f 100644
--- a/e2e/editor.spec.ts
+++ b/e2e/editor.spec.ts
@@ -13,6 +13,35 @@ test("opens a file from the project tree and types into it", async ({ workbench:
await expect(editor.locator(".cm-content")).not.toContainText("// typed by e2e");
});
+test("Delete edits text after the focused Project tree is remounted", async ({ workbench: page }) => {
+ const editor = await openFromTree(page, "README.md");
+ const content = editor.locator(".cm-content");
+ await content.press("Meta+End");
+ await page.keyboard.press("Enter");
+ await page.keyboard.insertText("delete probe abc");
+ const line = editor.locator(".cm-line", { hasText: "delete probe" });
+ await expect(line).toHaveText("delete probe abc");
+
+ const tree = page.getByRole("tree", { name: "Project" });
+ await tree.focus();
+ await page.evaluate(async () => {
+ const keymapModule = "/src/platform/keymap.ts";
+ const layoutModule = "/src/workbench/toolwindows/store.ts";
+ const { applyKeymapSettings } = await import(/* @vite-ignore */ keymapModule);
+ const { moveToolWindow } = await import(/* @vite-ignore */ layoutModule);
+ applyKeymapSettings("intellij-windows", {});
+ moveToolWindow("project", "right");
+ });
+ await expect(page.locator('[data-toolwindow="project"]')).toBeVisible();
+ await expect(tree).not.toBeFocused();
+
+ await content.press("ArrowLeft");
+ await page.keyboard.press("Delete");
+ await expect(line).toHaveText("delete probe ab");
+ await page.keyboard.press("Backspace");
+ await expect(line).toHaveText("delete probe a");
+});
+
test("⌘F opens the find bar and highlights matches; Escape returns to the editor", async ({ workbench: page }) => {
const editor = await openFromTree(page, "build.gradle.kts");
await editor.locator(".cm-content").click();
diff --git a/src/features/database/DatabaseView.tsx b/src/features/database/DatabaseView.tsx
index 36abea84..4a71fabb 100644
--- a/src/features/database/DatabaseView.tsx
+++ b/src/features/database/DatabaseView.tsx
@@ -7,7 +7,6 @@
*/
import { useEffect, useMemo, useRef, type CSSProperties } from "react";
import type { DbDatabases, DbTableDetails } from "@/platform/ipc";
-import { setContext } from "@/platform/context";
import { executeAction } from "@/platform/actions";
import { shortcutLabel } from "@/platform/keymap";
import {
@@ -378,13 +377,10 @@ export function DatabaseView() {
return (
-
setContext(DB_VIEW_FOCUS, (e.target as HTMLElement).getAttribute("role") === "tree")}
- onBlur={() => setContext(DB_VIEW_FOCUS, false)}
- >
+
aria-label="Data sources"
+ focusContext={DB_VIEW_FOCUS}
roots={roots}
handleRef={tree}
expanded={st.treeExpanded}
diff --git a/src/features/project-tree/ProjectView.tsx b/src/features/project-tree/ProjectView.tsx
index 0d796cfa..7ff4a721 100644
--- a/src/features/project-tree/ProjectView.tsx
+++ b/src/features/project-tree/ProjectView.tsx
@@ -20,7 +20,6 @@ import { createPortal } from "react-dom";
import clsx from "clsx";
import { revealRange, useEditorRegistry } from "@/editor/api";
import { executeAction } from "@/platform/actions";
-import { setContext } from "@/platform/context";
import { tx, useTxTemplate } from "@/platform/i18n";
import { isTauri } from "@/platform/ipc";
import { nativeDropPoint } from "@/platform/nativeDrop";
@@ -779,8 +778,6 @@ export function ProjectView() {
onCopy={(e) => onClipboard(e, "project.copy")}
onCut={(e) => onClipboard(e, "project.cut")}
onPaste={(e) => onClipboard(e, "project.paste")}
- onFocus={(e) => setContext("projectViewFocus", (e.target as HTMLElement).getAttribute("role") === "tree")}
- onBlur={() => setContext("projectViewFocus", false)}
>
{mode === "unity" && unity && !unity.projects.length && (
@@ -795,6 +792,7 @@ export function ProjectView() {
horizontalScroll
aria-label="Project"
+ focusContext="projectViewFocus"
handleRef={setTreeHandle}
roots={model.roots}
getKey={getKey}
diff --git a/src/features/services/ServicesTree.tsx b/src/features/services/ServicesTree.tsx
index b259ebf8..83924764 100644
--- a/src/features/services/ServicesTree.tsx
+++ b/src/features/services/ServicesTree.tsx
@@ -5,7 +5,6 @@
*/
import { useEffect, useMemo, useRef } from "react";
import clsx from "clsx";
-import { setContext } from "@/platform/context";
import { useTxTemplate, tx } from "@/platform/i18n";
import { shortcutLabel } from "@/platform/keymap";
import { useProject } from "@/platform/project";
@@ -208,15 +207,10 @@ export function ServicesTreeView() {
) : (
-
- setContext(SERVICES_VIEW_FOCUS, (e.target as HTMLElement).getAttribute("role") === "tree")
- }
- onBlur={() => setContext(SERVICES_VIEW_FOCUS, false)}
- >
+
key={location}
+ focusContext={SERVICES_VIEW_FOCUS}
reorder={reorder}
roots={roots}
getKey={(n) => n.key}
diff --git a/src/ui/Tree.tsx b/src/ui/Tree.tsx
index 38855aef..a89ff4b4 100644
--- a/src/ui/Tree.tsx
+++ b/src/ui/Tree.tsx
@@ -30,6 +30,7 @@ import { fuzzyMatch } from "./match";
import { acquirePopup } from "./popupState";
import styles from "./Tree.module.css";
import { T } from "./T";
+import { setContext } from "@/platform/context";
import { tx } from "@/platform/i18n";
import { useTreeReorder, type TreeReorder } from "./treeReorder";
import { useTreeDirectoryContext } from "./treeDirectoryContext";
@@ -116,6 +117,8 @@ export interface TreeProps {
rowClassName?: (node: T) => string | undefined;
"aria-label"?: string;
handleRef?: Ref;
+ /** Context key set while the tree itself has keyboard focus. */
+ focusContext?: string;
empty?: ReactNode;
/** Rows wider than the pane scroll horizontally instead of clipping. */
horizontalScroll?: boolean;
@@ -592,6 +595,31 @@ export function Tree(props: TreeProps) {
);
const noMatch = !!query && matchMap.size === 0;
+ const { focusContext } = props;
+ useLayoutEffect(() => {
+ const element = scrollRef.current;
+ if (!focusContext || !element) return;
+ let focused = false;
+ const focus = () => {
+ focused = true;
+ setContext(focusContext, true);
+ };
+ const blur = () => {
+ if (!focused) return;
+ focused = false;
+ setContext(focusContext, false);
+ };
+ element.addEventListener("focus", focus);
+ element.addEventListener("blur", blur);
+ if (element.ownerDocument.activeElement === element) focus();
+ return () => {
+ element.removeEventListener("focus", focus);
+ element.removeEventListener("blur", blur);
+ // Removing a focused tree can skip blur, leaving its shortcuts active.
+ blur();
+ };
+ }, [emptyState, focusContext]);
+
if (emptyState) return {empty}
;
return (