дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(search): scope Find in Files to the focused folder (#534)
Find in Files from the keyboard ignored the selected folder and reused the saved scope; only the context menu passed a directory. Focused trees now publish the selected folder, or a file's parent, as an action context, and Find and Replace in Files use it when no explicit directory is given. This covers the Project view, Commit and Changes, Bookmarks and Scratches. Group rows and editor focus keep the previous behaviour. Fixes #526
Цей коміт міститься в:
1 джерело
a553dafa7a
коміт
c5cb217c7c
11 files changed
+352
-9
No files matched your search
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
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";
|
||||
|
||||
interface N {
|
||||
id: string;
|
||||
@@ -45,6 +46,97 @@ const labels = (tree: HTMLElement) =>
|
||||
.queryAllByRole("treeitem")
|
||||
.map((el) => el.textContent);
|
||||
|
||||
describe("Tree directory context", () => {
|
||||
it("does not contribute virtual document URIs as filesystem directories", () => {
|
||||
const { key } = setup({ getDirectory: () => "jdt://contents/library.jar/package" });
|
||||
key("Home");
|
||||
expect(getContext().focusedDirectory).toBeUndefined();
|
||||
});
|
||||
|
||||
it("follows the selected lead, clears on non-path rows, blur and unmount", () => {
|
||||
const { tree, key, unmount } = setup({
|
||||
getDirectory: (n) => (n.id === "main" ? null : `/p/${n.id}`),
|
||||
});
|
||||
expect(getContext().focusedDirectory).toBeUndefined();
|
||||
key("ArrowDown");
|
||||
expect(getContext().focusedDirectory).toBe("/p/src");
|
||||
key("End", { shiftKey: true });
|
||||
expect(getContext().focusedDirectory).toBe("/p/README.md");
|
||||
key("Home");
|
||||
key("ArrowRight");
|
||||
key("ArrowRight");
|
||||
expect(getContext().focusedDirectory).toBeUndefined();
|
||||
key("Home");
|
||||
fireEvent.blur(tree);
|
||||
expect(getContext().focusedDirectory).toBeUndefined();
|
||||
fireEvent.focus(tree);
|
||||
expect(getContext().focusedDirectory).toBe("/p/src");
|
||||
unmount();
|
||||
expect(getContext().focusedDirectory).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates a controlled selection while focused and clears a removed selection", () => {
|
||||
const view = (selected: Set<string>, roots = data) => (
|
||||
<Tree<N>
|
||||
roots={roots}
|
||||
getKey={(n) => n.id}
|
||||
getLabel={(n) => n.id}
|
||||
getChildren={(n) => n.children}
|
||||
isExpandable={(n) => !!n.children}
|
||||
selected={selected}
|
||||
getDirectory={(n) => `/p/${n.id}`}
|
||||
/>
|
||||
);
|
||||
const { rerender } = render(view(new Set(["src"])));
|
||||
screen.getByRole("tree").focus();
|
||||
expect(getContext().focusedDirectory).toBe("/p/src");
|
||||
rerender(view(new Set()));
|
||||
expect(getContext().focusedDirectory).toBeUndefined();
|
||||
rerender(view(new Set(["src"])));
|
||||
expect(getContext().focusedDirectory).toBe("/p/src");
|
||||
rerender(view(new Set(["src"]), []));
|
||||
expect(getContext().focusedDirectory).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears on focus transfer and an old tree's unmount does not clear the new owner", () => {
|
||||
const view = (showFirst: boolean) => (
|
||||
<>
|
||||
{showFirst && (
|
||||
<Tree<N>
|
||||
roots={data}
|
||||
getKey={(n) => n.id}
|
||||
getLabel={(n) => n.id}
|
||||
getChildren={() => []}
|
||||
isExpandable={() => false}
|
||||
defaultSelected={["src"]}
|
||||
getDirectory={() => "/first"}
|
||||
/>
|
||||
)}
|
||||
<Tree<N>
|
||||
roots={data}
|
||||
getKey={(n) => n.id}
|
||||
getLabel={(n) => n.id}
|
||||
getChildren={() => []}
|
||||
isExpandable={() => false}
|
||||
defaultSelected={["src"]}
|
||||
getDirectory={() => "/second"}
|
||||
/>
|
||||
<input aria-label="Editor" />
|
||||
</>
|
||||
);
|
||||
const { rerender } = render(view(true));
|
||||
const [first, second] = screen.getAllByRole("tree");
|
||||
first!.focus();
|
||||
expect(getContext().focusedDirectory).toBe("/first");
|
||||
second!.focus();
|
||||
expect(getContext().focusedDirectory).toBe("/second");
|
||||
rerender(view(false));
|
||||
expect(getContext().focusedDirectory).toBe("/second");
|
||||
screen.getByLabelText("Editor").focus();
|
||||
expect(getContext().focusedDirectory).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tree keyboard navigation", () => {
|
||||
it("moves with arrows, expands with Right and collapses / goes to parent with Left", () => {
|
||||
const { tree, lead, key } = setup();
|
||||
|
||||
@@ -32,6 +32,7 @@ import styles from "./Tree.module.css";
|
||||
import { T } from "./T";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { useTreeReorder, type TreeReorder } from "./treeReorder";
|
||||
import { useTreeDirectoryContext } from "./treeDirectoryContext";
|
||||
export type { TreeReorder } from "./treeReorder";
|
||||
|
||||
export interface TreeRowState {
|
||||
@@ -85,6 +86,8 @@ export interface TreeProps<T> {
|
||||
getDetail?: (node: T) => string | undefined;
|
||||
/** Label colour, e.g. VCS status. */
|
||||
getColor?: (node: T) => string | undefined;
|
||||
/** Filesystem directory for actions on the focused selection; groups return null. */
|
||||
getDirectory?: (node: T) => string | null | undefined;
|
||||
|
||||
expanded?: ReadonlySet<string>;
|
||||
defaultExpanded?: Iterable<string>;
|
||||
@@ -540,6 +543,11 @@ export function Tree<T>(props: TreeProps<T>) {
|
||||
};
|
||||
|
||||
const leadRow = leadIndex >= 0 ? rows[leadIndex] : undefined;
|
||||
useTreeDirectoryContext(
|
||||
scrollRef,
|
||||
leadRow && !leadRow.loading && selected.has(leadRow.key) ? props.getDirectory?.(leadRow.node) : undefined,
|
||||
!!props.getDirectory && !emptyState,
|
||||
);
|
||||
const noMatch = !!query && matchMap.size === 0;
|
||||
|
||||
if (emptyState) return <div className={clsx(styles.tree, styles.emptyWrap, className)}>{empty}</div>;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useLayoutEffect, type RefObject } from "react";
|
||||
import { setContext } from "@/platform/context";
|
||||
import { isVirtualUri } from "@/platform/virtualDocs";
|
||||
|
||||
let owner: HTMLElement | null = null;
|
||||
|
||||
/** Only the focused tree contributes its selected directory to actions. */
|
||||
export function useTreeDirectoryContext(
|
||||
ref: RefObject<HTMLElement | null>,
|
||||
directory: string | null | undefined,
|
||||
enabled: boolean,
|
||||
) {
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled) return;
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
const clear = () => {
|
||||
if (owner !== element) return;
|
||||
owner = null;
|
||||
setContext("focusedDirectory", undefined);
|
||||
};
|
||||
const publish = () => {
|
||||
if (document.activeElement !== element) return;
|
||||
owner = element;
|
||||
setContext("focusedDirectory", directory && !isVirtualUri(directory) ? directory : undefined);
|
||||
};
|
||||
element.addEventListener("focus", publish);
|
||||
element.addEventListener("blur", clear);
|
||||
publish();
|
||||
return () => {
|
||||
element.removeEventListener("focus", publish);
|
||||
element.removeEventListener("blur", clear);
|
||||
clear();
|
||||
};
|
||||
}, [ref, directory, enabled]);
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача