diff --git a/src/features/search/__tests__/findPopup.test.tsx b/src/features/search/__tests__/findPopup.test.tsx index 9d10f9f6..e2928ccc 100644 --- a/src/features/search/__tests__/findPopup.test.tsx +++ b/src/features/search/__tests__/findPopup.test.tsx @@ -55,6 +55,13 @@ vi.mock("@/workbench/messages", async (orig) => ({ }, })); +const dialogOpen = vi.fn(); +vi.mock("@tauri-apps/plugin-dialog", () => ({ open: (o: unknown) => dialogOpen(o) })); +vi.mock("@/platform/ipc", async (orig) => ({ + ...(await orig()), + isTauri: () => true, +})); + const { FindInFilesOverlay } = await import("../find/FindInFiles"); const { findHistory, hideFindPopup, rememberFindString, runFindPopupCommand, showFindPopup, useFindPopup } = await import("../find/store"); @@ -176,6 +183,18 @@ describe("Find in Files popup", () => { act(() => hideFindPopup()); }); + it("the Directory scope's browse button picks the folder, starting at the current one", async () => { + dialogOpen.mockResolvedValue("/p/src/main"); + useFindPopup.setState({ scopeTab: "directory", directory: "/p/src" }); + render(); + act(() => showFindPopup(false, { query: "order" })); + await settle(); + fireEvent.click(screen.getByRole("button", { name: "Browse…" })); + await settle(); + expect(dialogOpen).toHaveBeenCalledWith(expect.objectContaining({ directory: true, defaultPath: "/p/src" })); + expect(useFindPopup.getState().directory).toBe("/p/src/main"); + }); + it("closing remembers the search text", async () => { render(); act(() => showFindPopup(false, { query: "order" })); diff --git a/src/features/search/find/FindInFiles.tsx b/src/features/search/find/FindInFiles.tsx index f265465f..7d3687ef 100644 --- a/src/features/search/find/FindInFiles.tsx +++ b/src/features/search/find/FindInFiles.tsx @@ -4,7 +4,7 @@ import { openEditorPaths, useEditorRegistry } from "@/editor/api"; import { executeAction } from "@/platform/actions"; import { setContext } from "@/platform/context"; import { useTxTemplate, tx } from "@/platform/i18n"; -import { call, commands, type SearchScope } from "@/platform/ipc"; +import { call, commands, isTauri, type SearchScope } from "@/platform/ipc"; import { useProject } from "@/platform/project"; import { useProjectModules } from "@/platform/projectRoots"; import { EDIT_SCOPES_ACTION, useCustomScopeList } from "@/platform/search"; @@ -60,6 +60,7 @@ import { recentMasks, rememberFindString, rememberMask, + resolveDirectory, resolveFindScope, setFindPopupCommandHandler, setFindPopupPinned, @@ -73,6 +74,19 @@ import { import { ipcFindDeps, openInFindWindow } from "./windowStore"; import styles from "./FindInFiles.module.css"; +/** The Directory scope's browse button: a folder chooser opened at the current directory. */ +async function chooseDirectory(current: string): Promise { + if (!isTauri()) return null; + const { open } = await import("@tauri-apps/plugin-dialog"); + const picked = await open({ + directory: true, + multiple: false, + title: tx("Select Search Directory"), + defaultPath: current || undefined, + }); + return (Array.isArray(picked) ? picked[0] : picked) ?? null; +} + const POPUP_MAX_MATCHES = 2000; const SEARCH_DEBOUNCE_MS = 150; const ROW = 22; @@ -631,6 +645,17 @@ function FindInFilesDialog() { onChange={(e) => set({ directory: e.target.value })} /> )} + {s.scopeTab === "directory" && ( + + void chooseDirectory(resolveDirectory(s.directory, root)).then( + (dir) => dir && set({ directory: dir }), + ) + } + /> + )} {s.scopeTab === "module" && ( value={s.module} diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index cafd9a14..cc26f3b3 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -1956,6 +1956,7 @@ "Select JDK Home Directory": "选择 JDK 主目录", "Select Local Repository": "选择本地仓库", "Select Maven Home Directory": "选择 Maven 主目录", + "Select Search Directory": "选择查找目录", "Select Maven settings.xml": "选择 Maven settings.xml", "Select Next Tab": "选择下一个标签页", "Select Opened File": "选择已打开的文件",