Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"@types/express": "^5.0.5",
"@types/pidusage": "^2.0.5",
"@vercel/blob": "^2.0.0",
"@vscode/ripgrep": "^1.15.9",
"@xterm/addon-clipboard": "0.3.0-beta.195",
"@xterm/addon-fit": "0.12.0-beta.195",
"@xterm/addon-image": "0.10.0-beta.195",
Expand Down Expand Up @@ -192,6 +193,7 @@
"highlight.js": "^11.11.1",
"html-to-image": "^1.11.13",
"http-proxy": "^1.18.1",
"https-proxy-agent": "^7.0.2",
"idb-keyval": "^6.2.2",
"jose": "^6.1.3",
"js-yaml": "^4.1.1",
Expand All @@ -213,6 +215,7 @@
"posthog-js": "1.310.1",
"posthog-node": "^5.24.7",
"prebuild-install": "^7.1.1",
"proxy-from-env": "^1.1.0",
"pyright": "^1.1.408",
"react": "19.2.0",
"react-dnd": "^16.0.1",
Expand Down Expand Up @@ -254,6 +257,7 @@
"vscode-languageserver-types": "3.17.3",
"ws": "^8.18.0",
"yaml-language-server": "^1.21.0",
"yauzl": "^2.9.2",
"zod": "^4.3.5",
"zustand": "^5.0.8"
},
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/runtime-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ const externalizedRuntimeModules: ExternalizedRuntimeModule[] = [
],
asarUnpackGlobs: ["**/node_modules/@libsql/**/*"],
},
{
// Ships the ripgrep binary with the app so VSCode-style .gitignore-aware
// Quick Open / Files tab search works for every user regardless of
// whether they have rg on their system PATH.
specifier: "@vscode/ripgrep",
materialize: ["@vscode/ripgrep"],
packagedCopies: [copyWholeModule("@vscode/ripgrep")],
asarUnpackGlobs: ["**/node_modules/@vscode/ripgrep/**/*"],
},
];

const packagedSupportModules = [
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/src/lib/trpc/routers/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ export const createFilesystemRouter = () => {
includePattern: z.string().optional(),
excludePattern: z.string().optional(),
limit: z.number().optional(),
openFilePaths: z.array(z.string()).optional(),
recentFilePaths: z.array(z.string()).optional(),
scopeId: z.string().optional(),
}),
)
.query(async ({ input }) => {
Expand All @@ -344,6 +347,25 @@ export const createFilesystemRouter = () => {
includePattern: input.includePattern,
excludePattern: input.excludePattern,
limit: input.limit,
openFilePaths: input.openFilePaths,
recentFilePaths: input.recentFilePaths,
scopeId: input.scopeId,
});
});
}),

