@@ -28,6 +28,27 @@ struct TreePanelRow {
2828struct NeoTreePanelEvent {
2929 action: String,
3030 row: Option<TreePanelRow>,
31+ text: Option<String>,
32+ }
33+
34+ struct TreeSearchMatch {
35+ path: String,
36+ ranges: [[i32]],
37+ }
38+
39+ struct TreeSearchResult {
40+ query: String,
41+ directories_only: bool,
42+ children: [Children],
43+ expanded: [String],
44+ matches: [TreeSearchMatch],
45+ total: i32,
46+ truncated: bool,
47+ error: Option<String>,
48+ }
49+
50+ struct TreeSearchTimer {
51+ timer_id: String,
3152}
3253
3354struct RestoredPane {
@@ -179,6 +200,14 @@ struct NeoTreeState {
179200 reveal_parts: [String],
180201 reveal_index: i32,
181202 reveal_parent: String,
203+ search_query: String,
204+ search_mode: String,
205+ search_timer: String,
206+ search_children: [Children],
207+ search_expanded: [String],
208+ search_matches: [TreeSearchMatch],
209+ search_saved_expanded: [String],
210+ search_persistent: bool,
182211}
183212
184213#[red::state]
@@ -212,6 +241,14 @@ fn initial_state() -> NeoTreeState {
212241 reveal_parts: [],
213242 reveal_index: 0,
214243 reveal_parent: ".",
244+ search_query: "",
245+ search_mode: "",
246+ search_timer: "",
247+ search_children: [],
248+ search_expanded: ["."],
249+ search_matches: [],
250+ search_saved_expanded: [],
251+ search_persistent: false,
215252 };
216253}
217254
@@ -288,10 +325,15 @@ fn close() {
288325 return;
289326 }
290327
328+ cancel_tree_search_timer();
291329 for watch in state().watches {
292330 red::execute("UnwatchDirectory", watch.id);
293331 }
294332 red::state_patch(NeoTreeState { watches: [] });
333+ red::state_patch(NeoTreeState { search_mode: "" });
334+ red::state_patch(NeoTreeState { search_query: "" });
335+ red::state_patch(NeoTreeState { search_children: [] });
336+ red::state_patch(NeoTreeState { search_matches: [] });
295337 red::execute("ClosePanel", "neotree");
296338 red::execute("FocusEditor");
297339 red::state_patch(NeoTreeState { created: false });
@@ -318,7 +360,52 @@ fn panel_event(event: NeoTreePanelEvent) {
318360 }
319361
320362 if event.action == "refresh" {
363+ red::execute("InvalidateWorkspacePaths", red::string(state().cwd, "."));
321364 reload_visible_tree();
365+ if state().search_query != "" {
366+ run_tree_search();
367+ }
368+ return;
369+ }
370+
371+ if event.action == "/" || event.action == "D" || event.action == "f" {
372+ begin_tree_search(event.action);
373+ return;
374+ }
375+
376+ if event.action == "Ctrl-x" {
377+ clear_tree_search();
378+ return;
379+ }
380+
381+ if event.action == "search_query" {
382+ tree_search_query(optional_text(event.text, ""));
383+ return;
384+ }
385+
386+ if event.action == "search_cancel" {
387+ clear_tree_search();
388+ return;
389+ }
390+
391+ if event.action == "search_keep" {
392+ red::state_patch(NeoTreeState { search_persistent: true });
393+ red::execute("KeepPanelSearch", "neotree");
394+ return;
395+ }
396+
397+ if event.action == "search_submit" || event.action == "search_reveal" {
398+ if event.action == "search_submit" && state().search_persistent {
399+ red::execute("KeepPanelSearch", "neotree");
400+ return;
401+ }
402+ if let Some(row) = event.row {
403+ finish_tree_search(row, event.action == "search_reveal");
404+ }
405+ return;
406+ }
407+
408+ if event.action == "search_move" {
322409 return;
323410 }
324411
@@ -436,6 +523,131 @@ fn panel_event(event: NeoTreePanelEvent) {
436523 }
437524}
438525
526+ fn begin_tree_search(mode: String) {
527+ if state().search_mode == "" {
528+ red::state_patch(NeoTreeState { search_saved_expanded: state().expanded });
529+ }
530+ red::state_patch(NeoTreeState { search_mode: mode });
531+ red::state_patch(NeoTreeState { search_persistent: mode == "f" });
532+ let prefix = "/";
533+ if mode == "D" { prefix = "D"; }
534+ if mode == "f" { prefix = "f"; }
535+ red::execute("OpenPanelSearch", "neotree", state().search_query, prefix);
536+ if state().search_query != "" {
537+ run_tree_search();
538+ }
539+ }
540+
541+ fn tree_search_query(query: String) {
542+ red::state_patch(NeoTreeState { search_query: query });
543+ cancel_tree_search_timer();
544+ if red::trim(query) == "" {
545+ red::state_patch(NeoTreeState { search_children: [] });
546+ red::state_patch(NeoTreeState { search_matches: [] });
547+ red::execute("UpdatePanelSearch", "neotree", "");
548+ render();
549+ return;
550+ }
551+ red::execute("UpdatePanelSearch", "neotree", "…");
552+ red::state_patch(NeoTreeState { search_timer: red::execute("SetTimeout", 60) });
553+ }
554+
555+ #[red::on("timeout:callback")]
556+ fn tree_search_timeout(event: TreeSearchTimer) {
557+ if event.timer_id != state().search_timer {
558+ return;
559+ }
560+ red::state_patch(NeoTreeState { search_timer: "" });
561+ run_tree_search();
562+ }
563+
564+ fn run_tree_search() {
565+ let query = red::trim(state().search_query);
566+ if query == "" {
567+ return;
568+ }
569+ red::request(
570+ "SearchWorkspacePaths",
571+ tree_search_loaded,
572+ red::string(state().cwd, "."),
573+ query,
574+ state().search_mode == "D"
575+ );
576+ }
577+
578+ fn tree_search_loaded(result: TreeSearchResult) {
579+ if state().search_mode == ""
580+ || result.query != red::trim(state().search_query)
581+ || result.directories_only != (state().search_mode == "D") {
582+ return;
583+ }
584+ if let Some(error) = result.error {
585+ red::execute("UpdatePanelSearch", "neotree", "error");
586+ red::execute("Print", "Neo-tree search failed: " + error);
587+ return;
588+ }
589+ red::state_patch(NeoTreeState { search_children: result.children });
590+ red::state_patch(NeoTreeState { search_expanded: result.expanded });
591+ red::state_patch(NeoTreeState { search_matches: result.matches });
592+ let count = "" + result.total;
593+ if result.truncated { count = count + "+"; }
594+ red::execute("UpdatePanelSearch", "neotree", count);
595+ render();
596+ if red::len(result.matches) > 0 {
597+ red::execute("SelectPanelRow", "neotree", result.matches[0].path);
598+ }
599+ }
600+
601+ fn finish_tree_search(row: TreePanelRow, reveal_only: bool) {
602+ if state().search_persistent && !reveal_only {
603+ red::execute("KeepPanelSearch", "neotree");
604+ if row.kind == "directory" {
605+ return;
606+ }
607+ }
608+ if row.kind == "file" && !reveal_only {
609+ red::execute("OpenLocation", Location { path: row.path, line: 0, column: 0 });
610+ close();
611+ return;
612+ }
613+ // Keep the synthetic ancestry available even when the selected child falls
614+ // beyond a normal 160-entry directory snapshot.
615+ for listing in state().search_children {
616+ set_children(listing.path, listing.entries, false);
617+ }
618+ red::state_patch(NeoTreeState { expanded: state().search_expanded });
619+ cancel_tree_search_timer();
620+ red::state_patch(NeoTreeState { search_mode: "" });
621+ red::state_patch(NeoTreeState { search_query: "" });
622+ red::state_patch(NeoTreeState { search_children: [] });
623+ red::state_patch(NeoTreeState { search_matches: [] });
624+ red::execute("ClosePanelSearch", "neotree");
625+ render();
626+ red::execute("SelectPanelRow", "neotree", row.path);
627+ }
628+
629+ fn clear_tree_search() {
630+ cancel_tree_search_timer();
631+ if state().search_mode != "" {
632+ red::state_patch(NeoTreeState { expanded: state().search_saved_expanded });
633+ }
634+ red::state_patch(NeoTreeState { search_mode: "" });
635+ red::state_patch(NeoTreeState { search_query: "" });
636+ red::state_patch(NeoTreeState { search_children: [] });
637+ red::state_patch(NeoTreeState { search_matches: [] });
638+ red::state_patch(NeoTreeState { search_saved_expanded: [] });
639+ red::state_patch(NeoTreeState { search_persistent: false });
640+ red::execute("ClosePanelSearch", "neotree");
641+ render();
642+ }
643+
644+ fn cancel_tree_search_timer() {
645+ if state().search_timer != "" {
646+ red::execute("CancelTimeout", state().search_timer);
647+ red::state_patch(NeoTreeState { search_timer: "" });
648+ }
649+ }
650+
439651fn open_create_prompt(row: TreePanelRow, directory: bool) {
440652 let parent = row.path;
441653 if row.kind != "directory" {
@@ -730,6 +942,7 @@ fn stat_loaded(result: FileStatResult) {
730942
731943fn show_help() {
732944 red::execute("OpenPicker", "Neo-tree keys", [
945+ PickerItem { id: "search", label: "/ / D / f / Ctrl-x search / directories / filter / clear", kind: "Search" },
733946 PickerItem { id: "select", label: "Tab toggle selection", kind: "Selection" },
734947 PickerItem { id: "create", label: "a / A create file / directory", kind: "Create" },
735948 PickerItem { id: "rename", label: "r / b rename / rename basename", kind: "Rename" },
@@ -820,8 +1033,12 @@ fn git_status_loaded(result: GitStatusResult) {
8201033}
8211034
8221035fn directory_changed(event: DirectoryChangedEvent) {
1036+ red::execute("InvalidateWorkspacePaths", red::string(state().cwd, "."));
8231037 request_directory(event.path);
8241038 request_git_status();
1039+ if state().search_query != "" {
1040+ run_tree_search();
1041+ }
8251042}
8261043
8271044fn watch_directory(path: String) {
@@ -1053,6 +1270,19 @@ fn loading_rows() -> [TreePanelRow] {
10531270}
10541271
10551272fn build_rows() -> [TreePanelRow] {
1273+ if state().search_query != "" {
1274+ return red::neotree_core(
1275+ "build_search_rows",
1276+ red::string(state().cwd, "."),
1277+ state().search_children,
1278+ state().search_expanded,
1279+ state().selected,
1280+ state().clipboard,
1281+ optional_text(state().status.root, ""),
1282+ state().status_entries,
1283+ state().search_matches
1284+ );
1285+ }
10561286 return red::neotree_core(
10571287 "build_rows",
10581288 red::string(state().cwd, "."),
0 commit comments