Skip to content

Commit d6d8b11

Browse files
committed
feat: add file search functionality with progress tracking
- Implemented a new file search feature in the dotdir-core library, allowing users to search for files based on various criteria such as name patterns and content patterns. - Introduced a `FileSearchRequest` struct to encapsulate search parameters and a `FileSearchEvent` enum to handle different search events (matches, completion, cancellation, and errors). - Added a new `search_files` function to perform the search operation, utilizing glob patterns and regex for matching. - Integrated the search functionality into the Tauri backend, allowing for starting and cancelling searches via commands. - Enhanced the frontend bridge to support search operations, including progress updates and event handling. - Updated the UI components to display search results and handle user interactions related to searching. - Added necessary dependencies for glob and regex handling in the Rust codebase.
1 parent 836fe1d commit d6d8b11

34 files changed

Lines changed: 1697 additions & 105 deletions

TERMINAL_ROADMAP.md

Lines changed: 0 additions & 80 deletions
This file was deleted.

packages/ui/lib/DotDir.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export type {
2525
DeleteProgressEvent,
2626
ExtensionInstallProgressEvent,
2727
ExtensionInstallRequest,
28+
FileSearchMatch,
29+
FileSearchProgressEvent,
30+
FileSearchRequest,
2831
FsChangeEvent,
2932
FsChangeType,
3033
FsEntry,

packages/ui/lib/app.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type AppHandle = {
2929
};
3030

3131
const THEME_STARTUP_TIMEOUT_MS = 5_000;
32+
const WINDOW_SHOW_TIMEOUT_MS = 5_000;
3233

3334
export const App = forwardRef<AppHandle, { widget: React.ReactNode }>(function App({ widget }, ref) {
3435
const commandRegistry = useCommandRegistry();
@@ -54,6 +55,7 @@ export const App = forwardRef<AppHandle, { widget: React.ReactNode }>(function A
5455
const windowShownRef = useRef(false);
5556
const initialThemeRefreshDoneRef = useRef(false);
5657
const [themeStartupTimedOut, setThemeStartupTimedOut] = useState(false);
58+
const [windowShowTimedOut, setWindowShowTimedOut] = useState(false);
5759

5860
const { uiStateLoaded } = useWorkspaceRestoreProcess();
5961
const startupReady = uiStateLoaded && (themesReady || themeStartupTimedOut);
@@ -107,15 +109,29 @@ export const App = forwardRef<AppHandle, { widget: React.ReactNode }>(function A
107109
return () => clearTimeout(timer);
108110
}, [themesReady]);
109111

112+
useEffect(() => {
113+
if (startupReady) {
114+
setWindowShowTimedOut(false);
115+
return;
116+
}
117+
118+
const timer = setTimeout(() => {
119+
console.warn("[startup] Window show timed out after 5000ms; forcing window visibility.");
120+
setWindowShowTimedOut(true);
121+
}, WINDOW_SHOW_TIMEOUT_MS);
122+
123+
return () => clearTimeout(timer);
124+
}, [startupReady]);
125+
110126
useEffect(() => {
111127
if (windowShownRef.current) return;
112-
if (!startupReady) return;
128+
if (!startupReady && !windowShowTimedOut) return;
113129
if (!bridge.window?.showCurrent) return;
114130
windowShownRef.current = true;
115131
void bridge.window.showCurrent().catch(() => {
116132
// Ignore show failures so startup can proceed.
117133
});
118-
}, [bridge.window, startupReady]);
134+
}, [bridge.window, startupReady, windowShowTimedOut]);
119135

120136
useEffect(() => {
121137
if (!startupReady || !themesReady) return;

packages/ui/lib/dialogs/CopyConfigDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function CopyConfigDialog({ itemCount, destPath, suggestionRoots, onConfi
8888
roots={suggestionRoots}
8989
mode="directories"
9090
inputRef={inputRef}
91-
inputClassName={styles["open-create-file-field"] ? undefined : undefined}
91+
inputClassName={styles["dialog-input"]}
9292
{...INPUT_NO_ASSIST}
9393
/>
9494
</div>
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import type { FileSearchRequest } from "@/features/bridge";
2+
import { INPUT_NO_ASSIST } from "@/utils/inputNoAssist";
3+
import { useMemo, useRef, useState } from "react";
4+
import { PathAutocompleteInput } from "./PathAutocompleteInput";
5+
import { SmartLabel } from "./dialogHotkeys";
6+
import styles from "./dialogs.module.css";
7+
import { OverlayDialog } from "./OverlayDialog";
8+
import { useDialogButtonNav } from "./useDialogButtonNav";
9+
10+
export interface FindFilesDialogProps {
11+
initialRequest: FileSearchRequest;
12+
suggestionRoots: Array<{ id: string; label: string; path: string }>;
13+
onConfirm: (request: FileSearchRequest) => void;
14+
onCancel: () => void;
15+
stackIndex?: number;
16+
}
17+
18+
function formatIgnoreDirs(value: string[]): string {
19+
return value.join("; ");
20+
}
21+
22+
function parseIgnoreDirs(value: string): string[] {
23+
return value
24+
.split(/[;,\n]/)
25+
.map((part) => part.trim())
26+
.filter(Boolean);
27+
}
28+
29+
export function FindFilesDialog({
30+
initialRequest,
31+
suggestionRoots,
32+
onConfirm,
33+
onCancel,
34+
stackIndex = 0,
35+
}: FindFilesDialogProps) {
36+
const buttonsRef = useRef<HTMLDivElement>(null);
37+
const inputRef = useRef<HTMLInputElement>(null);
38+
const { onKeyDown } = useDialogButtonNav(buttonsRef, { defaultIndex: 0 });
39+
40+
const [startPath, setStartPath] = useState(initialRequest.startPath);
41+
const [ignoreDirsEnabled, setIgnoreDirsEnabled] = useState(initialRequest.ignoreDirsEnabled);
42+
const [ignoreDirsText, setIgnoreDirsText] = useState(formatIgnoreDirs(initialRequest.ignoreDirs));
43+
const [filePattern, setFilePattern] = useState(initialRequest.filePattern);
44+
const [contentPattern, setContentPattern] = useState(initialRequest.contentPattern);
45+
const [recursive, setRecursive] = useState(initialRequest.recursive);
46+
const [followSymlinks, setFollowSymlinks] = useState(initialRequest.followSymlinks);
47+
const [shellPatterns, setShellPatterns] = useState(initialRequest.shellPatterns);
48+
const [caseSensitiveFileName, setCaseSensitiveFileName] = useState(initialRequest.caseSensitiveFileName);
49+
const [wholeWords, setWholeWords] = useState(initialRequest.wholeWords);
50+
const [regex, setRegex] = useState(initialRequest.regex);
51+
const [caseSensitiveContent, setCaseSensitiveContent] = useState(initialRequest.caseSensitiveContent);
52+
const [allCharsets, setAllCharsets] = useState(initialRequest.allCharsets);
53+
const [firstHit, setFirstHit] = useState(initialRequest.firstHit);
54+
const [skipHidden, setSkipHidden] = useState(initialRequest.skipHidden);
55+
56+
const canSubmit = useMemo(() => startPath.trim().length > 0, [startPath]);
57+
58+
const handleSubmit = (event: React.FormEvent) => {
59+
event.preventDefault();
60+
if (!canSubmit) return;
61+
onConfirm({
62+
startPath: startPath.trim(),
63+
ignoreDirsEnabled,
64+
ignoreDirs: ignoreDirsEnabled ? parseIgnoreDirs(ignoreDirsText) : [],
65+
filePattern: filePattern.trim() || "*",
66+
contentPattern: contentPattern.trim(),
67+
recursive,
68+
followSymlinks,
69+
shellPatterns,
70+
caseSensitiveFileName,
71+
wholeWords,
72+
regex,
73+
caseSensitiveContent,
74+
allCharsets,
75+
firstHit,
76+
skipHidden,
77+
});
78+
};
79+
80+
return (
81+
<OverlayDialog className={styles["find-files-dialog"]} onClose={onCancel} onKeyDown={onKeyDown} initialFocusRef={inputRef} stackIndex={stackIndex}>
82+
<div className={styles["modal-dialog-header"]}>Find File</div>
83+
<form className={styles["modal-dialog-form"]} onSubmit={handleSubmit}>
84+
<div className={styles["modal-dialog-body"]}>
85+
<div className={styles["find-files-start-row"]}>
86+
<div className={styles["find-files-field"]}>
87+
<label htmlFor="find-files-start">
88+
<SmartLabel>Start at</SmartLabel>
89+
</label>
90+
<PathAutocompleteInput
91+
id="find-files-start"
92+
value={startPath}
93+
onChange={setStartPath}
94+
roots={suggestionRoots}
95+
mode="directories"
96+
inputRef={inputRef}
97+
inputClassName={styles["dialog-input"]}
98+
{...INPUT_NO_ASSIST}
99+
/>
100+
</div>
101+
</div>
102+
103+
<div className={styles["find-files-ignore-row"]}>
104+
<label className={styles["find-files-checkbox"]}>
105+
<input
106+
type="checkbox"
107+
checked={ignoreDirsEnabled}
108+
onChange={(event) => setIgnoreDirsEnabled(event.target.checked)}
109+
/>
110+
<SmartLabel>Enable ignore directories</SmartLabel>
111+
</label>
112+
<input
113+
type="text"
114+
value={ignoreDirsText}
115+
onChange={(event) => setIgnoreDirsText(event.target.value)}
116+
disabled={!ignoreDirsEnabled}
117+
placeholder=".git; node_modules; dist"
118+
{...INPUT_NO_ASSIST}
119+
/>
120+
</div>
121+
122+
<div className={styles["find-files-search-grid"]}>
123+
<div className={styles["find-files-field"]}>
124+
<label htmlFor="find-files-name">
125+
<SmartLabel>File name</SmartLabel>
126+
</label>
127+
<input
128+
id="find-files-name"
129+
type="text"
130+
value={filePattern}
131+
onChange={(event) => setFilePattern(event.target.value)}
132+
{...INPUT_NO_ASSIST}
133+
/>
134+
</div>
135+
<div className={styles["find-files-field"]}>
136+
<label htmlFor="find-files-content">
137+
<SmartLabel>Content</SmartLabel>
138+
</label>
139+
<input
140+
id="find-files-content"
141+
type="text"
142+
value={contentPattern}
143+
onChange={(event) => setContentPattern(event.target.value)}
144+
{...INPUT_NO_ASSIST}
145+
/>
146+
</div>
147+
</div>
148+
149+
<div className={styles["find-files-options-grid"]}>
150+
<label className={styles["find-files-checkbox"]}>
151+
<input type="checkbox" checked={recursive} onChange={(event) => setRecursive(event.target.checked)} />
152+
<SmartLabel>Find recursively</SmartLabel>
153+
</label>
154+
<label className={styles["find-files-checkbox"]}>
155+
<input type="checkbox" checked={wholeWords} onChange={(event) => setWholeWords(event.target.checked)} />
156+
<SmartLabel>Whole words</SmartLabel>
157+
</label>
158+
<label className={styles["find-files-checkbox"]}>
159+
<input type="checkbox" checked={followSymlinks} onChange={(event) => setFollowSymlinks(event.target.checked)} />
160+
<SmartLabel>Follow symlinks</SmartLabel>
161+
</label>
162+
<label className={styles["find-files-checkbox"]}>
163+
<input type="checkbox" checked={regex} onChange={(event) => setRegex(event.target.checked)} />
164+
<SmartLabel>Regular expression</SmartLabel>
165+
</label>
166+
<label className={styles["find-files-checkbox"]}>
167+
<input type="checkbox" checked={shellPatterns} onChange={(event) => setShellPatterns(event.target.checked)} />
168+
<SmartLabel>Using shell patterns</SmartLabel>
169+
</label>
170+
<label className={styles["find-files-checkbox"]}>
171+
<input
172+
type="checkbox"
173+
checked={caseSensitiveFileName}
174+
onChange={(event) => setCaseSensitiveFileName(event.target.checked)}
175+
/>
176+
<SmartLabel>Case sensitive name</SmartLabel>
177+
</label>
178+
<label className={styles["find-files-checkbox"]}>
179+
<input
180+
type="checkbox"
181+
checked={caseSensitiveContent}
182+
onChange={(event) => setCaseSensitiveContent(event.target.checked)}
183+
/>
184+
<SmartLabel>Case sensitive content</SmartLabel>
185+
</label>
186+
<label className={styles["find-files-checkbox"]}>
187+
<input type="checkbox" checked={allCharsets} onChange={(event) => setAllCharsets(event.target.checked)} />
188+
<SmartLabel>All charsets</SmartLabel>
189+
</label>
190+
<label className={styles["find-files-checkbox"]}>
191+
<input type="checkbox" checked={firstHit} onChange={(event) => setFirstHit(event.target.checked)} />
192+
<SmartLabel>First hit</SmartLabel>
193+
</label>
194+
<label className={styles["find-files-checkbox"]}>
195+
<input type="checkbox" checked={skipHidden} onChange={(event) => setSkipHidden(event.target.checked)} />
196+
<SmartLabel>Skip hidden</SmartLabel>
197+
</label>
198+
</div>
199+
</div>
200+
<div className={styles["modal-dialog-buttons"]} ref={buttonsRef}>
201+
<button type="submit" disabled={!canSubmit}>
202+
<SmartLabel>OK</SmartLabel>
203+
</button>
204+
<button type="button" onClick={onCancel}>
205+
<SmartLabel>Cancel</SmartLabel>
206+
</button>
207+
</div>
208+
</form>
209+
</OverlayDialog>
210+
);
211+
}

0 commit comments

Comments
 (0)