warmupSearchIndex: publicProcedure
.input(
z.object({
workspaceId: z.string(),
includeHidden: z.boolean().optional(),
}),
)
.mutation(async ({ input }) => {
return await withFilesystemErrorBoundary(async () => {
const service = getServiceForWorkspace(input.workspaceId);
return await service.warmupSearchIndex({
includeHidden: input.includeHidden,
});
});
}),
Expand Down
24 changes: 22 additions & 2 deletions apps/desktop/src/lib/trpc/routers/workspace-fs-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { execFile } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";
import {
createFsHostService,
type FsHostService,
Expand All @@ -7,13 +9,28 @@ import {
type WorkspaceFsPathError,
} from "@superset/workspace-fs/host";
import { TRPCError } from "@trpc/server";
import { rgPath as bundledRgPath } from "@vscode/ripgrep";
import { shell } from "electron";
import { getWorkspace } from "./workspaces/utils/db-helpers";
import { execWithShellEnv } from "./workspaces/utils/shell-env";
import { getWorkspacePath } from "./workspaces/utils/worktree";

const execFileAsync = promisify(execFile);

const filesystemWatcherManager = new FsWatcherManager();

// electron-builder packs node_modules into app.asar, but native binaries can't
// execute from inside asar. We unpack @vscode/ripgrep via `asarUnpack` in
// electron-builder.ts, and at runtime we rewrite the path from the asar view
// to the asar.unpacked view so `execFile` can invoke it.
const rgExecutablePath = bundledRgPath.includes(
`${path.sep}app.asar${path.sep}`,
)
? bundledRgPath.replace(
`${path.sep}app.asar${path.sep}`,
`${path.sep}app.asar.unpacked${path.sep}`,
)
: bundledRgPath;

const sharedHostServiceOptions = {
trashItem: async (absolutePath: string) => {
await shell.trashItem(absolutePath);
Expand All @@ -22,7 +39,10 @@ const sharedHostServiceOptions = {
args: string[],
options: { cwd: string; maxBuffer: number; signal?: AbortSignal },
) => {
const result = await execWithShellEnv("rg", args, {
// Shipping our own ripgrep (via @vscode/ripgrep) means users don't
// have to `brew install ripgrep` to get .gitignore-aware search.
// Matches VSCode's approach.
const result = await execFileAsync(rgExecutablePath, args, {
cwd: options.cwd,
maxBuffer: options.maxBuffer,
windowsHide: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ interface UseWorkspaceFileSearchParams {
workspaceId: string;
searchTerm: string;
limit?: number;
includePattern?: string;
excludePattern?: string;
openFilePaths?: string[];
recentFilePaths?: string[];
}

export function useWorkspaceFileSearch({
workspaceId,
searchTerm,
limit = SEARCH_RESULT_LIMIT,
includePattern = "",
excludePattern = "",
openFilePaths,
recentFilePaths,
}: UseWorkspaceFileSearchParams) {
const trimmedQuery = searchTerm.trim();
const debouncedQuery = useDebouncedValue(trimmedQuery, 150);
Expand All @@ -24,6 +32,10 @@ export function useWorkspaceFileSearch({
workspaceId,
query: debouncedQuery,
limit,
includePattern,
excludePattern,
openFilePaths,
recentFilePaths,
},
{
enabled: debouncedQuery.length > 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,12 +489,23 @@ function WorkspaceContent({
[store],
);

const openFilePathsList = useMemo(
() => Array.from(openFilePaths),
[openFilePaths],
);
const recentFilePathsList = useMemo(
() => recentFiles.map((file) => file.absolutePath),
[recentFiles],
);

// FORK NOTE: fork uses the richer `useCommandPalette` (supports filters,
// cross-workspace open, scope switching) instead of upstream's simple
// boolean-state palette.
const commandPalette = useCommandPalette({
workspaceId,
navigate,
openFilePaths: openFilePathsList,
recentFilePaths: recentFilePathsList,
onSelectFile: ({ close, filePath, targetWorkspaceId }) => {
close();
if (targetWorkspaceId !== workspaceId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,25 @@ interface UseCommandPaletteParams {
close: () => void;
navigate: UseNavigateResult<string>;
}) => void;
/**
* Absolute paths currently open in the editor. Forwarded to `searchFiles`
* so VSCode-style Quick Open boosts them above unrelated hits.
*/
openFilePaths?: readonly string[];
/**
* Absolute paths ordered most-recent-first. Forwarded to `searchFiles` for
* MRU boosting and tiebreaking.
*/
recentFilePaths?: readonly string[];
}

export function useCommandPalette({
workspaceId,
navigate,
enabled = true,
onSelectFile,
openFilePaths,
recentFilePaths,
}: UseCommandPaletteParams) {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
Expand Down Expand Up @@ -64,6 +76,20 @@ export function useCommandPalette({
},
);

// Kick off a background index build whenever Cmd+P is opened. The backend
// deduplicates concurrent builds via its in-flight Promise cache and the
// TTL (30s), so firing this on every open is cheap and keeps results
// instant even after the cache expires between uses.
const warmupMutation =
electronTrpc.filesystem.warmupSearchIndex.useMutation();
const warmupMutate = warmupMutation.mutate;
useEffect(() => {
if (!open || !workspaceId) {
return;
}
warmupMutate({ workspaceId });
}, [open, workspaceId, warmupMutate]);

// Build roots array for multi-workspace search
const roots = useMemo(() => {
if (scope !== "global" || !allGrouped) return [];
Expand Down Expand Up @@ -103,16 +129,47 @@ export function useCommandPalette({
return result;
}, [scope, allGrouped]);

// Stabilize identity of the MRU/open arrays across renders. Joining into a
// single sentinel string is cheap and means React Query only re-fetches
// when the actual set of paths changes, not on every parent render.
const openFilePathsKey = useMemo(
() => (openFilePaths ? openFilePaths.join("\u0000") : ""),
[openFilePaths],
);
const recentFilePathsKey = useMemo(
() => (recentFilePaths ? recentFilePaths.join("\u0000") : ""),
[recentFilePaths],
);
const openFilePathsList = useMemo(
() =>
openFilePathsKey.length > 0
? openFilePathsKey.split("\u0000")
: undefined,
[openFilePathsKey],
);
const recentFilePathsList = useMemo(
() =>
recentFilePathsKey.length > 0
? recentFilePathsKey.split("\u0000")
: undefined,
[recentFilePathsKey],
);

// Single-workspace search (existing behavior)
const singleSearch = useFileSearch({
workspaceId: open && scope === "workspace" ? workspaceId : undefined,
searchTerm: query,
includePattern,
excludePattern,
limit: SEARCH_LIMIT,
openFilePaths: openFilePathsList,
recentFilePaths: recentFilePathsList,
scopeId: "quick-open",
});

// Multi-workspace search
// Multi-workspace search. Note that MRU/open boosts aren't forwarded here
// because the recency lists are scoped to the current workspace; applying
// them across other workspaces would mis-rank unrelated paths.
const debouncedQuery = useDebouncedValue(query.trim(), 150);
const multiSearchQueries = electronTrpc.useQueries((t) =>
open && scope === "global" && roots.length > 0 && debouncedQuery.length > 0
Expand All @@ -123,6 +180,7 @@ export function useCommandPalette({
includePattern,
excludePattern,
limit: SEARCH_LIMIT,
scopeId: "quick-open-global",
}),
)
: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button } from "@superset/ui/button";
import { Input } from "@superset/ui/input";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback } from "react";
import {
LuChevronsDownUp,
LuFilePlus,
Expand All @@ -11,7 +11,6 @@ import {
LuX,
} from "react-icons/lu";
import { useFileExplorerStore } from "renderer/stores/file-explorer";
import { SEARCH_DEBOUNCE_MS } from "../../constants";

interface FileTreeToolbarProps {
searchTerm: string;
Expand All @@ -33,48 +32,18 @@ export function FileTreeToolbar({
isRefreshing = false,
}: FileTreeToolbarProps) {
const { showFileTooltips, toggleFileTooltips } = useFileExplorerStore();
const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm);
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
debounceTimeoutRef.current = null;
}
setLocalSearchTerm(searchTerm);
}, [searchTerm]);

useEffect(() => {
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
};
}, []);

// Debounce lives entirely in `useFileSearch` so the input stays responsive
// and we avoid the two-layer debounce chain that previously delayed renders
// unpredictably depending on stale closures.
const handleSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setLocalSearchTerm(value);

if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}

debounceTimeoutRef.current = setTimeout(() => {
onSearchChange(value);
debounceTimeoutRef.current = null;
}, SEARCH_DEBOUNCE_MS);
onSearchChange(e.target.value);
},
[onSearchChange],
);

const handleClearSearch = useCallback(() => {
setLocalSearchTerm("");
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
debounceTimeoutRef.current = null;
}
onSearchChange("");
}, [onSearchChange]);

Expand All @@ -84,11 +53,11 @@ export function FileTreeToolbar({
<Input
type="text"
placeholder="Search files..."
value={localSearchTerm}
value={searchTerm}
onChange={handleSearchChange}
className="h-7 text-xs pr-7"
/>
{localSearchTerm && (
{searchTerm && (
<button
type="button"
onClick={handleClearSearch}
Expand Down
Loading
Loading