Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:00:57.099Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

file_finder_tests.rs

5184 lines · 170.2 KB · rust
1use std::{future::IntoFuture, path::Path, time::Duration};
2
3use super::*;
4use editor::Editor;
5use gpui::{Entity, TestAppContext, VisualTestContext};
6use menu::{Cancel, Confirm, SelectNext, SelectPrevious};
7use pretty_assertions::{assert_eq, assert_matches};
8use project::{FS_WATCH_LATENCY, RemoveOptions};
9use serde_json::json;
10use settings::SettingsStore;
11use util::{path, rel_path::rel_path};
12use workspace::{
13    AppState, CloseActiveItem, Item, MultiWorkspace, OpenOptions, ToggleFileFinder, Workspace,
14    open_paths,
15};
16
17#[ctor::ctor(unsafe)]
18fn init_logger() {
19    zlog::init_test();
20}
21
22#[test]
23fn test_path_elision() {
24    #[track_caller]
25    fn check(path: &str, budget: usize, matches: impl IntoIterator<Item = usize>, expected: &str) {
26        let mut path = path.to_owned();
27        let slice = PathComponentSlice::new(&path);
28        let matches = Vec::from_iter(matches);
29        if let Some(range) = slice.elision_range(budget - 1, &matches) {
30            path.replace_range(range, "…");
31        }
32        assert_eq!(path, expected);
33    }
34
35    // Simple cases, mostly to check that different path shapes are handled gracefully.
36    check("p/a/b/c/d/", 6, [], "p/…/d/");
37    check("p/a/b/c/d/", 1, [2, 4, 6], "p/a/b/c/d/");
38    check("p/a/b/c/d/", 10, [2, 6], "p/a/…/c/d/");
39    check("p/a/b/c/d/", 8, [6], "p/…/c/d/");
40
41    check("p/a/b/c/d", 5, [], "p/…/d");
42    check("p/a/b/c/d", 9, [2, 4, 6], "p/a/b/c/d");
43    check("p/a/b/c/d", 9, [2, 6], "p/a/…/c/d");
44    check("p/a/b/c/d", 7, [6], "p/…/c/d");
45
46    check("/p/a/b/c/d/", 7, [], "/p/…/d/");
47    check("/p/a/b/c/d/", 11, [3, 5, 7], "/p/a/b/c/d/");
48    check("/p/a/b/c/d/", 11, [3, 7], "/p/a/…/c/d/");
49    check("/p/a/b/c/d/", 9, [7], "/p/…/c/d/");
50
51    // If the budget can't be met, no elision is done.
52    check(
53        "project/dir/child/grandchild",
54        5,
55        [],
56        "project/dir/child/grandchild",
57    );
58
59    // The longest unmatched segment is picked for elision.
60    check(
61        "project/one/two/X/three/sub",
62        21,
63        [16],
64        "project/…/X/three/sub",
65    );
66
67    // Elision stops when the budget is met, even though there are more components in the chosen segment.
68    // It proceeds from the end of the unmatched segment that is closer to the midpoint of the path.
69    check(
70        "project/one/two/three/X/sub",
71        21,
72        [22],
73        "project/…/three/X/sub",
74    )
75}
76
77#[test]
78fn test_custom_project_search_ordering_in_file_finder() {
79    let mut file_finder_sorted_output = vec![
80        ProjectPanelOrdMatch(PathMatch {
81            score: 0.5,
82            positions: Vec::new(),
83            worktree_id: 0,
84            path: rel_path("b0.5").into(),
85            path_prefix: rel_path("").into(),
86            distance_to_relative_ancestor: 0,
87            is_dir: false,
88        }),
89        ProjectPanelOrdMatch(PathMatch {
90            score: 1.0,
91            positions: Vec::new(),
92            worktree_id: 0,
93            path: rel_path("c1.0").into(),
94            path_prefix: rel_path("").into(),
95            distance_to_relative_ancestor: 0,
96            is_dir: false,
97        }),
98        ProjectPanelOrdMatch(PathMatch {
99            score: 1.0,
100            positions: Vec::new(),
101            worktree_id: 0,
102            path: rel_path("a1.0").into(),
103            path_prefix: rel_path("").into(),
104            distance_to_relative_ancestor: 0,
105            is_dir: false,
106        }),
107        ProjectPanelOrdMatch(PathMatch {
108            score: 0.5,
109            positions: Vec::new(),
110            worktree_id: 0,
111            path: rel_path("a0.5").into(),
112            path_prefix: rel_path("").into(),
113            distance_to_relative_ancestor: 0,
114            is_dir: false,
115        }),
116        ProjectPanelOrdMatch(PathMatch {
117            score: 1.0,
118            positions: Vec::new(),
119            worktree_id: 0,
120            path: rel_path("b1.0").into(),
121            path_prefix: rel_path("").into(),
122            distance_to_relative_ancestor: 0,
123            is_dir: false,
124        }),
125    ];
126    file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
127
128    assert_eq!(
129        file_finder_sorted_output,
130        vec![
131            ProjectPanelOrdMatch(PathMatch {
132                score: 1.0,
133                positions: Vec::new(),
134                worktree_id: 0,
135                path: rel_path("a1.0").into(),
136                path_prefix: rel_path("").into(),
137                distance_to_relative_ancestor: 0,
138                is_dir: false,
139            }),
140            ProjectPanelOrdMatch(PathMatch {
141                score: 1.0,
142                positions: Vec::new(),
143                worktree_id: 0,
144                path: rel_path("b1.0").into(),
145                path_prefix: rel_path("").into(),
146                distance_to_relative_ancestor: 0,
147                is_dir: false,
148            }),
149            ProjectPanelOrdMatch(PathMatch {
150                score: 1.0,
151                positions: Vec::new(),
152                worktree_id: 0,
153                path: rel_path("c1.0").into(),
154                path_prefix: rel_path("").into(),
155                distance_to_relative_ancestor: 0,
156                is_dir: false,
157            }),
158            ProjectPanelOrdMatch(PathMatch {
159                score: 0.5,
160                positions: Vec::new(),
161                worktree_id: 0,
162                path: rel_path("a0.5").into(),
163                path_prefix: rel_path("").into(),
164                distance_to_relative_ancestor: 0,
165                is_dir: false,
166            }),
167            ProjectPanelOrdMatch(PathMatch {
168                score: 0.5,
169                positions: Vec::new(),
170                worktree_id: 0,
171                path: rel_path("b0.5").into(),
172                path_prefix: rel_path("").into(),
173                distance_to_relative_ancestor: 0,
174                is_dir: false,
175            }),
176        ]
177    );
178}
179
180#[gpui::test]
181async fn test_matching_paths(cx: &mut TestAppContext) {
182    let app_state = init_test(cx);
183    app_state
184        .fs
185        .as_fake()
186        .insert_tree(
187            path!("/root"),
188            json!({
189                "a": {
190                    "banana": "",
191                    "bandana": "",
192                }
193            }),
194        )
195        .await;
196
197    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
198
199    let (picker, workspace, cx) = build_find_picker(project, cx);
200
201    simulate_input(cx, "bna");
202    picker.update(cx, |picker, _| {
203        assert_eq!(picker.delegate.matches.len(), 3);
204    });
205    cx.dispatch_action(SelectNext);
206    cx.dispatch_action(Confirm);
207    cx.read(|cx| {
208        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
209        assert_eq!(active_editor.read(cx).title(cx), "bandana");
210    });
211
212    for bandana_query in [
213        "bandana",
214        "./bandana",
215        ".\\bandana",
216        util::path!("a/bandana"),
217        "b/bandana",
218        "b\\bandana",
219        " bandana",
220        "bandana ",
221        " bandana ",
222        " ndan ",
223        " band ",
224        "a bandana",
225        "bandana:",
226    ] {
227        picker
228            .update_in(cx, |picker, window, cx| {
229                picker
230                    .delegate
231                    .update_matches(bandana_query.to_string(), window, cx)
232            })
233            .await;
234        picker.update(cx, |picker, _| {
235            assert_eq!(
236                picker.delegate.matches.len(),
237                // existence of CreateNew option depends on whether path already exists
238                if bandana_query == util::path!("a/bandana") {
239                    1
240                } else {
241                    2
242                },
243                "Wrong number of matches for bandana query '{bandana_query}'. Matches: {:?}",
244                picker.delegate.matches
245            );
246        });
247        cx.dispatch_action(SelectNext);
248        cx.dispatch_action(Confirm);
249        cx.read(|cx| {
250            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
251            assert_eq!(
252                active_editor.read(cx).title(cx),
253                "bandana",
254                "Wrong match for bandana query '{bandana_query}'"
255            );
256        });
257    }
258}
259
260#[gpui::test]
261async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
262    let app_state = init_test(cx);
263    app_state
264        .fs
265        .as_fake()
266        .insert_tree(
267            path!("/root"),
268            json!({
269                "a": {
270                    "foo:bar.rs": "",
271                    "foo.rs": "",
272                }
273            }),
274        )
275        .await;
276
277    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
278
279    let (picker, _, cx) = build_find_picker(project, cx);
280
281    // 'foo:' matches both files
282    simulate_input(cx, "foo:");
283    picker.update(cx, |picker, _| {
284        assert_eq!(picker.delegate.matches.len(), 3);
285        assert_match_at_position(picker, 0, "foo.rs");
286        assert_match_at_position(picker, 1, "foo:bar.rs");
287    });
288
289    // 'foo:b' matches one of the files
290    simulate_input(cx, "b");
291    picker.update(cx, |picker, _| {
292        assert_eq!(picker.delegate.matches.len(), 2);
293        assert_match_at_position(picker, 0, "foo:bar.rs");
294    });
295
296    cx.dispatch_action(editor::actions::Backspace);
297
298    // 'foo:1' matches both files, specifying which row to jump to
299    simulate_input(cx, "1");
300    picker.update(cx, |picker, _| {
301        assert_eq!(picker.delegate.matches.len(), 3);
302        assert_match_at_position(picker, 0, "foo.rs");
303        assert_match_at_position(picker, 1, "foo:bar.rs");
304    });
305}
306
307#[gpui::test]
308async fn test_unicode_paths(cx: &mut TestAppContext) {
309    let app_state = init_test(cx);
310    app_state
311        .fs
312        .as_fake()
313        .insert_tree(
314            path!("/root"),
315            json!({
316                "a": {
317                    "İg": " ",
318                }
319            }),
320        )
321        .await;
322
323    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
324
325    let (picker, workspace, cx) = build_find_picker(project, cx);
326
327    simulate_input(cx, "g");
328    picker.update(cx, |picker, _| {
329        assert_eq!(picker.delegate.matches.len(), 2);
330        assert_match_at_position(picker, 1, "g");
331    });
332    cx.dispatch_action(Confirm);
333    cx.read(|cx| {
334        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
335        assert_eq!(active_editor.read(cx).title(cx), "İg");
336    });
337}
338
339#[gpui::test]
340async fn test_absolute_paths(cx: &mut TestAppContext) {
341    let app_state = init_test(cx);
342    app_state
343        .fs
344        .as_fake()
345        .insert_tree(
346            path!("/root"),
347            json!({
348                "a": {
349                    "file1.txt": "",
350                    "b": {
351                        "file2.txt": "",
352                    },
353                }
354            }),
355        )
356        .await;
357
358    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
359
360    let (picker, workspace, cx) = build_find_picker(project, cx);
361
362    let matching_abs_path = path!("/root/a/b/file2.txt").to_string();
363    picker
364        .update_in(cx, |picker, window, cx| {
365            picker
366                .delegate
367                .update_matches(matching_abs_path, window, cx)
368        })
369        .await;
370    picker.update(cx, |picker, _| {
371        assert_eq!(
372            collect_search_matches(picker).search_paths_only(),
373            vec![rel_path("a/b/file2.txt").into()],
374            "Matching abs path should be the only match"
375        )
376    });
377    cx.dispatch_action(SelectNext);
378    cx.dispatch_action(Confirm);
379    cx.read(|cx| {
380        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
381        assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
382    });
383
384    let mismatching_abs_path = path!("/root/a/b/file1.txt").to_string();
385    picker
386        .update_in(cx, |picker, window, cx| {
387            picker
388                .delegate
389                .update_matches(mismatching_abs_path, window, cx)
390        })
391        .await;
392    picker.update(cx, |picker, _| {
393        assert_eq!(
394            collect_search_matches(picker).search_paths_only(),
395            Vec::new(),
396            "Mismatching abs path should produce no matches"
397        )
398    });
399}
400
401#[gpui::test]
402async fn test_complex_path(cx: &mut TestAppContext) {
403    let app_state = init_test(cx);
404
405    cx.update(|cx| {
406        let settings = *ProjectPanelSettings::get_global(cx);
407        ProjectPanelSettings::override_global(
408            ProjectPanelSettings {
409                hide_root: true,
410                ..settings
411            },
412            cx,
413        );
414    });
415
416    app_state
417        .fs
418        .as_fake()
419        .insert_tree(
420            path!("/root"),
421            json!({
422                "其他": {
423                    "S数据表格": {
424                        "task.xlsx": "some content",
425                    },
426                }
427            }),
428        )
429        .await;
430
431    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
432
433    let (picker, workspace, cx) = build_find_picker(project, cx);
434
435    simulate_input(cx, "t");
436    picker.update(cx, |picker, _| {
437        assert_eq!(picker.delegate.matches.len(), 2);
438        assert_eq!(
439            collect_search_matches(picker).search_paths_only(),
440            vec![rel_path("其他/S数据表格/task.xlsx").into()],
441        )
442    });
443    cx.dispatch_action(Confirm);
444    cx.read(|cx| {
445        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
446        assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
447    });
448}
449
450#[gpui::test]
451async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
452    let app_state = init_test(cx);
453
454    let first_file_name = "first.rs";
455    let first_file_contents = "// First Rust file";
456    app_state
457        .fs
458        .as_fake()
459        .insert_tree(
460            path!("/src"),
461            json!({
462                "test": {
463                    first_file_name: first_file_contents,
464                    "second.rs": "// Second Rust file",
465                }
466            }),
467        )
468        .await;
469
470    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
471
472    let (picker, workspace, cx) = build_find_picker(project, cx);
473
474    let file_query = &first_file_name[..3];
475    let file_row = 1;
476    let file_column = 3;
477    assert!(file_column <= first_file_contents.len());
478    let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
479    picker
480        .update_in(cx, |finder, window, cx| {
481            finder
482                .delegate
483                .update_matches(query_inside_file.to_string(), window, cx)
484        })
485        .await;
486    picker.update(cx, |finder, _| {
487        assert_match_at_position(finder, 1, &query_inside_file.to_string());
488        let finder = &finder.delegate;
489        assert_eq!(finder.matches.len(), 2);
490        let latest_search_query = finder
491            .latest_search_query
492            .as_ref()
493            .expect("Finder should have a query after the update_matches call");
494        assert_eq!(latest_search_query.raw_query, query_inside_file);
495        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
496        assert_eq!(latest_search_query.path_position.row, Some(file_row));
497        assert_eq!(
498            latest_search_query.path_position.column,
499            Some(file_column as u32)
500        );
501    });
502
503    cx.dispatch_action(Confirm);
504
505    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
506    cx.executor().advance_clock(Duration::from_secs(2));
507
508    editor.update(cx, |editor, cx| {
509            let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
510            assert_eq!(
511                all_selections.len(),
512                1,
513                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
514            );
515            let caret_selection = all_selections.into_iter().next().unwrap();
516            assert_eq!(caret_selection.start, caret_selection.end,
517                "Caret selection should have its start and end at the same position");
518            assert_eq!(file_row, caret_selection.start.row + 1,
519                "Query inside file should get caret with the same focus row");
520            assert_eq!(file_column, caret_selection.start.column as usize + 1,
521                "Query inside file should get caret with the same focus column");
522        });
523}
524
525#[gpui::test]
526async fn test_row_column_numbers_query_inside_unicode_file(cx: &mut TestAppContext) {
527    let app_state = init_test(cx);
528
529    let first_file_name = "first.rs";
530    let first_file_contents = "aéøbcdef";
531    app_state
532        .fs
533        .as_fake()
534        .insert_tree(
535            path!("/src"),
536            json!({
537                "test": {
538                    first_file_name: first_file_contents,
539                    "second.rs": "// Second Rust file",
540                }
541            }),
542        )
543        .await;
544
545    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
546
547    let (picker, workspace, cx) = build_find_picker(project, cx);
548
549    let file_query = &first_file_name[..3];
550    let file_row = 1;
551    let file_column = 5;
552    let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
553    picker
554        .update_in(cx, |finder, window, cx| {
555            finder
556                .delegate
557                .update_matches(query_inside_file.to_string(), window, cx)
558        })
559        .await;
560    picker.update(cx, |finder, _| {
561        assert_match_at_position(finder, 1, &query_inside_file.to_string());
562        let finder = &finder.delegate;
563        assert_eq!(finder.matches.len(), 2);
564        let latest_search_query = finder
565            .latest_search_query
566            .as_ref()
567            .expect("Finder should have a query after the update_matches call");
568        assert_eq!(latest_search_query.raw_query, query_inside_file);
569        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
570        assert_eq!(latest_search_query.path_position.row, Some(file_row));
571        assert_eq!(latest_search_query.path_position.column, Some(file_column));
572    });
573
574    cx.dispatch_action(Confirm);
575
576    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
577    cx.executor().advance_clock(Duration::from_secs(2));
578
579    let expected_column = first_file_contents
580        .chars()
581        .take(file_column as usize - 1)
582        .map(|character| character.len_utf8())
583        .sum::<usize>();
584
585    editor.update(cx, |editor, cx| {
586        let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
587        assert_eq!(
588            all_selections.len(),
589            1,
590            "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
591        );
592        let caret_selection = all_selections.into_iter().next().unwrap();
593        assert_eq!(
594            caret_selection.start, caret_selection.end,
595            "Caret selection should have its start and end at the same position"
596        );
597        assert_eq!(
598            file_row,
599            caret_selection.start.row + 1,
600            "Query inside file should get caret with the same focus row"
601        );
602        assert_eq!(
603            expected_column,
604            caret_selection.start.column as usize,
605            "Query inside file should map user-visible columns to byte offsets for Unicode text"
606        );
607    });
608}
609
610#[gpui::test]
611async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
612    let app_state = init_test(cx);
613
614    let first_file_name = "first.rs";
615    let first_file_contents = "// First Rust file";
616    app_state
617        .fs
618        .as_fake()
619        .insert_tree(
620            path!("/src"),
621            json!({
622                "test": {
623                    first_file_name: first_file_contents,
624                    "second.rs": "// Second Rust file",
625                }
626            }),
627        )
628        .await;
629
630    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
631
632    let (picker, workspace, cx) = build_find_picker(project, cx);
633
634    let file_query = &first_file_name[..3];
635    let file_row = 200;
636    let file_column = 300;
637    assert!(file_column > first_file_contents.len());
638    let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
639    picker
640        .update_in(cx, |picker, window, cx| {
641            picker
642                .delegate
643                .update_matches(query_outside_file.to_string(), window, cx)
644        })
645        .await;
646    picker.update(cx, |finder, _| {
647        assert_match_at_position(finder, 1, &query_outside_file.to_string());
648        let delegate = &finder.delegate;
649        assert_eq!(delegate.matches.len(), 2);
650        let latest_search_query = delegate
651            .latest_search_query
652            .as_ref()
653            .expect("Finder should have a query after the update_matches call");
654        assert_eq!(latest_search_query.raw_query, query_outside_file);
655        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
656        assert_eq!(latest_search_query.path_position.row, Some(file_row));
657        assert_eq!(
658            latest_search_query.path_position.column,
659            Some(file_column as u32)
660        );
661    });
662
663    cx.dispatch_action(Confirm);
664
665    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
666    cx.executor().advance_clock(Duration::from_secs(2));
667
668    editor.update(cx, |editor, cx| {
669            let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
670            assert_eq!(
671                all_selections.len(),
672                1,
673                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
674            );
675            let caret_selection = all_selections.into_iter().next().unwrap();
676            assert_eq!(caret_selection.start, caret_selection.end,
677                "Caret selection should have its start and end at the same position");
678            assert_eq!(0, caret_selection.start.row,
679                "Excessive rows (as in query outside file borders) should get trimmed to last file row");
680            assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
681                "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
682        });
683}
684
685#[test]
686fn test_line_range_query_parsing() {
687    let query = parse_file_search_query("fs/smb/server/connection.c:428-440");
688
689    assert_eq!(query.raw_query, "fs/smb/server/connection.c:428-440");
690    assert_eq!(
691        query.file_query_end,
692        Some("fs/smb/server/connection.c".len())
693    );
694    assert_eq!(query.path_query(), "fs/smb/server/connection.c");
695    assert_eq!(query.path_position.row, Some(428));
696    assert_eq!(query.path_position.column, None);
697    assert_eq!(query.line_range, Some(428..=440));
698}
699
700#[test]
701fn test_parse_search_query() {
702    // Test trailing colon stripping.
703    let query = parse_file_search_query("content.rs:2:");
704    assert_eq!(query.raw_query, "content.rs:2");
705    assert_eq!(query.path_query(), "content.rs");
706    assert_eq!(query.path_position.row, Some(2));
707    assert_eq!(query.path_position.column, None);
708    assert_eq!(query.line_range, None);
709
710    // Test multiple trailing colons are also stripped.
711    let query = parse_file_search_query("content.rs:2:::");
712    assert_eq!(query.raw_query, "content.rs:2");
713    assert_eq!(query.path_query(), "content.rs");
714    assert_eq!(query.path_position.row, Some(2));
715    assert_eq!(query.path_position.column, None);
716    assert_eq!(query.line_range, None);
717
718    // Test trailing colon after an incomplete range is stripped.
719    let query = parse_file_search_query("content.rs:2-:");
720    assert_eq!(query.raw_query, "content.rs:2-");
721    assert_eq!(query.path_query(), "content.rs");
722    assert_eq!(query.path_position.row, Some(2));
723    assert_eq!(query.path_position.column, None);
724    assert_eq!(query.line_range, None);
725
726    // Test trailing colon after a complete range is stripped, range is preserved.
727    let query = parse_file_search_query("content.rs:2-4:");
728    assert_eq!(query.raw_query, "content.rs:2-4");
729    assert_eq!(query.path_query(), "content.rs");
730    assert_eq!(query.path_position.row, Some(2));
731    assert_eq!(query.path_position.column, None);
732    assert_eq!(query.line_range, Some(2..=4));
733
734    // Test multiple trailing colons after a complete range are all stripped.
735    let query = parse_file_search_query("content.rs:2-4:::");
736    assert_eq!(query.raw_query, "content.rs:2-4");
737    assert_eq!(query.path_query(), "content.rs");
738    assert_eq!(query.path_position.row, Some(2));
739    assert_eq!(query.path_position.column, None);
740    assert_eq!(query.line_range, Some(2..=4));
741
742    // Test invalid end should fall back to using the start as a single row.
743    let query = parse_file_search_query("content.rs:5-x");
744    assert_eq!(query.raw_query, "content.rs:5-x");
745    assert_eq!(query.path_query(), "content.rs");
746    assert_eq!(query.path_position.row, Some(5));
747    assert_eq!(query.path_position.column, None);
748    assert_eq!(query.line_range, None);
749
750    // Test reversed range (end < start) should fall back to using the start as a single row.
751    let query = parse_file_search_query("content.rs:10-5");
752    assert_eq!(query.raw_query, "content.rs:10-5");
753    assert_eq!(query.path_query(), "content.rs");
754    assert_eq!(query.path_position.row, Some(10));
755    assert_eq!(query.path_position.column, None);
756    assert_eq!(query.line_range, None);
757}
758
759#[gpui::test]
760async fn test_line_range_query_selects_lines(cx: &mut TestAppContext) {
761    let app_state = init_test(cx);
762
763    let first_file_contents = "line 1\nline 2\nline 3\nline 4\nline 5";
764    app_state
765        .fs
766        .as_fake()
767        .insert_tree(
768            path!("/src"),
769            json!({
770                "test": {
771                    "first.rs": first_file_contents,
772                }
773            }),
774        )
775        .await;
776
777    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
778
779    let (picker, workspace, cx) = build_find_picker(project, cx);
780
781    let query = "test/first.rs:2-4";
782    picker
783        .update_in(cx, |finder, window, cx| {
784            finder
785                .delegate
786                .update_matches(query.to_string(), window, cx)
787        })
788        .await;
789    picker.update(cx, |finder, _| {
790        assert_eq!(finder.delegate.matches.len(), 2);
791        assert_match_at_position(finder, 0, "first.rs");
792        assert_match_at_position(finder, 1, "first.rs:2-4");
793
794        let latest_search_query = finder
795            .delegate
796            .latest_search_query
797            .as_ref()
798            .expect("Finder should have a query after the update_matches call");
799        assert_eq!(latest_search_query.raw_query, query);
800        assert_eq!(
801            latest_search_query.file_query_end,
802            Some("test/first.rs".len())
803        );
804        assert_eq!(latest_search_query.path_position.row, Some(2));
805        assert_eq!(latest_search_query.path_position.column, None);
806        assert_eq!(latest_search_query.line_range, Some(2..=4));
807    });
808
809    cx.dispatch_action(Confirm);
810
811    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
812    cx.executor().advance_clock(Duration::from_secs(2));
813
814    editor.update(cx, |editor, cx| {
815        let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
816        assert_eq!(
817            all_selections.len(),
818            1,
819            "Expected to have 1 selection after file finder confirm, but got: {all_selections:?}"
820        );
821        let selection = all_selections.into_iter().next().unwrap();
822        assert_eq!(selection.start.row, 1);
823        assert_eq!(selection.start.column, 0);
824        assert_eq!(selection.end.row, 4);
825        assert_eq!(selection.end.column, 0);
826    });
827}
828
829#[gpui::test]
830async fn test_line_range_query_outside_file_clamps_to_eof(cx: &mut TestAppContext) {
831    let app_state = init_test(cx);
832
833    let first_file_contents = "line 1\nline 2";
834    app_state
835        .fs
836        .as_fake()
837        .insert_tree(
838            path!("/src"),
839            json!({
840                "test": {
841                    "first.rs": first_file_contents,
842                }
843            }),
844        )
845        .await;
846
847    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
848
849    let (picker, workspace, cx) = build_find_picker(project, cx);
850
851    let query = "test/first.rs:200-300";
852    picker
853        .update_in(cx, |finder, window, cx| {
854            finder
855                .delegate
856                .update_matches(query.to_string(), window, cx)
857        })
858        .await;
859
860    cx.dispatch_action(Confirm);
861
862    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
863    cx.executor().advance_clock(Duration::from_secs(2));
864
865    editor.update(cx, |editor, cx| {
866        let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
867        assert_eq!(
868            all_selections.len(),
869            1,
870            "Expected to have 1 selection after file finder confirm, but got: {all_selections:?}"
871        );
872        let selection = all_selections.into_iter().next().unwrap();
873        assert_eq!(selection.start, selection.end);
874        assert_eq!(selection.start.row, 1);
875        assert_eq!(selection.start.column, "line 2".len() as u32);
876    });
877}
878
879#[gpui::test]
880async fn test_matching_cancellation(cx: &mut TestAppContext) {
881    let app_state = init_test(cx);
882    app_state
883        .fs
884        .as_fake()
885        .insert_tree(
886            "/dir",
887            json!({
888                "hello": "",
889                "goodbye": "",
890                "halogen-light": "",
891                "happiness": "",
892                "height": "",
893                "hi": "",
894                "hiccup": "",
895            }),
896        )
897        .await;
898
899    let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
900
901    let (picker, _, cx) = build_find_picker(project, cx);
902
903    let query = test_path_position("hi");
904    picker
905        .update_in(cx, |picker, window, cx| {
906            picker.delegate.spawn_search(query.clone(), window, cx)
907        })
908        .await;
909
910    picker.update(cx, |picker, _cx| {
911        // CreateNew option not shown in this case since file already exists
912        assert_eq!(picker.delegate.matches.len(), 5);
913    });
914
915    picker.update_in(cx, |picker, window, cx| {
916        let matches = collect_search_matches(picker).search_matches_only();
917        let delegate = &mut picker.delegate;
918
919        // Simulate a search being cancelled after the time limit,
920        // returning only a subset of the matches that would have been found.
921        drop(delegate.spawn_search(query.clone(), window, cx));
922        delegate.set_search_matches(
923            delegate.latest_search_id,
924            true, // did-cancel
925            query.clone(),
926            vec![
927                ProjectPanelOrdMatch(matches[1].clone()),
928                ProjectPanelOrdMatch(matches[3].clone()),
929            ],
930            cx,
931        );
932
933        // Simulate another cancellation.
934        drop(delegate.spawn_search(query.clone(), window, cx));
935        delegate.set_search_matches(
936            delegate.latest_search_id,
937            true, // did-cancel
938            query.clone(),
939            vec![
940                ProjectPanelOrdMatch(matches[0].clone()),
941                ProjectPanelOrdMatch(matches[2].clone()),
942                ProjectPanelOrdMatch(matches[3].clone()),
943            ],
944            cx,
945        );
946
947        assert_eq!(
948            collect_search_matches(picker)
949                .search_matches_only()
950                .as_slice(),
951            &matches[0..4]
952        );
953    });
954}
955
956#[gpui::test]
957async fn test_ignored_root_with_file_inclusions(cx: &mut TestAppContext) {
958    let app_state = init_test(cx);
959    cx.update(|cx| {
960        cx.update_global::<SettingsStore, _>(|store, cx| {
961            store.update_user_settings(cx, |settings| {
962                settings.project.worktree.file_scan_inclusions = Some(vec![
963                    "height_demo/**/hi_bonjour".to_string(),
964                    "**/height_1".to_string(),
965                ]);
966            });
967        })
968    });
969    app_state
970        .fs
971        .as_fake()
972        .insert_tree(
973            "/ancestor",
974            json!({
975                ".gitignore": "ignored-root",
976                "ignored-root": {
977                    "happiness": "",
978                    "height": "",
979                    "hi": "",
980                    "hiccup": "",
981                },
982                "tracked-root": {
983                    ".gitignore": "height*",
984                    "happiness": "",
985                    "height": "",
986                    "heights": {
987                        "height_1": "",
988                        "height_2": "",
989                    },
990                    "height_demo": {
991                        "test_1": {
992                            "hi_bonjour": "hi_bonjour",
993                            "hi": "hello",
994                        },
995                        "hihi": "bye",
996                        "test_2": {
997                            "hoi": "nl"
998                        }
999                    },
1000                    "height_include": {
1001                        "height_1_include": "",
1002                        "height_2_include": "",
1003                    },
1004                    "hi": "",
1005                    "hiccup": "",
1006                },
1007            }),
1008        )
1009        .await;
1010
1011    let project = Project::test(
1012        app_state.fs.clone(),
1013        [
1014            Path::new(path!("/ancestor/tracked-root")),
1015            Path::new(path!("/ancestor/ignored-root")),
1016        ],
1017        cx,
1018    )
1019    .await;
1020    let (picker, _workspace, cx) = build_find_picker(project, cx);
1021
1022    picker
1023        .update_in(cx, |picker, window, cx| {
1024            picker
1025                .delegate
1026                .spawn_search(test_path_position("hi"), window, cx)
1027        })
1028        .await;
1029    picker.update(cx, |picker, _| {
1030        let matches = collect_search_matches(picker);
1031        assert_eq!(matches.history.len(), 0);
1032        assert_eq!(
1033            matches.search,
1034            vec![
1035                rel_path("ignored-root/hi").into(),
1036                rel_path("tracked-root/hi").into(),
1037                rel_path("ignored-root/hiccup").into(),
1038                rel_path("tracked-root/hiccup").into(),
1039                rel_path("tracked-root/height_demo/test_1/hi_bonjour").into(),
1040                rel_path("ignored-root/height").into(),
1041                rel_path("tracked-root/heights/height_1").into(),
1042                rel_path("ignored-root/happiness").into(),
1043                rel_path("tracked-root/happiness").into(),
1044            ],
1045            "All ignored files that were indexed are found for default ignored mode"
1046        );
1047    });
1048}
1049
1050#[gpui::test]
1051async fn test_ignored_root_with_file_inclusions_repro(cx: &mut TestAppContext) {
1052    let app_state = init_test(cx);
1053    cx.update(|cx| {
1054        cx.update_global::<SettingsStore, _>(|store, cx| {
1055            store.update_user_settings(cx, |settings| {
1056                settings.project.worktree.file_scan_inclusions = Some(vec!["**/.env".to_string()]);
1057            });
1058        })
1059    });
1060    app_state
1061        .fs
1062        .as_fake()
1063        .insert_tree(
1064            "/src",
1065            json!({
1066                ".gitignore": "node_modules",
1067                "node_modules": {
1068                    "package.json": "// package.json",
1069                    ".env": "BAR=FOO"
1070                },
1071                ".env": "FOO=BAR"
1072            }),
1073        )
1074        .await;
1075
1076    let project = Project::test(app_state.fs.clone(), [Path::new(path!("/src"))], cx).await;
1077    let (picker, _workspace, cx) = build_find_picker(project, cx);
1078
1079    picker
1080        .update_in(cx, |picker, window, cx| {
1081            picker
1082                .delegate
1083                .spawn_search(test_path_position("json"), window, cx)
1084        })
1085        .await;
1086    picker.update(cx, |picker, _| {
1087        let matches = collect_search_matches(picker);
1088        assert_eq!(matches.history.len(), 0);
1089        assert_eq!(
1090            matches.search,
1091            vec![],
1092            "All ignored files that were indexed are found for default ignored mode"
1093        );
1094    });
1095}
1096
1097#[gpui::test]
1098async fn test_toggle_action_include_ignored_param(cx: &mut TestAppContext) {
1099    let app_state = init_test(cx);
1100    let project = Project::test(app_state.fs.clone(), [], cx).await;
1101    let (multi_workspace, cx) =
1102        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1103    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1104
1105    let cases = [
1106        (None, Some(true), Some(true)),
1107        (None, Some(false), Some(false)),
1108        (None, None, None),
1109        (Some(true), Some(false), Some(false)),
1110        (Some(false), Some(true), Some(true)),
1111        (Some(true), None, Some(true)),
1112    ];
1113    for (setting, action_param, expected) in cases {
1114        cx.update(|_, cx| {
1115            let settings = *FileFinderSettings::get_global(cx);
1116            FileFinderSettings::override_global(
1117                FileFinderSettings {
1118                    include_ignored: setting,
1119                    ..settings
1120                },
1121                cx,
1122            );
1123        });
1124        cx.dispatch_action(ToggleFileFinder {
1125            separate_history: false,
1126            include_ignored: action_param,
1127        });
1128        let picker = active_file_picker(&workspace, cx);
1129        picker.update(cx, |picker, _| {
1130            assert_eq!(
1131                picker.delegate.include_ignored, expected,
1132                "setting: {setting:?}, action param: {action_param:?}"
1133            );
1134        });
1135        cx.dispatch_action(menu::Cancel);
1136    }
1137}
1138
1139#[gpui::test]
1140async fn test_ignored_root(cx: &mut TestAppContext) {
1141    let app_state = init_test(cx);
1142    app_state
1143        .fs
1144        .as_fake()
1145        .insert_tree(
1146            "/ancestor",
1147            json!({
1148                ".gitignore": "ignored-root",
1149                "ignored-root": {
1150                    "happiness": "",
1151                    "height": "",
1152                    "hi": "",
1153                    "hiccup": "",
1154                },
1155                "tracked-root": {
1156                    ".gitignore": "height*",
1157                    "happiness": "",
1158                    "height": "",
1159                    "heights": {
1160                        "height_1": "",
1161                        "height_2": "",
1162                    },
1163                    "hi": "",
1164                    "hiccup": "",
1165                },
1166            }),
1167        )
1168        .await;
1169
1170    let project = Project::test(
1171        app_state.fs.clone(),
1172        [
1173            Path::new(path!("/ancestor/tracked-root")),
1174            Path::new(path!("/ancestor/ignored-root")),
1175        ],
1176        cx,
1177    )
1178    .await;
1179    let (picker, workspace, cx) = build_find_picker(project, cx);
1180
1181    picker
1182        .update_in(cx, |picker, window, cx| {
1183            picker
1184                .delegate
1185                .spawn_search(test_path_position("hi"), window, cx)
1186        })
1187        .await;
1188    picker.update(cx, |picker, _| {
1189        let matches = collect_search_matches(picker);
1190        assert_eq!(matches.history.len(), 0);
1191        assert_eq!(
1192            matches.search,
1193            vec![
1194                rel_path("ignored-root/hi").into(),
1195                rel_path("tracked-root/hi").into(),
1196                rel_path("ignored-root/hiccup").into(),
1197                rel_path("tracked-root/hiccup").into(),
1198                rel_path("ignored-root/height").into(),
1199                rel_path("ignored-root/happiness").into(),
1200                rel_path("tracked-root/happiness").into(),
1201            ],
1202            "All ignored files that were indexed are found for default ignored mode"
1203        );
1204    });
1205    cx.dispatch_action(ToggleIncludeIgnored);
1206    picker
1207        .update_in(cx, |picker, window, cx| {
1208            picker
1209                .delegate
1210                .spawn_search(test_path_position("hi"), window, cx)
1211        })
1212        .await;
1213    picker.update(cx, |picker, _| {
1214        let matches = collect_search_matches(picker);
1215        assert_eq!(matches.history.len(), 0);
1216        assert_eq!(
1217            matches.search,
1218            vec![
1219                rel_path("ignored-root/hi").into(),
1220                rel_path("tracked-root/hi").into(),
1221                rel_path("ignored-root/hiccup").into(),
1222                rel_path("tracked-root/hiccup").into(),
1223                rel_path("ignored-root/height").into(),
1224                rel_path("tracked-root/height").into(),
1225                rel_path("ignored-root/happiness").into(),
1226                rel_path("tracked-root/happiness").into(),
1227            ],
1228            "All ignored files should be found, for the toggled on ignored mode"
1229        );
1230    });
1231
1232    picker
1233        .update_in(cx, |picker, window, cx| {
1234            picker.delegate.include_ignored = Some(false);
1235            picker
1236                .delegate
1237                .spawn_search(test_path_position("hi"), window, cx)
1238        })
1239        .await;
1240    picker.update(cx, |picker, _| {
1241        let matches = collect_search_matches(picker);
1242        assert_eq!(matches.history.len(), 0);
1243        assert_eq!(
1244            matches.search,
1245            vec![
1246                rel_path("tracked-root/hi").into(),
1247                rel_path("tracked-root/hiccup").into(),
1248                rel_path("tracked-root/happiness").into(),
1249            ],
1250            "Only non-ignored files should be found for the turned off ignored mode"
1251        );
1252    });
1253
1254    workspace
1255        .update_in(cx, |workspace, window, cx| {
1256            workspace.open_abs_path(
1257                PathBuf::from(path!("/ancestor/tracked-root/heights/height_1")),
1258                OpenOptions {
1259                    visible: Some(OpenVisible::None),
1260                    ..OpenOptions::default()
1261                },
1262                window,
1263                cx,
1264            )
1265        })
1266        .await
1267        .unwrap();
1268    cx.run_until_parked();
1269    workspace
1270        .update_in(cx, |workspace, window, cx| {
1271            workspace.active_pane().update(cx, |pane, cx| {
1272                pane.close_active_item(&CloseActiveItem::default(), window, cx)
1273            })
1274        })
1275        .await
1276        .unwrap();
1277    cx.run_until_parked();
1278
1279    picker
1280        .update_in(cx, |picker, window, cx| {
1281            picker.delegate.include_ignored = None;
1282            picker
1283                .delegate
1284                .spawn_search(test_path_position("hi"), window, cx)
1285        })
1286        .await;
1287    picker.update(cx, |picker, _| {
1288        let matches = collect_search_matches(picker);
1289        assert_eq!(matches.history.len(), 0);
1290        assert_eq!(
1291            matches.search,
1292            vec![
1293                rel_path("ignored-root/hi").into(),
1294                rel_path("tracked-root/hi").into(),
1295                rel_path("ignored-root/hiccup").into(),
1296                rel_path("tracked-root/hiccup").into(),
1297                rel_path("ignored-root/height").into(),
1298                rel_path("ignored-root/happiness").into(),
1299                rel_path("tracked-root/happiness").into(),
1300            ],
1301            "Only for the worktree with the ignored root, all indexed ignored files are found in the auto ignored mode"
1302        );
1303    });
1304
1305    picker
1306        .update_in(cx, |picker, window, cx| {
1307            picker.delegate.include_ignored = Some(true);
1308            picker
1309                .delegate
1310                .spawn_search(test_path_position("hi"), window, cx)
1311        })
1312        .await;
1313    picker.update(cx, |picker, _| {
1314        let matches = collect_search_matches(picker);
1315        assert_eq!(matches.history.len(), 0);
1316        assert_eq!(
1317            matches.search,
1318            vec![
1319                rel_path("ignored-root/hi").into(),
1320                rel_path("tracked-root/hi").into(),
1321                rel_path("ignored-root/hiccup").into(),
1322                rel_path("tracked-root/hiccup").into(),
1323                rel_path("ignored-root/height").into(),
1324                rel_path("tracked-root/height").into(),
1325                rel_path("tracked-root/heights/height_1").into(),
1326                rel_path("tracked-root/heights/height_2").into(),
1327                rel_path("ignored-root/happiness").into(),
1328                rel_path("tracked-root/happiness").into(),
1329            ],
1330            "All ignored files that were indexed are found in the turned on ignored mode"
1331        );
1332    });
1333
1334    picker
1335        .update_in(cx, |picker, window, cx| {
1336            picker.delegate.include_ignored = Some(false);
1337            picker
1338                .delegate
1339                .spawn_search(test_path_position("hi"), window, cx)
1340        })
1341        .await;
1342    picker.update(cx, |picker, _| {
1343        let matches = collect_search_matches(picker);
1344        assert_eq!(matches.history.len(), 0);
1345        assert_eq!(
1346            matches.search,
1347            vec![
1348                rel_path("tracked-root/hi").into(),
1349                rel_path("tracked-root/hiccup").into(),
1350                rel_path("tracked-root/happiness").into(),
1351            ],
1352            "Only non-ignored files should be found for the turned off ignored mode"
1353        );
1354    });
1355}
1356
1357#[gpui::test]
1358async fn test_single_file_worktrees(cx: &mut TestAppContext) {
1359    let app_state = init_test(cx);
1360    app_state
1361        .fs
1362        .as_fake()
1363        .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
1364        .await;
1365
1366    let project = Project::test(
1367        app_state.fs.clone(),
1368        ["/root/the-parent-dir/the-file".as_ref()],
1369        cx,
1370    )
1371    .await;
1372
1373    let (picker, _, cx) = build_find_picker(project, cx);
1374
1375    // Even though there is only one worktree, that worktree's filename
1376    // is included in the matching, because the worktree is a single file.
1377    picker
1378        .update_in(cx, |picker, window, cx| {
1379            picker
1380                .delegate
1381                .spawn_search(test_path_position("thf"), window, cx)
1382        })
1383        .await;
1384    cx.read(|cx| {
1385        let picker = picker.read(cx);
1386        let delegate = &picker.delegate;
1387        let matches = collect_search_matches(picker).search_matches_only();
1388        assert_eq!(matches.len(), 1);
1389
1390        let (file_name, file_name_positions, full_path, full_path_positions) =
1391            delegate.labels_for_path_match(&matches[0], PathStyle::local());
1392        assert_eq!(file_name, "the-file");
1393        assert_eq!(file_name_positions, &[0, 1, 4]);
1394        assert_eq!(full_path, "");
1395        assert_eq!(full_path_positions, &[0; 0]);
1396    });
1397
1398    // Since the worktree root is a file, searching for its name followed by a slash does
1399    // not match anything.
1400    picker
1401        .update_in(cx, |picker, window, cx| {
1402            picker
1403                .delegate
1404                .spawn_search(test_path_position("thf/"), window, cx)
1405        })
1406        .await;
1407    picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
1408}
1409
1410#[gpui::test]
1411async fn test_history_items_uniqueness_for_multiple_worktree(cx: &mut TestAppContext) {
1412    let app_state = init_test(cx);
1413    app_state
1414        .fs
1415        .as_fake()
1416        .insert_tree(
1417            path!("/repo1"),
1418            json!({
1419                "package.json": r#"{"name": "repo1"}"#,
1420                "src": {
1421                    "index.js": "// Repo 1 index",
1422                }
1423            }),
1424        )
1425        .await;
1426
1427    app_state
1428        .fs
1429        .as_fake()
1430        .insert_tree(
1431            path!("/repo2"),
1432            json!({
1433                "package.json": r#"{"name": "repo2"}"#,
1434                "src": {
1435                    "index.js": "// Repo 2 index",
1436                }
1437            }),
1438        )
1439        .await;
1440
1441    let project = Project::test(
1442        app_state.fs.clone(),
1443        [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
1444        cx,
1445    )
1446    .await;
1447
1448    let (multi_workspace, cx) =
1449        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1450    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1451    let (worktree_id1, worktree_id2) = cx.read(|cx| {
1452        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1453        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1454    });
1455
1456    workspace
1457        .update_in(cx, |workspace, window, cx| {
1458            workspace.open_path(
1459                ProjectPath {
1460                    worktree_id: worktree_id1,
1461                    path: rel_path("package.json").into(),
1462                },
1463                None,
1464                true,
1465                window,
1466                cx,
1467            )
1468        })
1469        .await
1470        .unwrap();
1471
1472    cx.dispatch_action(workspace::CloseActiveItem {
1473        save_intent: None,
1474        close_pinned: false,
1475    });
1476
1477    let picker = open_file_picker(&workspace, cx);
1478    simulate_input(cx, "package.json");
1479
1480    picker.update(cx, |finder, _| {
1481        let matches = &finder.delegate.matches.matches;
1482
1483        assert_eq!(
1484            matches.len(),
1485            2,
1486            "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
1487            matches.len(),
1488            matches
1489        );
1490
1491        assert_matches!(matches[0], Match::History { .. });
1492
1493        let search_matches = collect_search_matches(finder);
1494        assert_eq!(
1495            search_matches.history.len(),
1496            1,
1497            "Should have exactly 1 history match"
1498        );
1499        assert_eq!(
1500            search_matches.search.len(),
1501            1,
1502            "Should have exactly 1 search match (the other package.json)"
1503        );
1504
1505        if let Match::History { path, .. } = &matches[0] {
1506            assert_eq!(path.project.worktree_id, worktree_id1);
1507            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
1508        }
1509
1510        if let Match::Search(path_match) = &matches[1] {
1511            assert_eq!(
1512                WorktreeId::from_usize(path_match.0.worktree_id),
1513                worktree_id2
1514            );
1515            assert_eq!(path_match.0.path.as_ref(), rel_path("package.json"));
1516        }
1517    });
1518}
1519
1520#[gpui::test]
1521async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
1522    let app_state = init_test(cx);
1523    app_state
1524        .fs
1525        .as_fake()
1526        .insert_tree(
1527            path!("/roota"),
1528            json!({ "the-parent-dira": { "filea": "" } }),
1529        )
1530        .await;
1531
1532    app_state
1533        .fs
1534        .as_fake()
1535        .insert_tree(
1536            path!("/rootb"),
1537            json!({ "the-parent-dirb": { "fileb": "" } }),
1538        )
1539        .await;
1540
1541    let project = Project::test(
1542        app_state.fs.clone(),
1543        [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1544        cx,
1545    )
1546    .await;
1547
1548    let (multi_workspace, cx) =
1549        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1550    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1551    let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1552        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1553        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1554    });
1555
1556    let b_path = ProjectPath {
1557        worktree_id: worktree_id2,
1558        path: rel_path("the-parent-dirb/fileb").into(),
1559    };
1560    workspace
1561        .update_in(cx, |workspace, window, cx| {
1562            workspace.open_path(b_path, None, true, window, cx)
1563        })
1564        .await
1565        .unwrap();
1566
1567    let finder = open_file_picker(&workspace, cx);
1568
1569    finder
1570        .update_in(cx, |f, window, cx| {
1571            f.delegate.spawn_search(
1572                test_path_position(path!("the-parent-dirb/filec")),
1573                window,
1574                cx,
1575            )
1576        })
1577        .await;
1578    cx.run_until_parked();
1579    finder.update_in(cx, |picker, window, cx| {
1580        assert_eq!(picker.delegate.matches.len(), 1);
1581        picker.delegate.confirm(false, window, cx)
1582    });
1583    cx.run_until_parked();
1584    cx.read(|cx| {
1585        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1586        let project_path = active_editor.read(cx).active_project_path(cx);
1587        assert_eq!(
1588            project_path,
1589            Some(ProjectPath {
1590                worktree_id: worktree_id2,
1591                path: rel_path("the-parent-dirb/filec").into()
1592            })
1593        );
1594    });
1595}
1596
1597#[gpui::test]
1598async fn test_create_file_focused_file_does_not_belong_to_available_worktrees(
1599    cx: &mut TestAppContext,
1600) {
1601    let app_state = init_test(cx);
1602    app_state
1603        .fs
1604        .as_fake()
1605        .insert_tree(path!("/roota"), json!({ "the-parent-dira": { "filea": ""}}))
1606        .await;
1607
1608    app_state
1609        .fs
1610        .as_fake()
1611        .insert_tree(path!("/rootb"), json!({"the-parent-dirb":{ "fileb": ""}}))
1612        .await;
1613
1614    let project = Project::test(
1615        app_state.fs.clone(),
1616        [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1617        cx,
1618    )
1619    .await;
1620
1621    let (multi_workspace, cx) =
1622        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1623    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1624
1625    let (worktree_id_a, worktree_id_b) = cx.read(|cx| {
1626        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1627        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1628    });
1629    workspace
1630        .update_in(cx, |workspace, window, cx| {
1631            workspace.open_abs_path(
1632                PathBuf::from(path!("/external/external-file.txt")),
1633                OpenOptions {
1634                    visible: Some(OpenVisible::None),
1635                    ..OpenOptions::default()
1636                },
1637                window,
1638                cx,
1639            )
1640        })
1641        .await
1642        .unwrap();
1643
1644    cx.run_until_parked();
1645    let finder = open_file_picker(&workspace, cx);
1646
1647    finder
1648        .update_in(cx, |f, window, cx| {
1649            f.delegate
1650                .spawn_search(test_path_position("new-file.txt"), window, cx)
1651        })
1652        .await;
1653
1654    cx.run_until_parked();
1655    finder.update_in(cx, |f, window, cx| {
1656        assert_eq!(f.delegate.matches.len(), 1);
1657        f.delegate.confirm(false, window, cx); // ✓ works
1658    });
1659    cx.run_until_parked();
1660
1661    cx.read(|cx| {
1662        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1663
1664        let project_path = active_editor.read(cx).active_project_path(cx);
1665
1666        assert!(
1667            project_path.is_some(),
1668            "Active editor should have a project path"
1669        );
1670
1671        let project_path = project_path.unwrap();
1672
1673        assert!(
1674            project_path.worktree_id == worktree_id_a || project_path.worktree_id == worktree_id_b,
1675            "New file should be created in one of the available worktrees (A or B), \
1676                not in a directory derived from the external file. Got worktree_id: {:?}",
1677            project_path.worktree_id
1678        );
1679
1680        assert_eq!(project_path.path.as_ref(), rel_path("new-file.txt"));
1681    });
1682}
1683
1684#[gpui::test]
1685async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppContext) {
1686    let app_state = init_test(cx);
1687    app_state
1688        .fs
1689        .as_fake()
1690        .insert_tree(
1691            path!("/roota"),
1692            json!({ "the-parent-dira": { "filea": "" } }),
1693        )
1694        .await;
1695
1696    app_state
1697        .fs
1698        .as_fake()
1699        .insert_tree(
1700            path!("/rootb"),
1701            json!({ "the-parent-dirb": { "fileb": "" } }),
1702        )
1703        .await;
1704
1705    let project = Project::test(
1706        app_state.fs.clone(),
1707        [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1708        cx,
1709    )
1710    .await;
1711
1712    let (multi_workspace, cx) =
1713        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1714    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1715    let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1716        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1717        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1718    });
1719
1720    let finder = open_file_picker(&workspace, cx);
1721
1722    finder
1723        .update_in(cx, |f, window, cx| {
1724            f.delegate
1725                .spawn_search(test_path_position(path!("rootb/filec")), window, cx)
1726        })
1727        .await;
1728    cx.run_until_parked();
1729    finder.update_in(cx, |picker, window, cx| {
1730        assert_eq!(picker.delegate.matches.len(), 1);
1731        picker.delegate.confirm(false, window, cx)
1732    });
1733    cx.run_until_parked();
1734    cx.read(|cx| {
1735        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1736        let project_path = active_editor.read(cx).active_project_path(cx);
1737        assert_eq!(
1738            project_path,
1739            Some(ProjectPath {
1740                worktree_id: worktree_id2,
1741                path: rel_path("filec").into()
1742            })
1743        );
1744    });
1745}
1746
1747#[gpui::test]
1748async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1749    let app_state = init_test(cx);
1750
1751    cx.update(|cx| {
1752        let settings = *ProjectPanelSettings::get_global(cx);
1753        ProjectPanelSettings::override_global(
1754            ProjectPanelSettings {
1755                hide_root: true,
1756                ..settings
1757            },
1758            cx,
1759        );
1760    });
1761
1762    app_state
1763        .fs
1764        .as_fake()
1765        .insert_tree(
1766            path!("/root"),
1767            json!({
1768                "dir1": { "a.txt": "" },
1769                "dir2": {
1770                    "a.txt": "",
1771                    "b.txt": ""
1772                }
1773            }),
1774        )
1775        .await;
1776
1777    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1778    let (multi_workspace, cx) =
1779        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1780    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1781
1782    let worktree_id = cx.read(|cx| {
1783        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1784        assert_eq!(worktrees.len(), 1);
1785        worktrees[0].read(cx).id()
1786    });
1787
1788    // When workspace has an active item, sort items which are closer to that item
1789    // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1790    // so that one should be sorted earlier
1791    let b_path = ProjectPath {
1792        worktree_id,
1793        path: rel_path("dir2/b.txt").into(),
1794    };
1795    workspace
1796        .update_in(cx, |workspace, window, cx| {
1797            workspace.open_path(b_path, None, true, window, cx)
1798        })
1799        .await
1800        .unwrap();
1801    let finder = open_file_picker(&workspace, cx);
1802    finder
1803        .update_in(cx, |f, window, cx| {
1804            f.delegate
1805                .spawn_search(test_path_position("a.txt"), window, cx)
1806        })
1807        .await;
1808
1809    finder.update(cx, |picker, _| {
1810        let matches = collect_search_matches(picker).search_paths_only();
1811        assert_eq!(matches[0].as_ref(), rel_path("dir2/a.txt"));
1812        assert_eq!(matches[1].as_ref(), rel_path("dir1/a.txt"));
1813    });
1814}
1815
1816#[gpui::test]
1817async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1818    let app_state = init_test(cx);
1819    app_state
1820        .fs
1821        .as_fake()
1822        .insert_tree(
1823            "/root",
1824            json!({
1825                "dir1": {},
1826                "dir2": {
1827                    "dir3": {}
1828                }
1829            }),
1830        )
1831        .await;
1832
1833    let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1834    let (picker, _workspace, cx) = build_find_picker(project, cx);
1835
1836    picker
1837        .update_in(cx, |f, window, cx| {
1838            f.delegate
1839                .spawn_search(test_path_position("dir"), window, cx)
1840        })
1841        .await;
1842    cx.read(|cx| {
1843        let finder = picker.read(cx);
1844        assert_eq!(finder.delegate.matches.len(), 1);
1845        assert_match_at_position(finder, 0, "dir");
1846    });
1847}
1848
1849#[gpui::test]
1850async fn test_query_history(cx: &mut gpui::TestAppContext) {
1851    let app_state = init_test(cx);
1852
1853    app_state
1854        .fs
1855        .as_fake()
1856        .insert_tree(
1857            path!("/src"),
1858            json!({
1859                "test": {
1860                    "first.rs": "// First Rust file",
1861                    "second.rs": "// Second Rust file",
1862                    "third.rs": "// Third Rust file",
1863                }
1864            }),
1865        )
1866        .await;
1867
1868    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1869    let (multi_workspace, cx) =
1870        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
1871    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1872    let worktree_id = cx.read(|cx| {
1873        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1874        assert_eq!(worktrees.len(), 1);
1875        worktrees[0].read(cx).id()
1876    });
1877
1878    // Open and close panels, getting their history items afterwards.
1879    // Ensure history items get populated with opened items, and items are kept in a certain order.
1880    // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1881    //
1882    // TODO: without closing, the opened items do not propagate their history changes for some reason
1883    // it does work in real app though, only tests do not propagate.
1884    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1885
1886    let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1887    assert!(
1888        initial_history.is_empty(),
1889        "Should have no history before opening any files"
1890    );
1891
1892    let history_after_first =
1893        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1894    assert_eq!(
1895        history_after_first,
1896        vec![FoundPath::new(
1897            ProjectPath {
1898                worktree_id,
1899                path: rel_path("test/first.rs").into(),
1900            },
1901            PathBuf::from(path!("/src/test/first.rs"))
1902        )],
1903        "Should show 1st opened item in the history when opening the 2nd item"
1904    );
1905
1906    let history_after_second =
1907        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1908    assert_eq!(
1909        history_after_second,
1910        vec![
1911            FoundPath::new(
1912                ProjectPath {
1913                    worktree_id,
1914                    path: rel_path("test/second.rs").into(),
1915                },
1916                PathBuf::from(path!("/src/test/second.rs"))
1917            ),
1918            FoundPath::new(
1919                ProjectPath {
1920                    worktree_id,
1921                    path: rel_path("test/first.rs").into(),
1922                },
1923                PathBuf::from(path!("/src/test/first.rs"))
1924            ),
1925        ],
1926        "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1927    2nd item should be the first in the history, as the last opened."
1928    );
1929
1930    let history_after_third =
1931        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1932    assert_eq!(
1933        history_after_third,
1934        vec![
1935            FoundPath::new(
1936                ProjectPath {
1937                    worktree_id,
1938                    path: rel_path("test/third.rs").into(),
1939                },
1940                PathBuf::from(path!("/src/test/third.rs"))
1941            ),
1942            FoundPath::new(
1943                ProjectPath {
1944                    worktree_id,
1945                    path: rel_path("test/second.rs").into(),
1946                },
1947                PathBuf::from(path!("/src/test/second.rs"))
1948            ),
1949            FoundPath::new(
1950                ProjectPath {
1951                    worktree_id,
1952                    path: rel_path("test/first.rs").into(),
1953                },
1954                PathBuf::from(path!("/src/test/first.rs"))
1955            ),
1956        ],
1957        "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1958    3rd item should be the first in the history, as the last opened."
1959    );
1960
1961    let history_after_second_again =
1962        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1963    assert_eq!(
1964        history_after_second_again,
1965        vec![
1966            FoundPath::new(
1967                ProjectPath {
1968                    worktree_id,
1969                    path: rel_path("test/second.rs").into(),
1970                },
1971                PathBuf::from(path!("/src/test/second.rs"))
1972            ),
1973            FoundPath::new(
1974                ProjectPath {
1975                    worktree_id,
1976                    path: rel_path("test/third.rs").into(),
1977                },
1978                PathBuf::from(path!("/src/test/third.rs"))
1979            ),
1980            FoundPath::new(
1981                ProjectPath {
1982                    worktree_id,
1983                    path: rel_path("test/first.rs").into(),
1984                },
1985                PathBuf::from(path!("/src/test/first.rs"))
1986            ),
1987        ],
1988        "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1989    2nd item, as the last opened, 3rd item should go next as it was opened right before."
1990    );
1991}
1992
1993#[gpui::test]
1994async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
1995    let app_state = init_test(cx);
1996
1997    cx.update(|cx| {
1998        let settings = *ProjectPanelSettings::get_global(cx);
1999        ProjectPanelSettings::override_global(
2000            ProjectPanelSettings {
2001                hide_root: true,
2002                ..settings
2003            },
2004            cx,
2005        );
2006    });
2007
2008    app_state
2009        .fs
2010        .as_fake()
2011        .insert_tree(
2012            path!("/src"),
2013            json!({
2014                "test": {
2015                    "first.rs": "// First Rust file",
2016                    "second.rs": "// Second Rust file",
2017                    "third.rs": "// Third Rust file",
2018                }
2019            }),
2020        )
2021        .await;
2022
2023    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2024    let (multi_workspace, cx) =
2025        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2026    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2027
2028    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
2029
2030    open_close_queried_buffer("efir", 1, "first.rs", &workspace, cx).await;
2031    let history = open_close_queried_buffer("second", 1, "second.rs", &workspace, cx).await;
2032    assert_eq!(history.len(), 1);
2033
2034    let picker = open_file_picker(&workspace, cx);
2035    simulate_input(cx, "fir");
2036    picker.update_in(cx, |finder, window, cx| {
2037        let matches = &finder.delegate.matches.matches;
2038        assert_matches!(
2039            matches.as_slice(),
2040            [Match::History { .. }, Match::CreateNew { .. }]
2041        );
2042        assert_eq!(
2043            matches[0].panel_match().unwrap().0.path.as_ref(),
2044            rel_path("test/first.rs")
2045        );
2046        assert_eq!(matches[0].panel_match().unwrap().0.positions, &[5, 6, 7]);
2047
2048        let (file_label, path_label) =
2049            finder
2050                .delegate
2051                .labels_for_match(&finder.delegate.matches.matches[0], window, cx);
2052        assert_eq!(file_label.text(), "first.rs");
2053        assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
2054        assert_eq!(
2055            path_label.text(),
2056            format!("test{}", PathStyle::local().primary_separator())
2057        );
2058        assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
2059    });
2060}
2061
2062#[gpui::test]
2063async fn test_history_labels_do_not_include_worktree_root_name(cx: &mut gpui::TestAppContext) {
2064    let app_state = init_test(cx);
2065
2066    cx.update(|cx| {
2067        let settings = *ProjectPanelSettings::get_global(cx);
2068        ProjectPanelSettings::override_global(
2069            ProjectPanelSettings {
2070                hide_root: true,
2071                ..settings
2072            },
2073            cx,
2074        );
2075    });
2076
2077    app_state
2078        .fs
2079        .as_fake()
2080        .insert_tree(
2081            path!("/my_project"),
2082            json!({
2083                "src": {
2084                    "first.rs": "// First Rust file",
2085                    "second.rs": "// Second Rust file",
2086                }
2087            }),
2088        )
2089        .await;
2090
2091    let project = Project::test(app_state.fs.clone(), [path!("/my_project").as_ref()], cx).await;
2092    let (multi_workspace, cx) =
2093        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2094    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2095
2096    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2097    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2098
2099    let picker = open_file_picker(&workspace, cx);
2100    picker.update_in(cx, |finder, window, cx| {
2101        let matches = &finder.delegate.matches.matches;
2102        assert!(matches.len() >= 2);
2103
2104        for m in matches.iter() {
2105            if let Match::History { panel_match, .. } = m {
2106                assert!(
2107                    panel_match.is_none(),
2108                    "History items with no query should not have a panel match"
2109                );
2110            }
2111        }
2112
2113        let separator = PathStyle::local().primary_separator();
2114
2115        let (file_label, path_label) = finder.delegate.labels_for_match(&matches[0], window, cx);
2116        assert_eq!(file_label.text(), "second.rs");
2117        assert_eq!(
2118            path_label.text(),
2119            format!("src{separator}"),
2120            "History path label must not contain root name 'my_project'"
2121        );
2122
2123        let (file_label, path_label) = finder.delegate.labels_for_match(&matches[1], window, cx);
2124        assert_eq!(file_label.text(), "first.rs");
2125        assert_eq!(
2126            path_label.text(),
2127            format!("src{separator}"),
2128            "History path label must not contain root name 'my_project'"
2129        );
2130    });
2131
2132    // Now type a query so history items get panel_match populated,
2133    // and verify labels stay consistent with the no-query case.
2134    let picker = active_file_picker(&workspace, cx);
2135    picker
2136        .update_in(cx, |finder, window, cx| {
2137            finder
2138                .delegate
2139                .update_matches("first".to_string(), window, cx)
2140        })
2141        .await;
2142    picker.update_in(cx, |finder, window, cx| {
2143        let matches = &finder.delegate.matches.matches;
2144        let history_match = matches
2145            .iter()
2146            .find(|m| matches!(m, Match::History { .. }))
2147            .expect("Should have a history match for 'first'");
2148
2149        let (file_label, path_label) = finder.delegate.labels_for_match(history_match, window, cx);
2150        assert_eq!(file_label.text(), "first.rs");
2151        let separator = PathStyle::local().primary_separator();
2152        assert_eq!(
2153            path_label.text(),
2154            format!("src{separator}"),
2155            "Queried history path label must not contain root name 'my_project'"
2156        );
2157    });
2158}
2159
2160#[gpui::test]
2161async fn test_history_labels_include_worktree_root_name_when_hide_root_false(
2162    cx: &mut gpui::TestAppContext,
2163) {
2164    let app_state = init_test(cx);
2165
2166    cx.update(|cx| {
2167        let settings = *ProjectPanelSettings::get_global(cx);
2168        ProjectPanelSettings::override_global(
2169            ProjectPanelSettings {
2170                hide_root: false,
2171                ..settings
2172            },
2173            cx,
2174        );
2175    });
2176
2177    app_state
2178        .fs
2179        .as_fake()
2180        .insert_tree(
2181            path!("/my_project"),
2182            json!({
2183                "src": {
2184                    "first.rs": "// First Rust file",
2185                    "second.rs": "// Second Rust file",
2186                }
2187            }),
2188        )
2189        .await;
2190
2191    let project = Project::test(app_state.fs.clone(), [path!("/my_project").as_ref()], cx).await;
2192    let (multi_workspace, cx) =
2193        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2194    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2195
2196    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2197    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2198
2199    let picker = open_file_picker(&workspace, cx);
2200    picker.update_in(cx, |finder, window, cx| {
2201        let matches = &finder.delegate.matches.matches;
2202        let separator = PathStyle::local().primary_separator();
2203
2204        let (_file_label, path_label) = finder.delegate.labels_for_match(&matches[0], window, cx);
2205        assert_eq!(
2206            path_label.text(),
2207            format!("my_project{separator}src{separator}"),
2208            "With hide_root=false, history path label should include root name 'my_project'"
2209        );
2210    });
2211}
2212
2213#[gpui::test]
2214async fn test_history_labels_include_worktree_root_name_when_hide_root_true_and_multiple_folders(
2215    cx: &mut gpui::TestAppContext,
2216) {
2217    let app_state = init_test(cx);
2218
2219    cx.update(|cx| {
2220        let settings = *ProjectPanelSettings::get_global(cx);
2221        ProjectPanelSettings::override_global(
2222            ProjectPanelSettings {
2223                hide_root: true,
2224                ..settings
2225            },
2226            cx,
2227        );
2228    });
2229
2230    app_state
2231        .fs
2232        .as_fake()
2233        .insert_tree(
2234            path!("/my_project"),
2235            json!({
2236                "src": {
2237                    "first.rs": "// First Rust file",
2238                    "second.rs": "// Second Rust file",
2239                }
2240            }),
2241        )
2242        .await;
2243
2244    app_state
2245        .fs
2246        .as_fake()
2247        .insert_tree(
2248            path!("/my_second_project"),
2249            json!({
2250                "src": {
2251                    "third.rs": "// Third Rust file",
2252                    "fourth.rs": "// Fourth Rust file",
2253                }
2254            }),
2255        )
2256        .await;
2257
2258    let project = Project::test(
2259        app_state.fs.clone(),
2260        [
2261            path!("/my_project").as_ref(),
2262            path!("/my_second_project").as_ref(),
2263        ],
2264        cx,
2265    )
2266    .await;
2267    let (multi_workspace, cx) =
2268        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2269    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2270
2271    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2272    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2273
2274    let picker = open_file_picker(&workspace, cx);
2275    picker.update_in(cx, |finder, window, cx| {
2276        let matches = &finder.delegate.matches.matches;
2277        assert!(matches.len() >= 2, "Should have at least 2 history matches");
2278
2279        let separator = PathStyle::local().primary_separator();
2280
2281        let first_match = matches
2282            .iter()
2283            .find(|m| {
2284                if let Match::History { path, .. } = m {
2285                    path.project.path.file_name()
2286                        .map(|n| n.to_string())
2287                        .map_or(false, |name| name == "first.rs")
2288                } else {
2289                    false
2290                }
2291            })
2292            .expect("Should have history match for first.rs");
2293
2294        let third_match = matches
2295            .iter()
2296            .find(|m| {
2297                if let Match::History { path, .. } = m {
2298                    path.project.path.file_name()
2299                        .map(|n| n.to_string())
2300                        .map_or(false, |name| name == "third.rs")
2301                } else {
2302                    false
2303                }
2304            })
2305            .expect("Should have history match for third.rs");
2306
2307        let (_file_label, path_label) =
2308            finder.delegate.labels_for_match(first_match, window, cx);
2309        assert_eq!(
2310            path_label.text(),
2311            format!("my_project{separator}src{separator}"),
2312            "With hide_root=true and multiple folders, history path label should include root name 'my_project'"
2313        );
2314
2315        let (_file_label, path_label) =
2316            finder.delegate.labels_for_match(third_match, window, cx);
2317        assert_eq!(
2318            path_label.text(),
2319            format!("my_second_project{separator}src{separator}"),
2320            "With hide_root=true and multiple folders, history path label should include root name 'my_second_project'"
2321        );
2322    });
2323}
2324
2325#[gpui::test]
2326async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
2327    let app_state = init_test(cx);
2328
2329    app_state
2330        .fs
2331        .as_fake()
2332        .insert_tree(
2333            path!("/src"),
2334            json!({
2335                "test": {
2336                    "first.rs": "// First Rust file",
2337                    "second.rs": "// Second Rust file",
2338                }
2339            }),
2340        )
2341        .await;
2342
2343    app_state
2344        .fs
2345        .as_fake()
2346        .insert_tree(
2347            path!("/external-src"),
2348            json!({
2349                "test": {
2350                    "third.rs": "// Third Rust file",
2351                    "fourth.rs": "// Fourth Rust file",
2352                }
2353            }),
2354        )
2355        .await;
2356
2357    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2358    cx.update(|cx| {
2359        project.update(cx, |project, cx| {
2360            project.find_or_create_worktree(path!("/external-src"), false, cx)
2361        })
2362    })
2363    .detach();
2364    cx.background_executor.run_until_parked();
2365
2366    let (multi_workspace, cx) =
2367        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2368    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2369    let worktree_id = cx.read(|cx| {
2370        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2371        assert_eq!(worktrees.len(), 1,);
2372
2373        worktrees[0].read(cx).id()
2374    });
2375    workspace
2376        .update_in(cx, |workspace, window, cx| {
2377            workspace.open_abs_path(
2378                PathBuf::from(path!("/external-src/test/third.rs")),
2379                OpenOptions {
2380                    visible: Some(OpenVisible::None),
2381                    ..Default::default()
2382                },
2383                window,
2384                cx,
2385            )
2386        })
2387        .detach();
2388    cx.background_executor.run_until_parked();
2389    let external_worktree_id = cx.read(|cx| {
2390        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2391        assert_eq!(
2392            worktrees.len(),
2393            2,
2394            "External file should get opened in a new worktree"
2395        );
2396
2397        worktrees
2398            .into_iter()
2399            .find(|worktree| worktree.read(cx).id() != worktree_id)
2400            .expect("New worktree should have a different id")
2401            .read(cx)
2402            .id()
2403    });
2404    cx.dispatch_action(workspace::CloseActiveItem {
2405        save_intent: None,
2406        close_pinned: false,
2407    });
2408
2409    let initial_history_items =
2410        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2411    assert_eq!(
2412        initial_history_items,
2413        vec![FoundPath::new(
2414            ProjectPath {
2415                worktree_id: external_worktree_id,
2416                path: rel_path("").into(),
2417            },
2418            PathBuf::from(path!("/external-src/test/third.rs"))
2419        )],
2420        "Should show external file with its full path in the history after it was open"
2421    );
2422
2423    let updated_history_items =
2424        open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2425    assert_eq!(
2426        updated_history_items,
2427        vec![
2428            FoundPath::new(
2429                ProjectPath {
2430                    worktree_id,
2431                    path: rel_path("test/second.rs").into(),
2432                },
2433                PathBuf::from(path!("/src/test/second.rs"))
2434            ),
2435            FoundPath::new(
2436                ProjectPath {
2437                    worktree_id: external_worktree_id,
2438                    path: rel_path("").into(),
2439                },
2440                PathBuf::from(path!("/external-src/test/third.rs"))
2441            ),
2442        ],
2443        "Should keep external file with history updates",
2444    );
2445}
2446
2447#[gpui::test]
2448async fn test_non_project_file_open_with_filter(cx: &mut gpui::TestAppContext) {
2449    let app_state = init_test(cx);
2450
2451    app_state
2452        .fs
2453        .as_fake()
2454        .insert_tree(
2455            path!("/project"),
2456            json!({
2457                "src": {
2458                    "main.rs": "fn main() {}",
2459                }
2460            }),
2461        )
2462        .await;
2463
2464    app_state
2465        .fs
2466        .as_fake()
2467        .insert_tree(path!("/external"), json!({ "notes.txt": "some notes" }))
2468        .await;
2469
2470    let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
2471    let (multi_workspace, cx) =
2472        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2473    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2474
2475    // Open the external file so it gets a single-file worktree and enters history.
2476    workspace
2477        .update_in(cx, |workspace, window, cx| {
2478            workspace.open_abs_path(
2479                PathBuf::from(path!("/external/notes.txt")),
2480                OpenOptions {
2481                    visible: Some(OpenVisible::None),
2482                    ..Default::default()
2483                },
2484                window,
2485                cx,
2486            )
2487        })
2488        .await
2489        .unwrap();
2490
2491    cx.run_until_parked();
2492
2493    let finder = open_file_picker(&workspace, cx);
2494    finder
2495        .update_in(cx, |f, window, cx| {
2496            f.delegate
2497                .spawn_search(test_path_position("notes"), window, cx)
2498        })
2499        .await;
2500    cx.run_until_parked();
2501
2502    finder.update(cx, |f, _| {
2503        let entries = collect_search_matches(f);
2504        assert_eq!(
2505            entries.search.len(),
2506            0,
2507            "External file should appear as a history match, not a search match"
2508        );
2509        assert_eq!(
2510            entries.history.len(),
2511            1,
2512            "Expected the external file in history matches"
2513        );
2514    });
2515
2516    // Confirming should open /external/notes.txt without a path-duplication error.
2517    // Explicitly select index 0: skip_focus_for_active_in_search would otherwise
2518    // auto-advance past the currently-open file to the CreateNew entry.
2519    finder.update_in(cx, |f, window, cx| {
2520        f.delegate.set_selected_index(0, window, cx);
2521        f.delegate.confirm(false, window, cx);
2522    });
2523    cx.run_until_parked();
2524
2525    cx.read(|cx| {
2526        let active_editor = workspace
2527            .read(cx)
2528            .active_item_as::<Editor>(cx)
2529            .expect("Should have an active editor after confirming");
2530        let abs_path = active_editor
2531            .read(cx)
2532            .buffer()
2533            .read(cx)
2534            .as_singleton()
2535            .and_then(|b| b.read(cx).file())
2536            .map(|f| f.full_path(cx));
2537        assert_eq!(
2538            abs_path.as_deref(),
2539            Some(Path::new(path!("/external/notes.txt"))),
2540            "Should open /external/notes.txt, not a duplicated path"
2541        );
2542    });
2543}
2544
2545#[gpui::test]
2546async fn test_non_project_file_matches_history_with_hidden_root(cx: &mut gpui::TestAppContext) {
2547    let app_state = init_test(cx);
2548
2549    cx.update(|cx| {
2550        let settings = *ProjectPanelSettings::get_global(cx);
2551        ProjectPanelSettings::override_global(
2552            ProjectPanelSettings {
2553                hide_root: true,
2554                ..settings
2555            },
2556            cx,
2557        );
2558    });
2559
2560    app_state
2561        .fs
2562        .as_fake()
2563        .insert_tree(
2564            path!("/project"),
2565            json!({
2566                "src": {
2567                    "main.rs": "fn main() {}",
2568                }
2569            }),
2570        )
2571        .await;
2572
2573    app_state
2574        .fs
2575        .as_fake()
2576        .insert_tree(path!("/external"), json!({ "notes.txt": "some notes" }))
2577        .await;
2578
2579    let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
2580    let (multi_workspace, cx) =
2581        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2582    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2583
2584    workspace
2585        .update_in(cx, |workspace, window, cx| {
2586            workspace.open_abs_path(
2587                PathBuf::from(path!("/external/notes.txt")),
2588                OpenOptions {
2589                    visible: Some(OpenVisible::None),
2590                    ..Default::default()
2591                },
2592                window,
2593                cx,
2594            )
2595        })
2596        .await
2597        .unwrap();
2598
2599    cx.run_until_parked();
2600
2601    let finder = open_file_picker(&workspace, cx);
2602
2603    finder
2604        .update_in(cx, |f, window, cx| {
2605            f.delegate
2606                .spawn_search(test_path_position("notes"), window, cx)
2607        })
2608        .await;
2609    cx.run_until_parked();
2610
2611    finder.update(cx, |f, _| {
2612        let entries = collect_search_matches(f);
2613        assert_eq!(
2614            entries.search.len(),
2615            0,
2616            "External file should appear as a history match, not a search match"
2617        );
2618        assert_eq!(
2619            entries.history.len(),
2620            1,
2621            "Expected the external file in history matches"
2622        );
2623    });
2624}
2625
2626#[gpui::test]
2627async fn test_single_file_search_result_split_open(cx: &mut gpui::TestAppContext) {
2628    let app_state = init_test(cx);
2629    app_state
2630        .fs
2631        .as_fake()
2632        .insert_tree(
2633            path!("/root"),
2634            json!({ "the-parent-dir": { "the-file": "" } }),
2635        )
2636        .await;
2637
2638    let project = Project::test(
2639        app_state.fs.clone(),
2640        [path!("/root/the-parent-dir/the-file").as_ref()],
2641        cx,
2642    )
2643    .await;
2644
2645    let (multi_workspace, cx) =
2646        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2647    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2648    let worktree_id = cx.read(|cx| {
2649        workspace
2650            .read(cx)
2651            .worktrees(cx)
2652            .next()
2653            .expect("Expected a single-file worktree")
2654            .read(cx)
2655            .id()
2656    });
2657    let finder = open_file_picker(&workspace, cx);
2658
2659    finder
2660        .update_in(cx, |finder, window, cx| {
2661            finder
2662                .delegate
2663                .spawn_search(test_path_position("thf"), window, cx)
2664        })
2665        .await;
2666    cx.run_until_parked();
2667
2668    finder.update(cx, |finder, _| {
2669        let matches = collect_search_matches(finder);
2670        assert_eq!(matches.history.len(), 0);
2671        assert_eq!(matches.search.len(), 1);
2672    });
2673
2674    cx.dispatch_action(pane::SplitRight::default());
2675    cx.run_until_parked();
2676
2677    cx.read(|cx| {
2678        let active_editor = workspace
2679            .read(cx)
2680            .active_item_as::<Editor>(cx)
2681            .expect("Should have an active editor after splitting the search result");
2682        assert_eq!(
2683            active_editor.read(cx).active_project_path(cx),
2684            Some(ProjectPath {
2685                worktree_id,
2686                path: RelPath::empty_arc(),
2687            }),
2688            "Should split-open the single-file worktree root with an empty relative path"
2689        );
2690    });
2691}
2692
2693#[gpui::test]
2694async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
2695    let app_state = init_test(cx);
2696
2697    app_state
2698        .fs
2699        .as_fake()
2700        .insert_tree(
2701            path!("/src"),
2702            json!({
2703                "test": {
2704                    "first.rs": "// First Rust file",
2705                    "second.rs": "// Second Rust file",
2706                    "third.rs": "// Third Rust file",
2707                }
2708            }),
2709        )
2710        .await;
2711
2712    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2713    let (multi_workspace, cx) =
2714        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2715    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2716
2717    // generate some history to select from
2718    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2719    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2720    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2721    let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2722
2723    for expected_selected_index in 0..current_history.len() {
2724        cx.dispatch_action(ToggleFileFinder::default());
2725        let picker = active_file_picker(&workspace, cx);
2726        let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
2727        assert_eq!(
2728            selected_index, expected_selected_index,
2729            "Should select the next item in the history"
2730        );
2731    }
2732
2733    cx.dispatch_action(ToggleFileFinder::default());
2734    let selected_index = workspace.update(cx, |workspace, cx| {
2735        workspace
2736            .active_modal::<FileFinder>(cx)
2737            .unwrap()
2738            .read(cx)
2739            .picker
2740            .read(cx)
2741            .delegate
2742            .selected_index()
2743    });
2744    assert_eq!(
2745        selected_index, 0,
2746        "Should wrap around the history and start all over"
2747    );
2748}
2749
2750#[gpui::test]
2751async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
2752    let app_state = init_test(cx);
2753
2754    cx.update(|cx| {
2755        let settings = *ProjectPanelSettings::get_global(cx);
2756        ProjectPanelSettings::override_global(
2757            ProjectPanelSettings {
2758                hide_root: true,
2759                ..settings
2760            },
2761            cx,
2762        );
2763    });
2764
2765    app_state
2766        .fs
2767        .as_fake()
2768        .insert_tree(
2769            path!("/src"),
2770            json!({
2771                "test": {
2772                    "first.rs": "// First Rust file",
2773                    "second.rs": "// Second Rust file",
2774                    "third.rs": "// Third Rust file",
2775                    "fourth.rs": "// Fourth Rust file",
2776                }
2777            }),
2778        )
2779        .await;
2780
2781    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2782    let (multi_workspace, cx) =
2783        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2784    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2785    let worktree_id = cx.read(|cx| {
2786        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2787        assert_eq!(worktrees.len(), 1,);
2788
2789        worktrees[0].read(cx).id()
2790    });
2791
2792    // generate some history to select from
2793    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2794    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2795    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2796    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2797
2798    let finder = open_file_picker(&workspace, cx);
2799    let first_query = "f";
2800    finder
2801        .update_in(cx, |finder, window, cx| {
2802            finder
2803                .delegate
2804                .update_matches(first_query.to_string(), window, cx)
2805        })
2806        .await;
2807    finder.update(cx, |picker, _| {
2808            let matches = collect_search_matches(picker);
2809            assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
2810            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
2811            assert_eq!(history_match, &FoundPath::new(
2812                ProjectPath {
2813                    worktree_id,
2814                    path: rel_path("test/first.rs").into(),
2815                },
2816                PathBuf::from(path!("/src/test/first.rs")),
2817            ));
2818            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
2819            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
2820        });
2821
2822    let second_query = "fsdasdsa";
2823    let finder = active_file_picker(&workspace, cx);
2824    finder
2825        .update_in(cx, |finder, window, cx| {
2826            finder
2827                .delegate
2828                .update_matches(second_query.to_string(), window, cx)
2829        })
2830        .await;
2831    finder.update(cx, |picker, _| {
2832        assert!(
2833            collect_search_matches(picker)
2834                .search_paths_only()
2835                .is_empty(),
2836            "No search entries should match {second_query}"
2837        );
2838    });
2839
2840    let first_query_again = first_query;
2841
2842    let finder = active_file_picker(&workspace, cx);
2843    finder
2844        .update_in(cx, |finder, window, cx| {
2845            finder
2846                .delegate
2847                .update_matches(first_query_again.to_string(), window, cx)
2848        })
2849        .await;
2850    finder.update(cx, |picker, _| {
2851            let matches = collect_search_matches(picker);
2852            assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
2853            let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
2854            assert_eq!(history_match, &FoundPath::new(
2855                ProjectPath {
2856                    worktree_id,
2857                    path: rel_path("test/first.rs").into(),
2858                },
2859                PathBuf::from(path!("/src/test/first.rs"))
2860            ));
2861            assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
2862            assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
2863        });
2864}
2865
2866#[gpui::test]
2867async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
2868    let app_state = init_test(cx);
2869
2870    cx.update(|cx| {
2871        let settings = *ProjectPanelSettings::get_global(cx);
2872        ProjectPanelSettings::override_global(
2873            ProjectPanelSettings {
2874                hide_root: true,
2875                ..settings
2876            },
2877            cx,
2878        );
2879    });
2880
2881    app_state
2882        .fs
2883        .as_fake()
2884        .insert_tree(
2885            path!("/root"),
2886            json!({
2887                "test": {
2888                    "1_qw": "// First file that matches the query",
2889                    "2_second": "// Second file",
2890                    "3_third": "// Third file",
2891                    "4_fourth": "// Fourth file",
2892                    "5_qwqwqw": "// A file with 3 more matches than the first one",
2893                    "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
2894                    "7_qwqwqw": "// One more, same amount of query matches as above",
2895                }
2896            }),
2897        )
2898        .await;
2899
2900    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2901    let (multi_workspace, cx) =
2902        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2903    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2904    // generate some history to select from
2905    open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
2906    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
2907    open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
2908    open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
2909    open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
2910
2911    let finder = open_file_picker(&workspace, cx);
2912    let query = "qw";
2913    finder
2914        .update_in(cx, |finder, window, cx| {
2915            finder
2916                .delegate
2917                .update_matches(query.to_string(), window, cx)
2918        })
2919        .await;
2920    finder.update(cx, |finder, _| {
2921        let search_matches = collect_search_matches(finder);
2922        assert_eq!(
2923            search_matches.history,
2924            vec![
2925                rel_path("test/1_qw").into(),
2926                rel_path("test/6_qwqwqw").into()
2927            ],
2928        );
2929        assert_eq!(
2930            search_matches.search,
2931            vec![
2932                rel_path("test/5_qwqwqw").into(),
2933                rel_path("test/7_qwqwqw").into()
2934            ],
2935        );
2936    });
2937}
2938
2939#[gpui::test]
2940async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
2941    let app_state = init_test(cx);
2942
2943    app_state
2944        .fs
2945        .as_fake()
2946        .insert_tree(
2947            path!("/root"),
2948            json!({
2949                "test": {
2950                    "1_qw": "",
2951                }
2952            }),
2953        )
2954        .await;
2955
2956    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2957    let (multi_workspace, cx) =
2958        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2959    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2960    // Open new buffer
2961    open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
2962
2963    let picker = open_file_picker(&workspace, cx);
2964    picker.update(cx, |finder, _| {
2965        assert_match_selection(finder, 0, "1_qw");
2966    });
2967}
2968
2969#[gpui::test]
2970async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
2971    cx: &mut TestAppContext,
2972) {
2973    let app_state = init_test(cx);
2974
2975    app_state
2976        .fs
2977        .as_fake()
2978        .insert_tree(
2979            path!("/src"),
2980            json!({
2981                "test": {
2982                    "bar.rs": "// Bar file",
2983                    "lib.rs": "// Lib file",
2984                    "maaa.rs": "// Maaaaaaa",
2985                    "main.rs": "// Main file",
2986                    "moo.rs": "// Moooooo",
2987                }
2988            }),
2989        )
2990        .await;
2991
2992    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2993    let (multi_workspace, cx) =
2994        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2995    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2996
2997    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2998    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2999    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
3000
3001    // main.rs is on top, previously used is selected
3002    let picker = open_file_picker(&workspace, cx);
3003    picker.update(cx, |finder, _| {
3004        assert_eq!(finder.delegate.matches.len(), 3);
3005        assert_match_selection(finder, 0, "main.rs");
3006        assert_match_at_position(finder, 1, "lib.rs");
3007        assert_match_at_position(finder, 2, "bar.rs");
3008    });
3009
3010    // all files match, main.rs is still on top, but the second item is selected
3011    picker
3012        .update_in(cx, |finder, window, cx| {
3013            finder
3014                .delegate
3015                .update_matches(".rs".to_string(), window, cx)
3016        })
3017        .await;
3018    picker.update(cx, |finder, _| {
3019        assert_eq!(finder.delegate.matches.len(), 6);
3020        assert_match_at_position(finder, 0, "main.rs");
3021        assert_match_selection(finder, 1, "bar.rs");
3022        assert_match_at_position(finder, 2, "lib.rs");
3023        assert_match_at_position(finder, 3, "moo.rs");
3024        assert_match_at_position(finder, 4, "maaa.rs");
3025        assert_match_at_position(finder, 5, ".rs");
3026    });
3027
3028    // main.rs is not among matches, select top item
3029    picker
3030        .update_in(cx, |finder, window, cx| {
3031            finder.delegate.update_matches("b".to_string(), window, cx)
3032        })
3033        .await;
3034    picker.update(cx, |finder, _| {
3035        assert_eq!(finder.delegate.matches.len(), 3);
3036        assert_match_at_position(finder, 0, "bar.rs");
3037        assert_match_at_position(finder, 1, "lib.rs");
3038        assert_match_at_position(finder, 2, "b");
3039    });
3040
3041    // main.rs is back, put it on top and select next item
3042    picker
3043        .update_in(cx, |finder, window, cx| {
3044            finder.delegate.update_matches("m".to_string(), window, cx)
3045        })
3046        .await;
3047    picker.update(cx, |finder, _| {
3048        assert_eq!(finder.delegate.matches.len(), 4);
3049        assert_match_at_position(finder, 0, "main.rs");
3050        assert_match_selection(finder, 1, "moo.rs");
3051        assert_match_at_position(finder, 2, "maaa.rs");
3052        assert_match_at_position(finder, 3, "m");
3053    });
3054
3055    // get back to the initial state
3056    picker
3057        .update_in(cx, |finder, window, cx| {
3058            finder.delegate.update_matches("".to_string(), window, cx)
3059        })
3060        .await;
3061    picker.update(cx, |finder, _| {
3062        assert_eq!(finder.delegate.matches.len(), 3);
3063        assert_match_selection(finder, 0, "main.rs");
3064        assert_match_at_position(finder, 1, "lib.rs");
3065        assert_match_at_position(finder, 2, "bar.rs");
3066    });
3067}
3068
3069#[gpui::test]
3070async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
3071    let app_state = init_test(cx);
3072
3073    cx.update(|cx| {
3074        let settings = *FileFinderSettings::get_global(cx);
3075
3076        FileFinderSettings::override_global(
3077            FileFinderSettings {
3078                skip_focus_for_active_in_search: false,
3079                ..settings
3080            },
3081            cx,
3082        );
3083    });
3084
3085    app_state
3086        .fs
3087        .as_fake()
3088        .insert_tree(
3089            path!("/src"),
3090            json!({
3091                "test": {
3092                    "bar.rs": "// Bar file",
3093                    "lib.rs": "// Lib file",
3094                    "maaa.rs": "// Maaaaaaa",
3095                    "main.rs": "// Main file",
3096                    "moo.rs": "// Moooooo",
3097                }
3098            }),
3099        )
3100        .await;
3101
3102    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3103    let (multi_workspace, cx) =
3104        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3105    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3106
3107    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
3108    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
3109    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
3110
3111    // main.rs is on top, previously used is selected
3112    let picker = open_file_picker(&workspace, cx);
3113    picker.update(cx, |finder, _| {
3114        assert_eq!(finder.delegate.matches.len(), 3);
3115        assert_match_selection(finder, 0, "main.rs");
3116        assert_match_at_position(finder, 1, "lib.rs");
3117        assert_match_at_position(finder, 2, "bar.rs");
3118    });
3119
3120    // all files match, main.rs is on top, and is selected
3121    picker
3122        .update_in(cx, |finder, window, cx| {
3123            finder
3124                .delegate
3125                .update_matches(".rs".to_string(), window, cx)
3126        })
3127        .await;
3128    picker.update(cx, |finder, _| {
3129        assert_eq!(finder.delegate.matches.len(), 6);
3130        assert_match_selection(finder, 0, "main.rs");
3131        assert_match_at_position(finder, 1, "bar.rs");
3132        assert_match_at_position(finder, 2, "lib.rs");
3133        assert_match_at_position(finder, 3, "moo.rs");
3134        assert_match_at_position(finder, 4, "maaa.rs");
3135        assert_match_at_position(finder, 5, ".rs");
3136    });
3137}
3138
3139#[gpui::test]
3140async fn test_non_separate_history_items(cx: &mut TestAppContext) {
3141    let app_state = init_test(cx);
3142
3143    app_state
3144        .fs
3145        .as_fake()
3146        .insert_tree(
3147            path!("/src"),
3148            json!({
3149                "test": {
3150                    "bar.rs": "// Bar file",
3151                    "lib.rs": "// Lib file",
3152                    "maaa.rs": "// Maaaaaaa",
3153                    "main.rs": "// Main file",
3154                    "moo.rs": "// Moooooo",
3155                }
3156            }),
3157        )
3158        .await;
3159
3160    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3161    let (multi_workspace, cx) =
3162        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3163    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3164
3165    open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
3166    open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
3167    open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
3168
3169    cx.dispatch_action(ToggleFileFinder::default());
3170    let picker = active_file_picker(&workspace, cx);
3171    // main.rs is on top, previously used is selected
3172    picker.update(cx, |finder, _| {
3173        assert_eq!(finder.delegate.matches.len(), 3);
3174        assert_match_selection(finder, 0, "main.rs");
3175        assert_match_at_position(finder, 1, "lib.rs");
3176        assert_match_at_position(finder, 2, "bar.rs");
3177    });
3178
3179    // all files match, main.rs is still on top, but the second item is selected
3180    picker
3181        .update_in(cx, |finder, window, cx| {
3182            finder
3183                .delegate
3184                .update_matches(".rs".to_string(), window, cx)
3185        })
3186        .await;
3187    picker.update(cx, |finder, _| {
3188        assert_eq!(finder.delegate.matches.len(), 6);
3189        assert_match_at_position(finder, 0, "main.rs");
3190        assert_match_selection(finder, 1, "moo.rs");
3191        assert_match_at_position(finder, 2, "bar.rs");
3192        assert_match_at_position(finder, 3, "lib.rs");
3193        assert_match_at_position(finder, 4, "maaa.rs");
3194        assert_match_at_position(finder, 5, ".rs");
3195    });
3196
3197    // main.rs is not among matches, select top item
3198    picker
3199        .update_in(cx, |finder, window, cx| {
3200            finder.delegate.update_matches("b".to_string(), window, cx)
3201        })
3202        .await;
3203    picker.update(cx, |finder, _| {
3204        assert_eq!(finder.delegate.matches.len(), 3);
3205        assert_match_at_position(finder, 0, "bar.rs");
3206        assert_match_at_position(finder, 1, "lib.rs");
3207        assert_match_at_position(finder, 2, "b");
3208    });
3209
3210    // main.rs is back, put it on top and select next item
3211    picker
3212        .update_in(cx, |finder, window, cx| {
3213            finder.delegate.update_matches("m".to_string(), window, cx)
3214        })
3215        .await;
3216    picker.update(cx, |finder, _| {
3217        assert_eq!(finder.delegate.matches.len(), 4);
3218        assert_match_at_position(finder, 0, "main.rs");
3219        assert_match_selection(finder, 1, "moo.rs");
3220        assert_match_at_position(finder, 2, "maaa.rs");
3221        assert_match_at_position(finder, 3, "m");
3222    });
3223
3224    // get back to the initial state
3225    picker
3226        .update_in(cx, |finder, window, cx| {
3227            finder.delegate.update_matches("".to_string(), window, cx)
3228        })
3229        .await;
3230    picker.update(cx, |finder, _| {
3231        assert_eq!(finder.delegate.matches.len(), 3);
3232        assert_match_selection(finder, 0, "main.rs");
3233        assert_match_at_position(finder, 1, "lib.rs");
3234        assert_match_at_position(finder, 2, "bar.rs");
3235    });
3236}
3237
3238#[gpui::test]
3239async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
3240    let app_state = init_test(cx);
3241
3242    app_state
3243        .fs
3244        .as_fake()
3245        .insert_tree(
3246            path!("/test"),
3247            json!({
3248                "test": {
3249                    "1.txt": "// One",
3250                    "2.txt": "// Two",
3251                    "3.txt": "// Three",
3252                }
3253            }),
3254        )
3255        .await;
3256
3257    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3258    let (multi_workspace, cx) =
3259        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3260    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3261
3262    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3263    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3264    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
3265
3266    let picker = open_file_picker(&workspace, cx);
3267    picker.update(cx, |finder, _| {
3268        assert_eq!(finder.delegate.matches.len(), 3);
3269        assert_match_selection(finder, 0, "3.txt");
3270        assert_match_at_position(finder, 1, "2.txt");
3271        assert_match_at_position(finder, 2, "1.txt");
3272    });
3273
3274    cx.dispatch_action(SelectNext);
3275    cx.dispatch_action(Confirm); // Open 2.txt
3276
3277    let picker = open_file_picker(&workspace, cx);
3278    picker.update(cx, |finder, _| {
3279        assert_eq!(finder.delegate.matches.len(), 3);
3280        assert_match_selection(finder, 0, "2.txt");
3281        assert_match_at_position(finder, 1, "3.txt");
3282        assert_match_at_position(finder, 2, "1.txt");
3283    });
3284
3285    cx.dispatch_action(SelectNext);
3286    cx.dispatch_action(SelectNext);
3287    cx.dispatch_action(Confirm); // Open 1.txt
3288
3289    let picker = open_file_picker(&workspace, cx);
3290    picker.update(cx, |finder, _| {
3291        assert_eq!(finder.delegate.matches.len(), 3);
3292        assert_match_selection(finder, 0, "1.txt");
3293        assert_match_at_position(finder, 1, "2.txt");
3294        assert_match_at_position(finder, 2, "3.txt");
3295    });
3296}
3297
3298#[gpui::test]
3299async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
3300    let app_state = init_test(cx);
3301
3302    app_state
3303        .fs
3304        .as_fake()
3305        .insert_tree(
3306            path!("/test"),
3307            json!({
3308                "test": {
3309                    "1.txt": "// One",
3310                    "2.txt": "// Two",
3311                    "3.txt": "// Three",
3312                }
3313            }),
3314        )
3315        .await;
3316
3317    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
3318    let (multi_workspace, cx) =
3319        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3320    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3321
3322    open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
3323    open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
3324    open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
3325
3326    let picker = open_file_picker(&workspace, cx);
3327    picker.update(cx, |finder, _| {
3328        assert_eq!(finder.delegate.matches.len(), 3);
3329        assert_match_selection(finder, 0, "3.txt");
3330        assert_match_at_position(finder, 1, "2.txt");
3331        assert_match_at_position(finder, 2, "1.txt");
3332    });
3333
3334    cx.dispatch_action(SelectNext);
3335
3336    // Add more files to the worktree to trigger update matches
3337    for i in 0..5 {
3338        let filename = if cfg!(windows) {
3339            format!("C:/test/{}.txt", 4 + i)
3340        } else {
3341            format!("/test/{}.txt", 4 + i)
3342        };
3343        app_state
3344            .fs
3345            .create_file(Path::new(&filename), Default::default())
3346            .await
3347            .expect("unable to create file");
3348    }
3349
3350    advance_worktree_update_refresh(cx);
3351
3352    picker.update(cx, |finder, _| {
3353        assert_eq!(finder.delegate.matches.len(), 3);
3354        assert_match_at_position(finder, 0, "3.txt");
3355        assert_match_selection(finder, 1, "2.txt");
3356        assert_match_at_position(finder, 2, "1.txt");
3357    });
3358}
3359
3360#[gpui::test]
3361async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
3362    let app_state = init_test(cx);
3363
3364    cx.update(|cx| {
3365        let settings = *ProjectPanelSettings::get_global(cx);
3366        ProjectPanelSettings::override_global(
3367            ProjectPanelSettings {
3368                hide_root: true,
3369                ..settings
3370            },
3371            cx,
3372        );
3373    });
3374
3375    app_state
3376        .fs
3377        .as_fake()
3378        .insert_tree(
3379            path!("/src"),
3380            json!({
3381                "collab_ui": {
3382                    "first.rs": "// First Rust file",
3383                    "second.rs": "// Second Rust file",
3384                    "third.rs": "// Third Rust file",
3385                    "collab_ui.rs": "// Fourth Rust file",
3386                }
3387            }),
3388        )
3389        .await;
3390
3391    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3392    let (multi_workspace, cx) =
3393        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3394    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3395    // generate some history to select from
3396    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
3397    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
3398    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
3399    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
3400
3401    let finder = open_file_picker(&workspace, cx);
3402    let query = "collab_ui";
3403    simulate_input(cx, query);
3404    finder.update(cx, |picker, _| {
3405            let search_entries = collect_search_matches(picker).search_paths_only();
3406            assert_eq!(
3407                search_entries,
3408                vec![
3409                    rel_path("collab_ui/collab_ui.rs").into(),
3410                    rel_path("collab_ui/first.rs").into(),
3411                    rel_path("collab_ui/third.rs").into(),
3412                    rel_path("collab_ui/second.rs").into(),
3413                ],
3414                "Despite all search results having the same directory name, the most matching one should be on top"
3415            );
3416        });
3417}
3418
3419#[gpui::test]
3420async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
3421    let app_state = init_test(cx);
3422
3423    cx.update(|cx| {
3424        let settings = *ProjectPanelSettings::get_global(cx);
3425        ProjectPanelSettings::override_global(
3426            ProjectPanelSettings {
3427                hide_root: true,
3428                ..settings
3429            },
3430            cx,
3431        );
3432    });
3433
3434    app_state
3435        .fs
3436        .as_fake()
3437        .insert_tree(
3438            path!("/src"),
3439            json!({
3440                "test": {
3441                    "first.rs": "// First Rust file",
3442                    "nonexistent.rs": "// Second Rust file",
3443                    "third.rs": "// Third Rust file",
3444                }
3445            }),
3446        )
3447        .await;
3448
3449    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3450    let (multi_workspace, cx) =
3451        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); // generate some history to select from
3452    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3453    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
3454    open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
3455    open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
3456    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
3457    app_state
3458        .fs
3459        .remove_file(
3460            Path::new(path!("/src/test/nonexistent.rs")),
3461            RemoveOptions::default(),
3462        )
3463        .await
3464        .unwrap();
3465    cx.run_until_parked();
3466
3467    let picker = open_file_picker(&workspace, cx);
3468    simulate_input(cx, "rs");
3469
3470    picker.update(cx, |picker, _| {
3471        assert_eq!(
3472            collect_search_matches(picker).history,
3473            vec![
3474                rel_path("test/first.rs").into(),
3475                rel_path("test/third.rs").into()
3476            ],
3477            "Should have all opened files in the history, except the ones that do not exist on disk"
3478        );
3479    });
3480}
3481
3482#[gpui::test]
3483async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
3484    let app_state = init_test(cx);
3485
3486    app_state
3487        .fs
3488        .as_fake()
3489        .insert_tree(
3490            "/src",
3491            json!({
3492                "lib.rs": "// Lib file",
3493                "main.rs": "// Bar file",
3494                "read.me": "// Readme file",
3495            }),
3496        )
3497        .await;
3498
3499    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
3500    let (multi_workspace, cx) =
3501        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3502    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3503
3504    // Initial state
3505    let picker = open_file_picker(&workspace, cx);
3506    simulate_input(cx, "rs");
3507    picker.update(cx, |finder, _| {
3508        assert_eq!(finder.delegate.matches.len(), 3);
3509        assert_match_at_position(finder, 0, "lib.rs");
3510        assert_match_at_position(finder, 1, "main.rs");
3511        assert_match_at_position(finder, 2, "rs");
3512    });
3513    // Delete main.rs
3514    app_state
3515        .fs
3516        .remove_file("/src/main.rs".as_ref(), Default::default())
3517        .await
3518        .expect("unable to remove file");
3519    advance_worktree_update_refresh(cx);
3520
3521    // main.rs is in not among search results anymore
3522    picker.update(cx, |finder, _| {
3523        assert_eq!(finder.delegate.matches.len(), 2);
3524        assert_match_at_position(finder, 0, "lib.rs");
3525        assert_match_at_position(finder, 1, "rs");
3526    });
3527
3528    // Create util.rs
3529    app_state
3530        .fs
3531        .create_file("/src/util.rs".as_ref(), Default::default())
3532        .await
3533        .expect("unable to create file");
3534    advance_worktree_update_refresh(cx);
3535
3536    // util.rs is among search results
3537    picker.update(cx, |finder, _| {
3538        assert_eq!(finder.delegate.matches.len(), 3);
3539        assert_match_at_position(finder, 0, "lib.rs");
3540        assert_match_at_position(finder, 1, "util.rs");
3541        assert_match_at_position(finder, 2, "rs");
3542    });
3543}
3544
3545#[gpui::test]
3546async fn test_worktree_entry_updates_are_coalesced(cx: &mut gpui::TestAppContext) {
3547    let app_state = init_test(cx);
3548
3549    app_state
3550        .fs
3551        .as_fake()
3552        .insert_tree(
3553            "/src",
3554            json!({
3555                "lib.rs": "// Lib file",
3556            }),
3557        )
3558        .await;
3559
3560    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
3561    let (picker, _, cx) = build_find_picker(project, cx);
3562
3563    simulate_input(cx, "rs");
3564    let initial_search_count = picker.read_with(cx, |picker, _| picker.delegate.search_count);
3565
3566    for filename in ["one.rs", "two.rs", "three.rs"] {
3567        app_state
3568            .fs
3569            .create_file(Path::new(&format!("/src/{filename}")), Default::default())
3570            .await
3571            .expect("unable to create file");
3572        cx.executor().advance_clock(FS_WATCH_LATENCY);
3573        cx.run_until_parked();
3574    }
3575
3576    cx.executor()
3577        .advance_clock(WORKTREE_UPDATE_REFRESH_DEBOUNCE);
3578    cx.run_until_parked();
3579
3580    picker.update(cx, |picker, _| {
3581        assert_eq!(
3582            picker.delegate.search_count,
3583            initial_search_count + 1,
3584            "bursty worktree entry updates should be coalesced into one search refresh"
3585        );
3586        assert_eq!(picker.delegate.matches.len(), 5);
3587    });
3588}
3589
3590#[gpui::test]
3591async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui::TestAppContext) {
3592    let app_state = init_test(cx);
3593
3594    app_state
3595        .fs
3596        .as_fake()
3597        .insert_tree(
3598            "/src",
3599            json!({
3600                "lib.rs": "// Lib file",
3601                "main.rs": "// Bar file",
3602                "read.me": "// Readme file",
3603            }),
3604        )
3605        .await;
3606    app_state
3607        .fs
3608        .as_fake()
3609        .insert_tree(
3610            "/test",
3611            json!({
3612                "new.rs": "// New file",
3613            }),
3614        )
3615        .await;
3616
3617    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
3618    let window = cx.add_window({
3619        let project = project.clone();
3620        |window, cx| MultiWorkspace::test_new(project, window, cx)
3621    });
3622    let cx = VisualTestContext::from_window(*window, cx).into_mut();
3623    let workspace = window
3624        .read_with(cx, |mw, _| mw.workspace().clone())
3625        .unwrap();
3626
3627    cx.update(|_, cx| {
3628        open_paths(
3629            &[PathBuf::from(path!("/test/new.rs"))],
3630            app_state,
3631            workspace::OpenOptions::default(),
3632            cx,
3633        )
3634    })
3635    .await
3636    .unwrap();
3637    assert_eq!(cx.update(|_, cx| cx.windows().len()), 1);
3638
3639    // Verify the standalone file appears as a history match when filtered. Because new.rs IS the
3640    // currently-open file and skip_focus_for_active_in_search is enabled, confirming would skip
3641    // it. Close the finder without confirming and use CloseActiveItem to close the file instead.
3642    let initial_history = {
3643        let picker = open_file_picker(&workspace, cx);
3644        simulate_input(cx, "new");
3645        let history_items = picker.update(cx, |finder, _| {
3646            assert_eq!(
3647                finder.delegate.matches.len(),
3648                2, // 1 history match + 1 CreateNew
3649                "Unexpected number of matches found for query `new`, matches: {:?}",
3650                finder.delegate.matches
3651            );
3652            let entries = collect_search_matches(finder);
3653            assert_eq!(entries.history.len(), 1, "new.rs should be a history match");
3654            assert_eq!(
3655                entries.search.len(),
3656                0,
3657                "new.rs should not be a plain search match"
3658            );
3659            finder.delegate.history_items.clone()
3660        });
3661        cx.dispatch_action(Cancel);
3662        history_items
3663    };
3664    assert_eq!(
3665        initial_history.first().unwrap().absolute,
3666        PathBuf::from(path!("/test/new.rs")),
3667        "Should show 1st opened item in the history when opening the 2nd item"
3668    );
3669
3670    cx.dispatch_action(CloseActiveItem {
3671        save_intent: None,
3672        close_pinned: false,
3673    });
3674    cx.run_until_parked();
3675
3676    let history_after_first = open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
3677    assert_eq!(
3678        history_after_first.first().unwrap().absolute,
3679        PathBuf::from(path!("/test/new.rs")),
3680        "Should show 1st opened item in the history when opening the 2nd item"
3681    );
3682}
3683
3684#[gpui::test]
3685async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
3686    cx: &mut gpui::TestAppContext,
3687) {
3688    let app_state = init_test(cx);
3689
3690    app_state
3691        .fs
3692        .as_fake()
3693        .insert_tree(
3694            "/test",
3695            json!({
3696                "project_1": {
3697                    "bar.rs": "// Bar file",
3698                    "lib.rs": "// Lib file",
3699                },
3700                "project_2": {
3701                    "Cargo.toml": "// Cargo file",
3702                    "main.rs": "// Main file",
3703                }
3704            }),
3705        )
3706        .await;
3707
3708    let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
3709    let (multi_workspace, cx) =
3710        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3711    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3712    let worktree_1_id = project.update(cx, |project, cx| {
3713        let worktree = project.worktrees(cx).last().expect("worktree not found");
3714        worktree.read(cx).id()
3715    });
3716
3717    // Initial state
3718    let picker = open_file_picker(&workspace, cx);
3719    simulate_input(cx, "rs");
3720    picker.update(cx, |finder, _| {
3721        assert_eq!(finder.delegate.matches.len(), 3);
3722        assert_match_at_position(finder, 0, "bar.rs");
3723        assert_match_at_position(finder, 1, "lib.rs");
3724        assert_match_at_position(finder, 2, "rs");
3725    });
3726
3727    // Add new worktree
3728    project
3729        .update(cx, |project, cx| {
3730            project
3731                .find_or_create_worktree("/test/project_2", true, cx)
3732                .into_future()
3733        })
3734        .await
3735        .expect("unable to create workdir");
3736    advance_worktree_update_refresh(cx);
3737
3738    // main.rs is among search results
3739    picker.update(cx, |finder, _| {
3740        assert_eq!(finder.delegate.matches.len(), 4);
3741        assert_match_at_position(finder, 0, "bar.rs");
3742        assert_match_at_position(finder, 1, "lib.rs");
3743        assert_match_at_position(finder, 2, "main.rs");
3744        assert_match_at_position(finder, 3, "rs");
3745    });
3746
3747    // Remove the first worktree
3748    project.update(cx, |project, cx| {
3749        project.remove_worktree(worktree_1_id, cx);
3750    });
3751    cx.executor().advance_clock(FS_WATCH_LATENCY);
3752
3753    // Files from the first worktree are not in the search results anymore
3754    picker.update(cx, |finder, _| {
3755        assert_eq!(finder.delegate.matches.len(), 2);
3756        assert_match_at_position(finder, 0, "main.rs");
3757        assert_match_at_position(finder, 1, "rs");
3758    });
3759}
3760
3761#[gpui::test]
3762async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files(
3763    cx: &mut TestAppContext,
3764) {
3765    let app_state = init_test(cx);
3766    app_state
3767        .fs
3768        .as_fake()
3769        .insert_tree(
3770            path!("/repo1"),
3771            json!({
3772                "package.json": r#"{"name": "repo1"}"#,
3773                "src": {
3774                    "index.js": "// Repo 1 index",
3775                }
3776            }),
3777        )
3778        .await;
3779
3780    app_state
3781        .fs
3782        .as_fake()
3783        .insert_tree(
3784            path!("/repo2"),
3785            json!({
3786                "package.json": r#"{"name": "repo2"}"#,
3787                "src": {
3788                    "index.js": "// Repo 2 index",
3789                }
3790            }),
3791        )
3792        .await;
3793
3794    let project = Project::test(
3795        app_state.fs.clone(),
3796        [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
3797        cx,
3798    )
3799    .await;
3800
3801    let (multi_workspace, cx) =
3802        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
3803    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3804    let (worktree_id1, worktree_id2) = cx.read(|cx| {
3805        let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
3806        (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
3807    });
3808
3809    workspace
3810        .update_in(cx, |workspace, window, cx| {
3811            workspace.open_path(
3812                ProjectPath {
3813                    worktree_id: worktree_id1,
3814                    path: rel_path("package.json").into(),
3815                },
3816                None,
3817                true,
3818                window,
3819                cx,
3820            )
3821        })
3822        .await
3823        .unwrap();
3824
3825    cx.dispatch_action(workspace::CloseActiveItem {
3826        save_intent: None,
3827        close_pinned: false,
3828    });
3829    workspace
3830        .update_in(cx, |workspace, window, cx| {
3831            workspace.open_path(
3832                ProjectPath {
3833                    worktree_id: worktree_id2,
3834                    path: rel_path("package.json").into(),
3835                },
3836                None,
3837                true,
3838                window,
3839                cx,
3840            )
3841        })
3842        .await
3843        .unwrap();
3844
3845    cx.dispatch_action(workspace::CloseActiveItem {
3846        save_intent: None,
3847        close_pinned: false,
3848    });
3849
3850    let picker = open_file_picker(&workspace, cx);
3851    simulate_input(cx, "package.json");
3852
3853    picker.update(cx, |finder, _| {
3854        let matches = &finder.delegate.matches.matches;
3855
3856        assert_eq!(
3857            matches.len(),
3858            2,
3859            "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
3860            matches.len(),
3861            matches
3862        );
3863
3864        assert_matches!(matches[0], Match::History { .. });
3865
3866        let search_matches = collect_search_matches(finder);
3867        assert_eq!(
3868            search_matches.history.len(),
3869            2,
3870            "Should have exactly 2 history match"
3871        );
3872        assert_eq!(
3873            search_matches.search.len(),
3874            0,
3875            "Should have exactly 0 search match (because we already opened the 2 package.json)"
3876        );
3877
3878        if let Match::History { path, panel_match } = &matches[0] {
3879            assert_eq!(path.project.worktree_id, worktree_id2);
3880            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
3881            let panel_match = panel_match.as_ref().unwrap();
3882            assert_eq!(panel_match.0.path_prefix, rel_path("repo2").into());
3883            assert_eq!(panel_match.0.path, rel_path("package.json").into());
3884            assert_eq!(
3885                panel_match.0.positions,
3886                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
3887            );
3888        }
3889
3890        if let Match::History { path, panel_match } = &matches[1] {
3891            assert_eq!(path.project.worktree_id, worktree_id1);
3892            assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
3893            let panel_match = panel_match.as_ref().unwrap();
3894            assert_eq!(panel_match.0.path_prefix, rel_path("repo1").into());
3895            assert_eq!(panel_match.0.path, rel_path("package.json").into());
3896            assert_eq!(
3897                panel_match.0.positions,
3898                vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
3899            );
3900        }
3901    });
3902}
3903
3904#[gpui::test]
3905async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
3906    let app_state = init_test(cx);
3907
3908    cx.update(|cx| {
3909        let settings = *ProjectPanelSettings::get_global(cx);
3910        ProjectPanelSettings::override_global(
3911            ProjectPanelSettings {
3912                hide_root: true,
3913                ..settings
3914            },
3915            cx,
3916        );
3917    });
3918
3919    app_state.fs.as_fake().insert_tree("/src", json!({})).await;
3920
3921    app_state
3922        .fs
3923        .create_dir("/src/even".as_ref())
3924        .await
3925        .expect("unable to create dir");
3926
3927    let initial_files_num = 5;
3928    for i in 0..initial_files_num {
3929        let filename = format!("/src/even/file_{}.txt", 10 + i);
3930        app_state
3931            .fs
3932            .create_file(Path::new(&filename), Default::default())
3933            .await
3934            .expect("unable to create file");
3935    }
3936
3937    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
3938    let (multi_workspace, cx) =
3939        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3940    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3941
3942    // Initial state
3943    let picker = open_file_picker(&workspace, cx);
3944    simulate_input(cx, "file");
3945    let selected_index = 3;
3946    // Checking only the filename, not the whole path
3947    let selected_file = format!("file_{}.txt", 10 + selected_index);
3948    // Select even/file_13.txt
3949    for _ in 0..selected_index {
3950        cx.dispatch_action(SelectNext);
3951    }
3952
3953    picker.update(cx, |finder, _| {
3954        assert_match_selection(finder, selected_index, &selected_file)
3955    });
3956
3957    // Add more matches to the search results
3958    let files_to_add = 10;
3959    for i in 0..files_to_add {
3960        let filename = format!("/src/file_{}.txt", 20 + i);
3961        app_state
3962            .fs
3963            .create_file(Path::new(&filename), Default::default())
3964            .await
3965            .expect("unable to create file");
3966        // Wait for each file system event to be observed before adding the next.
3967        cx.executor().advance_clock(FS_WATCH_LATENCY);
3968        cx.run_until_parked();
3969    }
3970
3971    cx.executor()
3972        .advance_clock(WORKTREE_UPDATE_REFRESH_DEBOUNCE);
3973    cx.run_until_parked();
3974
3975    // file_13.txt is still selected
3976    picker.update(cx, |finder, _| {
3977        let expected_selected_index = selected_index + files_to_add;
3978        assert_match_selection(finder, expected_selected_index, &selected_file);
3979    });
3980}
3981
3982#[gpui::test]
3983async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
3984    cx: &mut gpui::TestAppContext,
3985) {
3986    let app_state = init_test(cx);
3987
3988    app_state
3989        .fs
3990        .as_fake()
3991        .insert_tree(
3992            "/src",
3993            json!({
3994                "file_1.txt": "// file_1",
3995                "file_2.txt": "// file_2",
3996                "file_3.txt": "// file_3",
3997            }),
3998        )
3999        .await;
4000
4001    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
4002    let (multi_workspace, cx) =
4003        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4004    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4005
4006    // Initial state
4007    let picker = open_file_picker(&workspace, cx);
4008    simulate_input(cx, "file");
4009    // Select even/file_2.txt
4010    cx.dispatch_action(SelectNext);
4011
4012    // Remove the selected entry
4013    app_state
4014        .fs
4015        .remove_file("/src/file_2.txt".as_ref(), Default::default())
4016        .await
4017        .expect("unable to remove file");
4018    advance_worktree_update_refresh(cx);
4019
4020    // file_1.txt is now selected
4021    picker.update(cx, |finder, _| {
4022        assert_match_selection(finder, 0, "file_1.txt");
4023    });
4024}
4025
4026#[gpui::test]
4027async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
4028    let app_state = init_test(cx);
4029
4030    app_state
4031        .fs
4032        .as_fake()
4033        .insert_tree(
4034            path!("/test"),
4035            json!({
4036                "1.txt": "// One",
4037            }),
4038        )
4039        .await;
4040
4041    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
4042    let (multi_workspace, cx) =
4043        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
4044    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4045
4046    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
4047
4048    cx.simulate_modifiers_change(Modifiers::secondary_key());
4049    open_file_picker(&workspace, cx);
4050
4051    cx.simulate_modifiers_change(Modifiers::none());
4052    active_file_picker(&workspace, cx);
4053}
4054
4055#[gpui::test]
4056async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
4057    let app_state = init_test(cx);
4058
4059    app_state
4060        .fs
4061        .as_fake()
4062        .insert_tree(
4063            path!("/test"),
4064            json!({
4065                "1.txt": "// One",
4066                "2.txt": "// Two",
4067            }),
4068        )
4069        .await;
4070
4071    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
4072    let (multi_workspace, cx) =
4073        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
4074    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4075
4076    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
4077    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
4078
4079    cx.simulate_modifiers_change(Modifiers::secondary_key());
4080    let picker = open_file_picker(&workspace, cx);
4081    picker.update(cx, |finder, _| {
4082        assert_eq!(finder.delegate.matches.len(), 2);
4083        assert_match_selection(finder, 0, "2.txt");
4084        assert_match_at_position(finder, 1, "1.txt");
4085    });
4086
4087    cx.dispatch_action(SelectNext);
4088    cx.simulate_modifiers_change(Modifiers::none());
4089    cx.read(|cx| {
4090        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
4091        assert_eq!(active_editor.read(cx).title(cx), "1.txt");
4092    });
4093}
4094
4095#[gpui::test]
4096async fn test_switches_between_release_norelease_modes_on_forward_nav(
4097    cx: &mut gpui::TestAppContext,
4098) {
4099    let app_state = init_test(cx);
4100
4101    app_state
4102        .fs
4103        .as_fake()
4104        .insert_tree(
4105            path!("/test"),
4106            json!({
4107                "1.txt": "// One",
4108                "2.txt": "// Two",
4109            }),
4110        )
4111        .await;
4112
4113    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
4114    let (multi_workspace, cx) =
4115        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
4116    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4117
4118    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
4119    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
4120
4121    // Open with a shortcut
4122    cx.simulate_modifiers_change(Modifiers::secondary_key());
4123    let picker = open_file_picker(&workspace, cx);
4124    picker.update(cx, |finder, _| {
4125        assert_eq!(finder.delegate.matches.len(), 2);
4126        assert_match_selection(finder, 0, "2.txt");
4127        assert_match_at_position(finder, 1, "1.txt");
4128    });
4129
4130    // Switch to navigating with other shortcuts
4131    // Don't open file on modifiers release
4132    cx.simulate_modifiers_change(Modifiers::control());
4133    cx.dispatch_action(SelectNext);
4134    cx.simulate_modifiers_change(Modifiers::none());
4135    picker.update(cx, |finder, _| {
4136        assert_eq!(finder.delegate.matches.len(), 2);
4137        assert_match_at_position(finder, 0, "2.txt");
4138        assert_match_selection(finder, 1, "1.txt");
4139    });
4140
4141    // Back to navigation with initial shortcut
4142    // Open file on modifiers release
4143    cx.simulate_modifiers_change(Modifiers::secondary_key());
4144    cx.dispatch_action(ToggleFileFinder::default());
4145    cx.simulate_modifiers_change(Modifiers::none());
4146    cx.read(|cx| {
4147        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
4148        assert_eq!(active_editor.read(cx).title(cx), "2.txt");
4149    });
4150}
4151
4152#[gpui::test]
4153async fn test_switches_between_release_norelease_modes_on_backward_nav(
4154    cx: &mut gpui::TestAppContext,
4155) {
4156    let app_state = init_test(cx);
4157
4158    app_state
4159        .fs
4160        .as_fake()
4161        .insert_tree(
4162            path!("/test"),
4163            json!({
4164                "1.txt": "// One",
4165                "2.txt": "// Two",
4166                "3.txt": "// Three"
4167            }),
4168        )
4169        .await;
4170
4171    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
4172    let (multi_workspace, cx) =
4173        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
4174    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4175
4176    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
4177    open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
4178    open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
4179
4180    // Open with a shortcut
4181    cx.simulate_modifiers_change(Modifiers::secondary_key());
4182    let picker = open_file_picker(&workspace, cx);
4183    picker.update(cx, |finder, _| {
4184        assert_eq!(finder.delegate.matches.len(), 3);
4185        assert_match_selection(finder, 0, "3.txt");
4186        assert_match_at_position(finder, 1, "2.txt");
4187        assert_match_at_position(finder, 2, "1.txt");
4188    });
4189
4190    // Switch to navigating with other shortcuts
4191    // Don't open file on modifiers release
4192    cx.simulate_modifiers_change(Modifiers::control());
4193    cx.dispatch_action(menu::SelectPrevious);
4194    cx.simulate_modifiers_change(Modifiers::none());
4195    picker.update(cx, |finder, _| {
4196        assert_eq!(finder.delegate.matches.len(), 3);
4197        assert_match_at_position(finder, 0, "3.txt");
4198        assert_match_at_position(finder, 1, "2.txt");
4199        assert_match_selection(finder, 2, "1.txt");
4200    });
4201
4202    // Back to navigation with initial shortcut
4203    // Open file on modifiers release
4204    cx.simulate_modifiers_change(Modifiers::secondary_key());
4205    cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
4206    cx.simulate_modifiers_change(Modifiers::none());
4207    cx.read(|cx| {
4208        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
4209        assert_eq!(active_editor.read(cx).title(cx), "3.txt");
4210    });
4211}
4212
4213#[gpui::test]
4214async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
4215    let app_state = init_test(cx);
4216
4217    app_state
4218        .fs
4219        .as_fake()
4220        .insert_tree(
4221            path!("/test"),
4222            json!({
4223                "1.txt": "// One",
4224            }),
4225        )
4226        .await;
4227
4228    let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
4229    let (multi_workspace, cx) =
4230        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
4231    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4232
4233    open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
4234
4235    cx.simulate_modifiers_change(Modifiers::secondary_key());
4236    open_file_picker(&workspace, cx);
4237
4238    cx.simulate_modifiers_change(Modifiers::command_shift());
4239    active_file_picker(&workspace, cx);
4240}
4241
4242#[gpui::test]
4243async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
4244    let app_state = init_test(cx);
4245    app_state
4246        .fs
4247        .as_fake()
4248        .insert_tree(
4249            "/test",
4250            json!({
4251                "00.txt": "",
4252                "01.txt": "",
4253                "02.txt": "",
4254                "03.txt": "",
4255                "04.txt": "",
4256                "05.txt": "",
4257            }),
4258        )
4259        .await;
4260
4261    let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
4262    let (multi_workspace, cx) =
4263        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
4264    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4265
4266    cx.dispatch_action(ToggleFileFinder::default());
4267    let picker = active_file_picker(&workspace, cx);
4268
4269    picker.update_in(cx, |picker, window, cx| {
4270        picker.update_matches(".txt".to_string(), window, cx)
4271    });
4272
4273    cx.executor().advance_clock(SEARCH_DEBOUNCE);
4274    cx.run_until_parked();
4275
4276    picker.update(cx, |picker, _| {
4277        assert_eq!(picker.delegate.matches.len(), 7);
4278        assert_eq!(picker.delegate.selected_index, 0);
4279    });
4280
4281    // When toggling repeatedly, the picker scrolls to reveal the selected item.
4282    cx.dispatch_action(ToggleFileFinder::default());
4283    cx.dispatch_action(ToggleFileFinder::default());
4284    cx.dispatch_action(ToggleFileFinder::default());
4285
4286    cx.run_until_parked();
4287
4288    picker.update(cx, |picker, _| {
4289        assert_eq!(picker.delegate.matches.len(), 7);
4290        assert_eq!(picker.delegate.selected_index, 3);
4291    });
4292}
4293
4294#[gpui::test]
4295async fn test_open_without_dismiss_keeps_finder_open(cx: &mut TestAppContext) {
4296    let app_state = init_test(cx);
4297    app_state
4298        .fs
4299        .as_fake()
4300        .insert_tree(
4301            path!("/root"),
4302            json!({
4303                "a": {
4304                    "file1.txt": "content1",
4305                    "file2.txt": "content2",
4306                    "file3.txt": "content3",
4307                }
4308            }),
4309        )
4310        .await;
4311
4312    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4313    let (picker, workspace, cx) = build_find_picker(project, cx);
4314
4315    simulate_input(cx, "file");
4316    picker.update(cx, |picker, _| {
4317        assert!(
4318            picker.delegate.matches.len() >= 3,
4319            "Expected at least 3 matches for 'file', got {}",
4320            picker.delegate.matches.len()
4321        );
4322    });
4323
4324    cx.dispatch_action(OpenWithoutDismiss);
4325    cx.run_until_parked();
4326
4327    // Finder must still be visible after opening a file without dismiss.
4328    workspace.update(cx, |workspace, cx| {
4329        assert!(
4330            workspace.active_modal::<FileFinder>(cx).is_some(),
4331            "File finder should remain open after OpenWithoutDismiss"
4332        );
4333    });
4334
4335    // Exactly one file was opened in the pane.
4336    cx.read(|cx| {
4337        let items: Vec<_> = workspace.read(cx).active_pane().read(cx).items().collect();
4338        assert_eq!(items.len(), 1, "One file should be open in the pane");
4339    });
4340
4341    // The search query and results are preserved so the user can continue browsing.
4342    picker.update(cx, |picker, _| {
4343        assert!(
4344            picker.delegate.matches.len() >= 3,
4345            "Search results should remain unchanged after OpenWithoutDismiss"
4346        );
4347    });
4348}
4349
4350#[gpui::test]
4351async fn test_open_without_dismiss_opens_multiple_files(cx: &mut TestAppContext) {
4352    let app_state = init_test(cx);
4353    app_state
4354        .fs
4355        .as_fake()
4356        .insert_tree(
4357            path!("/root"),
4358            json!({
4359                "a": {
4360                    "alpha.txt": "alpha",
4361                    "beta.txt": "beta",
4362                    "gamma.txt": "gamma",
4363                }
4364            }),
4365        )
4366        .await;
4367
4368    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4369    let (_picker, workspace, cx) = build_find_picker(project, cx);
4370
4371    simulate_input(cx, "a");
4372
4373    // Open the first match and stay in the finder.
4374    cx.dispatch_action(OpenWithoutDismiss);
4375    cx.run_until_parked();
4376
4377    workspace.update(cx, |workspace, cx| {
4378        assert!(
4379            workspace.active_modal::<FileFinder>(cx).is_some(),
4380            "Finder should remain open after first OpenWithoutDismiss"
4381        );
4382    });
4383    cx.read(|cx| {
4384        let pane = workspace.read(cx).active_pane().read(cx);
4385        assert_eq!(
4386            pane.items().count(),
4387            1,
4388            "One file open after first OpenWithoutDismiss"
4389        );
4390    });
4391
4392    // Navigate to the next result and open it too.
4393    cx.dispatch_action(SelectNext);
4394    cx.dispatch_action(OpenWithoutDismiss);
4395    cx.run_until_parked();
4396
4397    workspace.update(cx, |workspace, cx| {
4398        assert!(
4399            workspace.active_modal::<FileFinder>(cx).is_some(),
4400            "Finder should remain open after second OpenWithoutDismiss"
4401        );
4402    });
4403    cx.read(|cx| {
4404        let pane = workspace.read(cx).active_pane().read(cx);
4405        assert_eq!(
4406            pane.items().count(),
4407            2,
4408            "Two files open after second OpenWithoutDismiss"
4409        );
4410        // The second opened file should now be the active tab.
4411        let active_index = pane.active_item_index();
4412        assert_eq!(active_index, 1, "Second file should be the active tab");
4413    });
4414}
4415
4416#[gpui::test]
4417async fn test_open_without_dismiss_then_confirm_closes_finder(cx: &mut TestAppContext) {
4418    let app_state = init_test(cx);
4419    app_state
4420        .fs
4421        .as_fake()
4422        .insert_tree(
4423            path!("/root"),
4424            json!({
4425                "a": {
4426                    "first.txt": "first",
4427                    "second.txt": "second",
4428                }
4429            }),
4430        )
4431        .await;
4432
4433    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4434    let (picker, workspace, cx) = build_find_picker(project, cx);
4435
4436    simulate_input(cx, "t");
4437    picker.update(cx, |picker, _| {
4438        assert!(picker.delegate.matches.len() >= 2);
4439    });
4440
4441    // Open first file, keep finder open.
4442    cx.dispatch_action(OpenWithoutDismiss);
4443    cx.run_until_parked();
4444
4445    workspace.update(cx, |workspace, cx| {
4446        assert!(workspace.active_modal::<FileFinder>(cx).is_some());
4447    });
4448
4449    // Navigate to the next match and confirm normally — this should close the finder.
4450    cx.dispatch_action(SelectNext);
4451    cx.dispatch_action(Confirm);
4452    cx.run_until_parked();
4453
4454    workspace.update(cx, |workspace, cx| {
4455        assert!(
4456            workspace.active_modal::<FileFinder>(cx).is_none(),
4457            "Finder should be closed after regular Confirm"
4458        );
4459    });
4460
4461    // Two files were opened in total, with the confirmed one now active.
4462    cx.read(|cx| {
4463        let pane = workspace.read(cx).active_pane().read(cx);
4464        assert_eq!(pane.items().count(), 2, "Two files should be open total");
4465        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
4466        let title = active_editor.read(cx).title(cx);
4467        assert!(
4468            title == "second.txt" || title == "first.txt",
4469            "Active editor should be one of the opened files, got: {title}"
4470        );
4471    });
4472}
4473
4474#[gpui::test]
4475async fn test_reopen_with_preview_keeps_results_width(cx: &mut TestAppContext) {
4476    let app_state = init_test(cx);
4477    app_state
4478        .fs
4479        .as_fake()
4480        .insert_tree(path!("/root"), json!({ "a.txt": "", "b.txt": "" }))
4481        .await;
4482    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4483    let (picker, workspace, cx) = build_find_picker(project, cx);
4484
4485    cx.dispatch_action(picker::SetPreviewRight);
4486    cx.run_until_parked();
4487    let in_session_width = picker.update_in(cx, |picker, window, _| picker.results_width(window));
4488
4489    cx.dispatch_action(Cancel);
4490    cx.run_until_parked();
4491
4492    let picker = open_file_picker(&workspace, cx);
4493    cx.run_until_parked();
4494    let reopened_width = picker.update_in(cx, |picker, window, _| picker.results_width(window));
4495
4496    assert_eq!(
4497        in_session_width, reopened_width,
4498        "reopening with the side preview must keep the same results width as the in-session toggle"
4499    );
4500
4501    // The preview layout is persisted in the key-value store, and tests in a
4502    // process share one in-memory fallback store (no per-App `AppDatabase` is
4503    // set in tests, so `AppDatabase::global` falls back to the shared static).
4504    // Reset to the default layout so this write doesn't leak into other tests.
4505    cx.dispatch_action(picker::SetPreviewHidden);
4506    cx.run_until_parked();
4507}
4508
4509async fn open_close_queried_buffer(
4510    input: &str,
4511    expected_matches: usize,
4512    expected_editor_title: &str,
4513    workspace: &Entity<Workspace>,
4514    cx: &mut gpui::VisualTestContext,
4515) -> Vec<FoundPath> {
4516    let history_items = open_queried_buffer(
4517        input,
4518        expected_matches,
4519        expected_editor_title,
4520        workspace,
4521        cx,
4522    )
4523    .await;
4524
4525    cx.dispatch_action(workspace::CloseActiveItem {
4526        save_intent: None,
4527        close_pinned: false,
4528    });
4529
4530    history_items
4531}
4532
4533async fn open_queried_buffer(
4534    input: &str,
4535    expected_matches: usize,
4536    expected_editor_title: &str,
4537    workspace: &Entity<Workspace>,
4538    cx: &mut gpui::VisualTestContext,
4539) -> Vec<FoundPath> {
4540    let picker = open_file_picker(workspace, cx);
4541    simulate_input(cx, input);
4542
4543    let history_items = picker.update(cx, |finder, _| {
4544        assert_eq!(
4545            finder.delegate.matches.len(),
4546            expected_matches + 1, // +1 from CreateNew option
4547            "Unexpected number of matches found for query `{input}`, matches: {:?}",
4548            finder.delegate.matches
4549        );
4550        finder.delegate.history_items.clone()
4551    });
4552
4553    cx.dispatch_action(Confirm);
4554    // Opening the buffer can trigger worktree updates that schedule a debounced
4555    // refresh; advance past it so a deferred confirm (confirm_on_update) runs.
4556    cx.executor().advance_clock(SEARCH_DEBOUNCE);
4557    cx.run_until_parked();
4558
4559    cx.read(|cx| {
4560        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
4561        let active_editor_title = active_editor.read(cx).title(cx);
4562        assert_eq!(
4563            expected_editor_title, active_editor_title,
4564            "Unexpected editor title for query `{input}`"
4565        );
4566    });
4567
4568    history_items
4569}
4570
4571pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4572    cx.update(|cx| {
4573        let state = AppState::test(cx);
4574        theme_settings::init(theme::LoadThemes::JustBase, cx);
4575        super::init(cx);
4576        editor::init(cx);
4577        state
4578    })
4579}
4580
4581fn test_path_position(test_str: &str) -> FileSearchQuery {
4582    parse_file_search_query(test_str)
4583}
4584
4585fn build_find_picker(
4586    project: Entity<Project>,
4587    cx: &mut TestAppContext,
4588) -> (
4589    Entity<Picker<FileFinderDelegate>>,
4590    Entity<Workspace>,
4591    &mut VisualTestContext,
4592) {
4593    let (multi_workspace, cx) =
4594        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
4595    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4596    let picker = open_file_picker(&workspace, cx);
4597    (picker, workspace, cx)
4598}
4599
4600#[track_caller]
4601pub(crate) fn open_file_picker(
4602    workspace: &Entity<Workspace>,
4603    cx: &mut VisualTestContext,
4604) -> Entity<Picker<FileFinderDelegate>> {
4605    cx.dispatch_action(ToggleFileFinder {
4606        separate_history: true,
4607        include_ignored: None,
4608    });
4609    active_file_picker(workspace, cx)
4610}
4611
4612/// Type `input` into the file finder and then let the debounced search run.
4613///
4614/// `update_matches` delays the actual search by [`SEARCH_DEBOUNCE`] (see its
4615/// doc comment), and `run_until_parked` does not advance the clock, so tests
4616/// must move time forward for the search to execute.
4617fn simulate_input(cx: &mut VisualTestContext, input: &str) {
4618    cx.simulate_input(input);
4619    cx.executor().advance_clock(SEARCH_DEBOUNCE);
4620    cx.run_until_parked();
4621}
4622
4623fn advance_worktree_update_refresh(cx: &mut VisualTestContext) {
4624    cx.executor()
4625        .advance_clock(FS_WATCH_LATENCY + WORKTREE_UPDATE_REFRESH_DEBOUNCE);
4626    cx.run_until_parked();
4627}
4628
4629#[track_caller]
4630pub(crate) fn active_file_picker(
4631    workspace: &Entity<Workspace>,
4632    cx: &mut VisualTestContext,
4633) -> Entity<Picker<FileFinderDelegate>> {
4634    workspace.update(cx, |workspace, cx| {
4635        workspace
4636            .active_modal::<FileFinder>(cx)
4637            .expect("file finder is not open")
4638            .read(cx)
4639            .picker
4640            .clone()
4641    })
4642}
4643
4644#[derive(Debug, Default)]
4645struct SearchEntries {
4646    history: Vec<Arc<RelPath>>,
4647    history_found_paths: Vec<FoundPath>,
4648    search: Vec<Arc<RelPath>>,
4649    search_matches: Vec<PathMatch>,
4650}
4651
4652impl SearchEntries {
4653    #[track_caller]
4654    fn search_paths_only(self) -> Vec<Arc<RelPath>> {
4655        assert!(
4656            self.history.is_empty(),
4657            "Should have no history matches, but got: {:?}",
4658            self.history
4659        );
4660        self.search
4661    }
4662
4663    #[track_caller]
4664    fn search_matches_only(self) -> Vec<PathMatch> {
4665        assert!(
4666            self.history.is_empty(),
4667            "Should have no history matches, but got: {:?}",
4668            self.history
4669        );
4670        self.search_matches
4671    }
4672}
4673
4674fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
4675    let mut search_entries = SearchEntries::default();
4676    for m in &picker.delegate.matches.matches {
4677        match m {
4678            Match::History {
4679                path: history_path,
4680                panel_match: path_match,
4681            } => {
4682                if let Some(path_match) = path_match.as_ref() {
4683                    search_entries
4684                        .history
4685                        .push(path_match.0.path_prefix.join(&path_match.0.path).into());
4686                } else {
4687                    // This occurs when the query is empty and we show history matches
4688                    // that are outside the project.
4689                    panic!("currently not exercised in tests");
4690                }
4691                search_entries
4692                    .history_found_paths
4693                    .push(history_path.clone());
4694            }
4695            Match::Search(path_match) => {
4696                search_entries
4697                    .search
4698                    .push(path_match.0.path_prefix.join(&path_match.0.path).into());
4699                search_entries.search_matches.push(path_match.0.clone());
4700            }
4701            Match::CreateNew(_) => {}
4702            Match::Channel { .. } => {}
4703        }
4704    }
4705    search_entries
4706}
4707
4708#[track_caller]
4709fn assert_match_selection(
4710    finder: &Picker<FileFinderDelegate>,
4711    expected_selection_index: usize,
4712    expected_file_name: &str,
4713) {
4714    assert_eq!(
4715        finder.delegate.selected_index(),
4716        expected_selection_index,
4717        "Match is not selected"
4718    );
4719    assert_match_at_position(finder, expected_selection_index, expected_file_name);
4720}
4721
4722#[track_caller]
4723fn assert_match_at_position(
4724    finder: &Picker<FileFinderDelegate>,
4725    match_index: usize,
4726    expected_file_name: &str,
4727) {
4728    let match_item = finder
4729        .delegate
4730        .matches
4731        .get(match_index)
4732        .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
4733    let match_file_name = match &match_item {
4734        Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
4735        Match::Search(path_match) => path_match.0.path.file_name(),
4736        Match::CreateNew(project_path) => project_path.path.file_name(),
4737        Match::Channel { channel_name, .. } => Some(channel_name.as_str()),
4738    }
4739    .unwrap();
4740    assert_eq!(match_file_name, expected_file_name);
4741}
4742
4743#[gpui::test]
4744async fn test_filename_precedence(cx: &mut TestAppContext) {
4745    let app_state = init_test(cx);
4746
4747    cx.update(|cx| {
4748        let settings = *ProjectPanelSettings::get_global(cx);
4749        ProjectPanelSettings::override_global(
4750            ProjectPanelSettings {
4751                hide_root: true,
4752                ..settings
4753            },
4754            cx,
4755        );
4756    });
4757
4758    app_state
4759        .fs
4760        .as_fake()
4761        .insert_tree(
4762            path!("/src"),
4763            json!({
4764                "layout": {
4765                    "app.css": "",
4766                    "app.d.ts": "",
4767                    "app.html": "",
4768                    "+page.svelte": "",
4769                },
4770                "routes": {
4771                    "+layout.svelte": "",
4772                }
4773            }),
4774        )
4775        .await;
4776
4777    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
4778    let (picker, _, cx) = build_find_picker(project, cx);
4779
4780    simulate_input(cx, "layout");
4781
4782    picker.update(cx, |finder, _| {
4783        let search_matches = collect_search_matches(finder).search_paths_only();
4784
4785        assert_eq!(
4786            search_matches,
4787            vec![
4788                rel_path("routes/+layout.svelte").into(),
4789                rel_path("layout/app.css").into(),
4790                rel_path("layout/app.d.ts").into(),
4791                rel_path("layout/app.html").into(),
4792                rel_path("layout/+page.svelte").into(),
4793            ],
4794            "File with 'layout' in filename should be prioritized over files in 'layout' directory"
4795        );
4796    });
4797}
4798
4799#[gpui::test]
4800async fn test_paths_with_starting_slash(cx: &mut TestAppContext) {
4801    let app_state = init_test(cx);
4802
4803    cx.update(|cx| {
4804        let settings = *ProjectPanelSettings::get_global(cx);
4805        ProjectPanelSettings::override_global(
4806            ProjectPanelSettings {
4807                hide_root: true,
4808                ..settings
4809            },
4810            cx,
4811        );
4812    });
4813
4814    app_state
4815        .fs
4816        .as_fake()
4817        .insert_tree(
4818            path!("/root"),
4819            json!({
4820                "a": {
4821                    "file1.txt": "",
4822                    "b": {
4823                        "file2.txt": "",
4824                    },
4825                }
4826            }),
4827        )
4828        .await;
4829
4830    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4831
4832    let (picker, workspace, cx) = build_find_picker(project, cx);
4833
4834    let matching_abs_path = "/file1.txt".to_string();
4835    picker
4836        .update_in(cx, |picker, window, cx| {
4837            picker
4838                .delegate
4839                .update_matches(matching_abs_path, window, cx)
4840        })
4841        .await;
4842    picker.update(cx, |picker, _| {
4843        assert_eq!(
4844            collect_search_matches(picker).search_paths_only(),
4845            vec![rel_path("a/file1.txt").into()],
4846            "Relative path starting with slash should match"
4847        )
4848    });
4849    cx.dispatch_action(SelectNext);
4850    cx.dispatch_action(Confirm);
4851    cx.read(|cx| {
4852        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
4853        assert_eq!(active_editor.read(cx).title(cx), "file1.txt");
4854    });
4855}
4856
4857#[gpui::test]
4858async fn test_clear_navigation_history(cx: &mut TestAppContext) {
4859    let app_state = init_test(cx);
4860    app_state
4861        .fs
4862        .as_fake()
4863        .insert_tree(
4864            path!("/src"),
4865            json!({
4866                "test": {
4867                    "first.rs": "// First file",
4868                    "second.rs": "// Second file",
4869                    "third.rs": "// Third file",
4870                }
4871            }),
4872        )
4873        .await;
4874
4875    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
4876    let (multi_workspace, cx) =
4877        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
4878    let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
4879
4880    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
4881
4882    // Open some files to generate navigation history
4883    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
4884    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
4885    let history_before_clear =
4886        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
4887
4888    assert_eq!(
4889        history_before_clear.len(),
4890        2,
4891        "Should have history items before clearing"
4892    );
4893
4894    // Verify that file finder shows history items
4895    let picker = open_file_picker(&workspace, cx);
4896    simulate_input(cx, "fir");
4897    picker.update(cx, |finder, _| {
4898        let matches = collect_search_matches(finder);
4899        assert!(
4900            !matches.history.is_empty(),
4901            "File finder should show history items before clearing"
4902        );
4903    });
4904    workspace.update_in(cx, |_, window, cx| {
4905        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
4906    });
4907
4908    // Verify navigation state before clear
4909    workspace.update(cx, |workspace, cx| {
4910        let pane = workspace.active_pane();
4911        pane.read(cx).can_navigate_backward()
4912    });
4913
4914    // Clear navigation history
4915    cx.dispatch_action(workspace::ClearNavigationHistory);
4916
4917    // Verify that navigation is disabled immediately after clear
4918    workspace.update(cx, |workspace, cx| {
4919        let pane = workspace.active_pane();
4920        assert!(
4921            !pane.read(cx).can_navigate_backward(),
4922            "Should not be able to navigate backward after clearing history"
4923        );
4924        assert!(
4925            !pane.read(cx).can_navigate_forward(),
4926            "Should not be able to navigate forward after clearing history"
4927        );
4928    });
4929
4930    // Verify that file finder no longer shows history items
4931    let picker = open_file_picker(&workspace, cx);
4932    simulate_input(cx, "fir");
4933    picker.update(cx, |finder, _| {
4934        let matches = collect_search_matches(finder);
4935        assert!(
4936            matches.history.is_empty(),
4937            "File finder should not show history items after clearing"
4938        );
4939    });
4940    workspace.update_in(cx, |_, window, cx| {
4941        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
4942    });
4943
4944    // Verify history is empty by opening a new file
4945    // (this should not show any previous history)
4946    let history_after_clear =
4947        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
4948    assert_eq!(
4949        history_after_clear.len(),
4950        0,
4951        "Should have no history items after clearing"
4952    );
4953}
4954
4955#[gpui::test]
4956async fn test_order_independent_search(cx: &mut TestAppContext) {
4957    let app_state = init_test(cx);
4958    app_state
4959        .fs
4960        .as_fake()
4961        .insert_tree(
4962            "/src",
4963            json!({
4964                "internal": {
4965                    "auth": {
4966                        "login.rs": "",
4967                    }
4968                }
4969            }),
4970        )
4971        .await;
4972    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
4973    let (picker, _, cx) = build_find_picker(project, cx);
4974
4975    // forward order
4976    picker
4977        .update_in(cx, |picker, window, cx| {
4978            picker
4979                .delegate
4980                .spawn_search(test_path_position("auth internal"), window, cx)
4981        })
4982        .await;
4983    picker.update(cx, |picker, _| {
4984        let matches = collect_search_matches(picker).search_matches_only();
4985        assert_eq!(matches.len(), 1);
4986        assert_eq!(matches[0].path.as_unix_str(), "internal/auth/login.rs");
4987    });
4988
4989    // reverse order should give same result
4990    picker
4991        .update_in(cx, |picker, window, cx| {
4992            picker
4993                .delegate
4994                .spawn_search(test_path_position("internal auth"), window, cx)
4995        })
4996        .await;
4997    picker.update(cx, |picker, _| {
4998        let matches = collect_search_matches(picker).search_matches_only();
4999        assert_eq!(matches.len(), 1);
5000        assert_eq!(matches[0].path.as_unix_str(), "internal/auth/login.rs");
5001    });
5002}
5003
5004#[gpui::test]
5005async fn test_filename_preferred_over_directory_match(cx: &mut TestAppContext) {
5006    let app_state = init_test(cx);
5007    app_state
5008        .fs
5009        .as_fake()
5010        .insert_tree(
5011            "/src",
5012            json!({
5013                "crates": {
5014                    "settings_ui": {
5015                        "src": {
5016                            "pages": {
5017                                "audio_test_window.rs": "",
5018                                "audio_input_output_setup.rs": "",
5019                            }
5020                        }
5021                    },
5022                    "audio": {
5023                        "src": {
5024                            "audio_settings.rs": "",
5025                        }
5026                    }
5027                }
5028            }),
5029        )
5030        .await;
5031    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
5032    let (picker, _, cx) = build_find_picker(project, cx);
5033
5034    picker
5035        .update_in(cx, |picker, window, cx| {
5036            picker
5037                .delegate
5038                .spawn_search(test_path_position("settings audio"), window, cx)
5039        })
5040        .await;
5041    picker.update(cx, |picker, _| {
5042        let matches = collect_search_matches(picker).search_matches_only();
5043        assert!(!matches.is_empty(),);
5044        assert_eq!(
5045            matches[0].path.as_unix_str(),
5046            "crates/audio/src/audio_settings.rs"
5047        );
5048    });
5049}
5050
5051#[gpui::test]
5052async fn test_start_of_word_preferred_over_scattered_match(cx: &mut TestAppContext) {
5053    let app_state = init_test(cx);
5054    app_state
5055        .fs
5056        .as_fake()
5057        .insert_tree(
5058            "/src",
5059            json!({
5060                "crates": {
5061                    "livekit_client": {
5062                        "src": {
5063                            "livekit_client": {
5064                                "playback.rs": "",
5065                            }
5066                        }
5067                    },
5068                    "vim": {
5069                        "test_data": {
5070                            "test_record_replay_interleaved.json": "",
5071                        }
5072                    }
5073                }
5074            }),
5075        )
5076        .await;
5077    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
5078    let (picker, _, cx) = build_find_picker(project, cx);
5079
5080    picker
5081        .update_in(cx, |picker, window, cx| {
5082            picker
5083                .delegate
5084                .spawn_search(test_path_position("live pla"), window, cx)
5085        })
5086        .await;
5087    picker.update(cx, |picker, _| {
5088        let matches = collect_search_matches(picker).search_matches_only();
5089        assert!(!matches.is_empty(),);
5090        assert_eq!(
5091            matches[0].path.as_unix_str(),
5092            "crates/livekit_client/src/livekit_client/playback.rs",
5093        );
5094    });
5095}
5096
5097#[gpui::test]
5098async fn test_exact_filename_stem_preferred(cx: &mut TestAppContext) {
5099    let app_state = init_test(cx);
5100    app_state
5101        .fs
5102        .as_fake()
5103        .insert_tree(
5104            "/src",
5105            json!({
5106                "assets": {
5107                    "icons": {
5108                        "file_icons": {
5109                            "nix.svg": "",
5110                        }
5111                    }
5112                },
5113                "crates": {
5114                    "zed": {
5115                        "resources": {
5116                            "app-icon-nightly@2x.png": "",
5117                            "app-icon-preview@2x.png": "",
5118                        }
5119                    }
5120                }
5121            }),
5122        )
5123        .await;
5124    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
5125    let (picker, _, cx) = build_find_picker(project, cx);
5126
5127    picker
5128        .update_in(cx, |picker, window, cx| {
5129            picker
5130                .delegate
5131                .spawn_search(test_path_position("nix icon"), window, cx)
5132        })
5133        .await;
5134    picker.update(cx, |picker, _| {
5135        let matches = collect_search_matches(picker).search_matches_only();
5136        assert!(!matches.is_empty(),);
5137        assert_eq!(
5138            matches[0].path.as_unix_str(),
5139            "assets/icons/file_icons/nix.svg",
5140        );
5141    });
5142}
5143
5144#[gpui::test]
5145async fn test_exact_filename_with_directory_token(cx: &mut TestAppContext) {
5146    let app_state = init_test(cx);
5147    app_state
5148        .fs
5149        .as_fake()
5150        .insert_tree(
5151            "/src",
5152            json!({
5153                "crates": {
5154                    "agent_servers": {
5155                        "src": {
5156                            "acp.rs": "",
5157                            "agent_server.rs": "",
5158                            "custom.rs": "",
5159                        }
5160                    }
5161                }
5162            }),
5163        )
5164        .await;
5165    let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
5166    let (picker, _, cx) = build_find_picker(project, cx);
5167
5168    picker
5169        .update_in(cx, |picker, window, cx| {
5170            picker
5171                .delegate
5172                .spawn_search(test_path_position("acp server"), window, cx)
5173        })
5174        .await;
5175    picker.update(cx, |picker, _| {
5176        let matches = collect_search_matches(picker).search_matches_only();
5177        assert!(!matches.is_empty(),);
5178        assert_eq!(
5179            matches[0].path.as_unix_str(),
5180            "crates/agent_servers/src/acp.rs",
5181        );
5182    });
5183}
5184
Served at tenant.openagents/omega Member data and write actions are omitted.