1use std::{future::IntoFuture, path::Path, time::Duration};
2
3use super::*;
4use editor::Editor;
5use gpui::{Entity, TestAppContext, VisualTestContext};
6use menu::{Confirm, SelectNext, SelectPrevious};
7use pretty_assertions::assert_eq;
8use project::{FS_WATCH_LATENCY, RemoveOptions};
9use serde_json::json;
10use util::path;
11use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace};
12
13#[ctor::ctor]
14fn init_logger() {
15 zlog::init_test();
16}
17
18#[test]
19fn test_path_elision() {
20 #[track_caller]
21 fn check(path: &str, budget: usize, matches: impl IntoIterator<Item = usize>, expected: &str) {
22 let mut path = path.to_owned();
23 let slice = PathComponentSlice::new(&path);
24 let matches = Vec::from_iter(matches);
25 if let Some(range) = slice.elision_range(budget - 1, &matches) {
26 path.replace_range(range, "…");
27 }
28 assert_eq!(path, expected);
29 }
30
31 // Simple cases, mostly to check that different path shapes are handled gracefully.
32 check("p/a/b/c/d/", 6, [], "p/…/d/");
33 check("p/a/b/c/d/", 1, [2, 4, 6], "p/a/b/c/d/");
34 check("p/a/b/c/d/", 10, [2, 6], "p/a/…/c/d/");
35 check("p/a/b/c/d/", 8, [6], "p/…/c/d/");
36
37 check("p/a/b/c/d", 5, [], "p/…/d");
38 check("p/a/b/c/d", 9, [2, 4, 6], "p/a/b/c/d");
39 check("p/a/b/c/d", 9, [2, 6], "p/a/…/c/d");
40 check("p/a/b/c/d", 7, [6], "p/…/c/d");
41
42 check("/p/a/b/c/d/", 7, [], "/p/…/d/");
43 check("/p/a/b/c/d/", 11, [3, 5, 7], "/p/a/b/c/d/");
44 check("/p/a/b/c/d/", 11, [3, 7], "/p/a/…/c/d/");
45 check("/p/a/b/c/d/", 9, [7], "/p/…/c/d/");
46
47 // If the budget can't be met, no elision is done.
48 check(
49 "project/dir/child/grandchild",
50 5,
51 [],
52 "project/dir/child/grandchild",
53 );
54
55 // The longest unmatched segment is picked for elision.
56 check(
57 "project/one/two/X/three/sub",
58 21,
59 [16],
60 "project/…/X/three/sub",
61 );
62
63 // Elision stops when the budget is met, even though there are more components in the chosen segment.
64 // It proceeds from the end of the unmatched segment that is closer to the midpoint of the path.
65 check(
66 "project/one/two/three/X/sub",
67 21,
68 [22],
69 "project/…/three/X/sub",
70 )
71}
72
73#[test]
74fn test_custom_project_search_ordering_in_file_finder() {
75 let mut file_finder_sorted_output = vec![
76 ProjectPanelOrdMatch(PathMatch {
77 score: 0.5,
78 positions: Vec::new(),
79 worktree_id: 0,
80 path: Arc::from(Path::new("b0.5")),
81 path_prefix: Arc::default(),
82 distance_to_relative_ancestor: 0,
83 is_dir: false,
84 }),
85 ProjectPanelOrdMatch(PathMatch {
86 score: 1.0,
87 positions: Vec::new(),
88 worktree_id: 0,
89 path: Arc::from(Path::new("c1.0")),
90 path_prefix: Arc::default(),
91 distance_to_relative_ancestor: 0,
92 is_dir: false,
93 }),
94 ProjectPanelOrdMatch(PathMatch {
95 score: 1.0,
96 positions: Vec::new(),
97 worktree_id: 0,
98 path: Arc::from(Path::new("a1.0")),
99 path_prefix: Arc::default(),
100 distance_to_relative_ancestor: 0,
101 is_dir: false,
102 }),
103 ProjectPanelOrdMatch(PathMatch {
104 score: 0.5,
105 positions: Vec::new(),
106 worktree_id: 0,
107 path: Arc::from(Path::new("a0.5")),
108 path_prefix: Arc::default(),
109 distance_to_relative_ancestor: 0,
110 is_dir: false,
111 }),
112 ProjectPanelOrdMatch(PathMatch {
113 score: 1.0,
114 positions: Vec::new(),
115 worktree_id: 0,
116 path: Arc::from(Path::new("b1.0")),
117 path_prefix: Arc::default(),
118 distance_to_relative_ancestor: 0,
119 is_dir: false,
120 }),
121 ];
122 file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
123
124 assert_eq!(
125 file_finder_sorted_output,
126 vec![
127 ProjectPanelOrdMatch(PathMatch {
128 score: 1.0,
129 positions: Vec::new(),
130 worktree_id: 0,
131 path: Arc::from(Path::new("a1.0")),
132 path_prefix: Arc::default(),
133 distance_to_relative_ancestor: 0,
134 is_dir: false,
135 }),
136 ProjectPanelOrdMatch(PathMatch {
137 score: 1.0,
138 positions: Vec::new(),
139 worktree_id: 0,
140 path: Arc::from(Path::new("b1.0")),
141 path_prefix: Arc::default(),
142 distance_to_relative_ancestor: 0,
143 is_dir: false,
144 }),
145 ProjectPanelOrdMatch(PathMatch {
146 score: 1.0,
147 positions: Vec::new(),
148 worktree_id: 0,
149 path: Arc::from(Path::new("c1.0")),
150 path_prefix: Arc::default(),
151 distance_to_relative_ancestor: 0,
152 is_dir: false,
153 }),
154 ProjectPanelOrdMatch(PathMatch {
155 score: 0.5,
156 positions: Vec::new(),
157 worktree_id: 0,
158 path: Arc::from(Path::new("a0.5")),
159 path_prefix: Arc::default(),
160 distance_to_relative_ancestor: 0,
161 is_dir: false,
162 }),
163 ProjectPanelOrdMatch(PathMatch {
164 score: 0.5,
165 positions: Vec::new(),
166 worktree_id: 0,
167 path: Arc::from(Path::new("b0.5")),
168 path_prefix: Arc::default(),
169 distance_to_relative_ancestor: 0,
170 is_dir: false,
171 }),
172 ]
173 );
174}
175
176#[gpui::test]
177async fn test_matching_paths(cx: &mut TestAppContext) {
178 let app_state = init_test(cx);
179 app_state
180 .fs
181 .as_fake()
182 .insert_tree(
183 path!("/root"),
184 json!({
185 "a": {
186 "banana": "",
187 "bandana": "",
188 }
189 }),
190 )
191 .await;
192
193 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
194
195 let (picker, workspace, cx) = build_find_picker(project, cx);
196
197 cx.simulate_input("bna");
198 picker.update(cx, |picker, _| {
199 assert_eq!(picker.delegate.matches.len(), 3);
200 });
201 cx.dispatch_action(SelectNext);
202 cx.dispatch_action(Confirm);
203 cx.read(|cx| {
204 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
205 assert_eq!(active_editor.read(cx).title(cx), "bandana");
206 });
207
208 for bandana_query in [
209 "bandana",
210 "./bandana",
211 ".\\bandana",
212 util::path!("a/bandana"),
213 "b/bandana",
214 "b\\bandana",
215 " bandana",
216 "bandana ",
217 " bandana ",
218 " ndan ",
219 " band ",
220 "a bandana",
221 "bandana:",
222 ] {
223 picker
224 .update_in(cx, |picker, window, cx| {
225 picker
226 .delegate
227 .update_matches(bandana_query.to_string(), window, cx)
228 })
229 .await;
230 picker.update(cx, |picker, _| {
231 assert_eq!(
232 picker.delegate.matches.len(),
233 // existence of CreateNew option depends on whether path already exists
234 if bandana_query == util::path!("a/bandana") {
235 1
236 } else {
237 2
238 },
239 "Wrong number of matches for bandana query '{bandana_query}'. Matches: {:?}",
240 picker.delegate.matches
241 );
242 });
243 cx.dispatch_action(SelectNext);
244 cx.dispatch_action(Confirm);
245 cx.read(|cx| {
246 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
247 assert_eq!(
248 active_editor.read(cx).title(cx),
249 "bandana",
250 "Wrong match for bandana query '{bandana_query}'"
251 );
252 });
253 }
254}
255
256#[gpui::test]
257async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
258 let app_state = init_test(cx);
259 app_state
260 .fs
261 .as_fake()
262 .insert_tree(
263 path!("/root"),
264 json!({
265 "a": {
266 "foo:bar.rs": "",
267 "foo.rs": "",
268 }
269 }),
270 )
271 .await;
272
273 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
274
275 let (picker, _, cx) = build_find_picker(project, cx);
276
277 // 'foo:' matches both files
278 cx.simulate_input("foo:");
279 picker.update(cx, |picker, _| {
280 assert_eq!(picker.delegate.matches.len(), 3);
281 assert_match_at_position(picker, 0, "foo.rs");
282 assert_match_at_position(picker, 1, "foo:bar.rs");
283 });
284
285 // 'foo:b' matches one of the files
286 cx.simulate_input("b");
287 picker.update(cx, |picker, _| {
288 assert_eq!(picker.delegate.matches.len(), 2);
289 assert_match_at_position(picker, 0, "foo:bar.rs");
290 });
291
292 cx.dispatch_action(editor::actions::Backspace);
293
294 // 'foo:1' matches both files, specifying which row to jump to
295 cx.simulate_input("1");
296 picker.update(cx, |picker, _| {
297 assert_eq!(picker.delegate.matches.len(), 3);
298 assert_match_at_position(picker, 0, "foo.rs");
299 assert_match_at_position(picker, 1, "foo:bar.rs");
300 });
301}
302
303#[gpui::test]
304async fn test_unicode_paths(cx: &mut TestAppContext) {
305 let app_state = init_test(cx);
306 app_state
307 .fs
308 .as_fake()
309 .insert_tree(
310 path!("/root"),
311 json!({
312 "a": {
313 "İg": " ",
314 }
315 }),
316 )
317 .await;
318
319 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
320
321 let (picker, workspace, cx) = build_find_picker(project, cx);
322
323 cx.simulate_input("g");
324 picker.update(cx, |picker, _| {
325 assert_eq!(picker.delegate.matches.len(), 2);
326 assert_match_at_position(picker, 1, "g");
327 });
328 cx.dispatch_action(Confirm);
329 cx.read(|cx| {
330 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
331 assert_eq!(active_editor.read(cx).title(cx), "İg");
332 });
333}
334
335#[gpui::test]
336async fn test_absolute_paths(cx: &mut TestAppContext) {
337 let app_state = init_test(cx);
338 app_state
339 .fs
340 .as_fake()
341 .insert_tree(
342 path!("/root"),
343 json!({
344 "a": {
345 "file1.txt": "",
346 "b": {
347 "file2.txt": "",
348 },
349 }
350 }),
351 )
352 .await;
353
354 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
355
356 let (picker, workspace, cx) = build_find_picker(project, cx);
357
358 let matching_abs_path = path!("/root/a/b/file2.txt").to_string();
359 picker
360 .update_in(cx, |picker, window, cx| {
361 picker
362 .delegate
363 .update_matches(matching_abs_path, window, cx)
364 })
365 .await;
366 picker.update(cx, |picker, _| {
367 assert_eq!(
368 collect_search_matches(picker).search_paths_only(),
369 vec![PathBuf::from("a/b/file2.txt")],
370 "Matching abs path should be the only match"
371 )
372 });
373 cx.dispatch_action(SelectNext);
374 cx.dispatch_action(Confirm);
375 cx.read(|cx| {
376 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
377 assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
378 });
379
380 let mismatching_abs_path = path!("/root/a/b/file1.txt").to_string();
381 picker
382 .update_in(cx, |picker, window, cx| {
383 picker
384 .delegate
385 .update_matches(mismatching_abs_path, window, cx)
386 })
387 .await;
388 picker.update(cx, |picker, _| {
389 assert_eq!(
390 collect_search_matches(picker).search_paths_only(),
391 Vec::<PathBuf>::new(),
392 "Mismatching abs path should produce no matches"
393 )
394 });
395}
396
397#[gpui::test]
398async fn test_complex_path(cx: &mut TestAppContext) {
399 let app_state = init_test(cx);
400 app_state
401 .fs
402 .as_fake()
403 .insert_tree(
404 path!("/root"),
405 json!({
406 "其他": {
407 "S数据表格": {
408 "task.xlsx": "some content",
409 },
410 }
411 }),
412 )
413 .await;
414
415 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
416
417 let (picker, workspace, cx) = build_find_picker(project, cx);
418
419 cx.simulate_input("t");
420 picker.update(cx, |picker, _| {
421 assert_eq!(picker.delegate.matches.len(), 2);
422 assert_eq!(
423 collect_search_matches(picker).search_paths_only(),
424 vec![PathBuf::from("其他/S数据表格/task.xlsx")],
425 )
426 });
427 cx.dispatch_action(Confirm);
428 cx.read(|cx| {
429 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
430 assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
431 });
432}
433
434#[gpui::test]
435async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
436 let app_state = init_test(cx);
437
438 let first_file_name = "first.rs";
439 let first_file_contents = "// First Rust file";
440 app_state
441 .fs
442 .as_fake()
443 .insert_tree(
444 path!("/src"),
445 json!({
446 "test": {
447 first_file_name: first_file_contents,
448 "second.rs": "// Second Rust file",
449 }
450 }),
451 )
452 .await;
453
454 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
455
456 let (picker, workspace, cx) = build_find_picker(project, cx);
457
458 let file_query = &first_file_name[..3];
459 let file_row = 1;
460 let file_column = 3;
461 assert!(file_column <= first_file_contents.len());
462 let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
463 picker
464 .update_in(cx, |finder, window, cx| {
465 finder
466 .delegate
467 .update_matches(query_inside_file.to_string(), window, cx)
468 })
469 .await;
470 picker.update(cx, |finder, _| {
471 assert_match_at_position(finder, 1, &query_inside_file.to_string());
472 let finder = &finder.delegate;
473 assert_eq!(finder.matches.len(), 2);
474 let latest_search_query = finder
475 .latest_search_query
476 .as_ref()
477 .expect("Finder should have a query after the update_matches call");
478 assert_eq!(latest_search_query.raw_query, query_inside_file);
479 assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
480 assert_eq!(latest_search_query.path_position.row, Some(file_row));
481 assert_eq!(
482 latest_search_query.path_position.column,
483 Some(file_column as u32)
484 );
485 });
486
487 cx.dispatch_action(Confirm);
488
489 let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
490 cx.executor().advance_clock(Duration::from_secs(2));
491
492 editor.update(cx, |editor, cx| {
493 let all_selections = editor.selections.all_adjusted(cx);
494 assert_eq!(
495 all_selections.len(),
496 1,
497 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
498 );
499 let caret_selection = all_selections.into_iter().next().unwrap();
500 assert_eq!(caret_selection.start, caret_selection.end,
501 "Caret selection should have its start and end at the same position");
502 assert_eq!(file_row, caret_selection.start.row + 1,
503 "Query inside file should get caret with the same focus row");
504 assert_eq!(file_column, caret_selection.start.column as usize + 1,
505 "Query inside file should get caret with the same focus column");
506 });
507}
508
509#[gpui::test]
510async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
511 let app_state = init_test(cx);
512
513 let first_file_name = "first.rs";
514 let first_file_contents = "// First Rust file";
515 app_state
516 .fs
517 .as_fake()
518 .insert_tree(
519 path!("/src"),
520 json!({
521 "test": {
522 first_file_name: first_file_contents,
523 "second.rs": "// Second Rust file",
524 }
525 }),
526 )
527 .await;
528
529 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
530
531 let (picker, workspace, cx) = build_find_picker(project, cx);
532
533 let file_query = &first_file_name[..3];
534 let file_row = 200;
535 let file_column = 300;
536 assert!(file_column > first_file_contents.len());
537 let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
538 picker
539 .update_in(cx, |picker, window, cx| {
540 picker
541 .delegate
542 .update_matches(query_outside_file.to_string(), window, cx)
543 })
544 .await;
545 picker.update(cx, |finder, _| {
546 assert_match_at_position(finder, 1, &query_outside_file.to_string());
547 let delegate = &finder.delegate;
548 assert_eq!(delegate.matches.len(), 2);
549 let latest_search_query = delegate
550 .latest_search_query
551 .as_ref()
552 .expect("Finder should have a query after the update_matches call");
553 assert_eq!(latest_search_query.raw_query, query_outside_file);
554 assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
555 assert_eq!(latest_search_query.path_position.row, Some(file_row));
556 assert_eq!(
557 latest_search_query.path_position.column,
558 Some(file_column as u32)
559 );
560 });
561
562 cx.dispatch_action(Confirm);
563
564 let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
565 cx.executor().advance_clock(Duration::from_secs(2));
566
567 editor.update(cx, |editor, cx| {
568 let all_selections = editor.selections.all_adjusted(cx);
569 assert_eq!(
570 all_selections.len(),
571 1,
572 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
573 );
574 let caret_selection = all_selections.into_iter().next().unwrap();
575 assert_eq!(caret_selection.start, caret_selection.end,
576 "Caret selection should have its start and end at the same position");
577 assert_eq!(0, caret_selection.start.row,
578 "Excessive rows (as in query outside file borders) should get trimmed to last file row");
579 assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
580 "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
581 });
582}
583
584#[gpui::test]
585async fn test_matching_cancellation(cx: &mut TestAppContext) {
586 let app_state = init_test(cx);
587 app_state
588 .fs
589 .as_fake()
590 .insert_tree(
591 "/dir",
592 json!({
593 "hello": "",
594 "goodbye": "",
595 "halogen-light": "",
596 "happiness": "",
597 "height": "",
598 "hi": "",
599 "hiccup": "",
600 }),
601 )
602 .await;
603
604 let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
605
606 let (picker, _, cx) = build_find_picker(project, cx);
607
608 let query = test_path_position("hi");
609 picker
610 .update_in(cx, |picker, window, cx| {
611 picker.delegate.spawn_search(query.clone(), window, cx)
612 })
613 .await;
614
615 picker.update(cx, |picker, _cx| {
616 // CreateNew option not shown in this case since file already exists
617 assert_eq!(picker.delegate.matches.len(), 5);
618 });
619
620 picker.update_in(cx, |picker, window, cx| {
621 let matches = collect_search_matches(picker).search_matches_only();
622 let delegate = &mut picker.delegate;
623
624 // Simulate a search being cancelled after the time limit,
625 // returning only a subset of the matches that would have been found.
626 drop(delegate.spawn_search(query.clone(), window, cx));
627 delegate.set_search_matches(
628 delegate.latest_search_id,
629 true, // did-cancel
630 query.clone(),
631 vec![
632 ProjectPanelOrdMatch(matches[1].clone()),
633 ProjectPanelOrdMatch(matches[3].clone()),
634 ],
635 cx,
636 );
637
638 // Simulate another cancellation.
639 drop(delegate.spawn_search(query.clone(), window, cx));
640 delegate.set_search_matches(
641 delegate.latest_search_id,
642 true, // did-cancel
643 query.clone(),
644 vec![
645 ProjectPanelOrdMatch(matches[0].clone()),
646 ProjectPanelOrdMatch(matches[2].clone()),
647 ProjectPanelOrdMatch(matches[3].clone()),
648 ],
649 cx,
650 );
651
652 assert_eq!(
653 collect_search_matches(picker)
654 .search_matches_only()
655 .as_slice(),
656 &matches[0..4]
657 );
658 });
659}
660
661#[gpui::test]
662async fn test_ignored_root(cx: &mut TestAppContext) {
663 let app_state = init_test(cx);
664 app_state
665 .fs
666 .as_fake()
667 .insert_tree(
668 "/ancestor",
669 json!({
670 ".gitignore": "ignored-root",
671 "ignored-root": {
672 "happiness": "",
673 "height": "",
674 "hi": "",
675 "hiccup": "",
676 },
677 "tracked-root": {
678 ".gitignore": "height*",
679 "happiness": "",
680 "height": "",
681 "heights": {
682 "height_1": "",
683 "height_2": "",
684 },
685 "hi": "",
686 "hiccup": "",
687 },
688 }),
689 )
690 .await;
691
692 let project = Project::test(
693 app_state.fs.clone(),
694 [
695 Path::new(path!("/ancestor/tracked-root")),
696 Path::new(path!("/ancestor/ignored-root")),
697 ],
698 cx,
699 )
700 .await;
701 let (picker, workspace, cx) = build_find_picker(project, cx);
702
703 picker
704 .update_in(cx, |picker, window, cx| {
705 picker
706 .delegate
707 .spawn_search(test_path_position("hi"), window, cx)
708 })
709 .await;
710 picker.update(cx, |picker, _| {
711 let matches = collect_search_matches(picker);
712 assert_eq!(matches.history.len(), 0);
713 assert_eq!(
714 matches.search,
715 vec![
716 PathBuf::from("ignored-root/hi"),
717 PathBuf::from("tracked-root/hi"),
718 PathBuf::from("ignored-root/hiccup"),
719 PathBuf::from("tracked-root/hiccup"),
720 PathBuf::from("ignored-root/height"),
721 PathBuf::from("ignored-root/happiness"),
722 PathBuf::from("tracked-root/happiness"),
723 ],
724 "All ignored files that were indexed are found for default ignored mode"
725 );
726 });
727 cx.dispatch_action(ToggleIncludeIgnored);
728 picker
729 .update_in(cx, |picker, window, cx| {
730 picker
731 .delegate
732 .spawn_search(test_path_position("hi"), window, cx)
733 })
734 .await;
735 picker.update(cx, |picker, _| {
736 let matches = collect_search_matches(picker);
737 assert_eq!(matches.history.len(), 0);
738 assert_eq!(
739 matches.search,
740 vec![
741 PathBuf::from("ignored-root/hi"),
742 PathBuf::from("tracked-root/hi"),
743 PathBuf::from("ignored-root/hiccup"),
744 PathBuf::from("tracked-root/hiccup"),
745 PathBuf::from("ignored-root/height"),
746 PathBuf::from("tracked-root/height"),
747 PathBuf::from("ignored-root/happiness"),
748 PathBuf::from("tracked-root/happiness"),
749 ],
750 "All ignored files should be found, for the toggled on ignored mode"
751 );
752 });
753
754 picker
755 .update_in(cx, |picker, window, cx| {
756 picker.delegate.include_ignored = Some(false);
757 picker
758 .delegate
759 .spawn_search(test_path_position("hi"), window, cx)
760 })
761 .await;
762 picker.update(cx, |picker, _| {
763 let matches = collect_search_matches(picker);
764 assert_eq!(matches.history.len(), 0);
765 assert_eq!(
766 matches.search,
767 vec![
768 PathBuf::from("tracked-root/hi"),
769 PathBuf::from("tracked-root/hiccup"),
770 PathBuf::from("tracked-root/happiness"),
771 ],
772 "Only non-ignored files should be found for the turned off ignored mode"
773 );
774 });
775
776 workspace
777 .update_in(cx, |workspace, window, cx| {
778 workspace.open_abs_path(
779 PathBuf::from(path!("/ancestor/tracked-root/heights/height_1")),
780 OpenOptions {
781 visible: Some(OpenVisible::None),
782 ..OpenOptions::default()
783 },
784 window,
785 cx,
786 )
787 })
788 .await
789 .unwrap();
790 cx.run_until_parked();
791 workspace
792 .update_in(cx, |workspace, window, cx| {
793 workspace.active_pane().update(cx, |pane, cx| {
794 pane.close_active_item(&CloseActiveItem::default(), window, cx)
795 })
796 })
797 .await
798 .unwrap();
799 cx.run_until_parked();
800
801 picker
802 .update_in(cx, |picker, window, cx| {
803 picker.delegate.include_ignored = None;
804 picker
805 .delegate
806 .spawn_search(test_path_position("hi"), window, cx)
807 })
808 .await;
809 picker.update(cx, |picker, _| {
810 let matches = collect_search_matches(picker);
811 assert_eq!(matches.history.len(), 0);
812 assert_eq!(
813 matches.search,
814 vec![
815 PathBuf::from("ignored-root/hi"),
816 PathBuf::from("tracked-root/hi"),
817 PathBuf::from("ignored-root/hiccup"),
818 PathBuf::from("tracked-root/hiccup"),
819 PathBuf::from("ignored-root/height"),
820 PathBuf::from("ignored-root/happiness"),
821 PathBuf::from("tracked-root/happiness"),
822 ],
823 "Only for the worktree with the ignored root, all indexed ignored files are found in the auto ignored mode"
824 );
825 });
826
827 picker
828 .update_in(cx, |picker, window, cx| {
829 picker.delegate.include_ignored = Some(true);
830 picker
831 .delegate
832 .spawn_search(test_path_position("hi"), window, cx)
833 })
834 .await;
835 picker.update(cx, |picker, _| {
836 let matches = collect_search_matches(picker);
837 assert_eq!(matches.history.len(), 0);
838 assert_eq!(
839 matches.search,
840 vec![
841 PathBuf::from("ignored-root/hi"),
842 PathBuf::from("tracked-root/hi"),
843 PathBuf::from("ignored-root/hiccup"),
844 PathBuf::from("tracked-root/hiccup"),
845 PathBuf::from("ignored-root/height"),
846 PathBuf::from("tracked-root/height"),
847 PathBuf::from("tracked-root/heights/height_1"),
848 PathBuf::from("tracked-root/heights/height_2"),
849 PathBuf::from("ignored-root/happiness"),
850 PathBuf::from("tracked-root/happiness"),
851 ],
852 "All ignored files that were indexed are found in the turned on ignored mode"
853 );
854 });
855
856 picker
857 .update_in(cx, |picker, window, cx| {
858 picker.delegate.include_ignored = Some(false);
859 picker
860 .delegate
861 .spawn_search(test_path_position("hi"), window, cx)
862 })
863 .await;
864 picker.update(cx, |picker, _| {
865 let matches = collect_search_matches(picker);
866 assert_eq!(matches.history.len(), 0);
867 assert_eq!(
868 matches.search,
869 vec![
870 PathBuf::from("tracked-root/hi"),
871 PathBuf::from("tracked-root/hiccup"),
872 PathBuf::from("tracked-root/happiness"),
873 ],
874 "Only non-ignored files should be found for the turned off ignored mode"
875 );
876 });
877}
878
879#[gpui::test]
880async fn test_single_file_worktrees(cx: &mut TestAppContext) {
881 let app_state = init_test(cx);
882 app_state
883 .fs
884 .as_fake()
885 .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
886 .await;
887
888 let project = Project::test(
889 app_state.fs.clone(),
890 ["/root/the-parent-dir/the-file".as_ref()],
891 cx,
892 )
893 .await;
894
895 let (picker, _, cx) = build_find_picker(project, cx);
896
897 // Even though there is only one worktree, that worktree's filename
898 // is included in the matching, because the worktree is a single file.
899 picker
900 .update_in(cx, |picker, window, cx| {
901 picker
902 .delegate
903 .spawn_search(test_path_position("thf"), window, cx)
904 })
905 .await;
906 cx.read(|cx| {
907 let picker = picker.read(cx);
908 let delegate = &picker.delegate;
909 let matches = collect_search_matches(picker).search_matches_only();
910 assert_eq!(matches.len(), 1);
911
912 let (file_name, file_name_positions, full_path, full_path_positions) =
913 delegate.labels_for_path_match(&matches[0]);
914 assert_eq!(file_name, "the-file");
915 assert_eq!(file_name_positions, &[0, 1, 4]);
916 assert_eq!(full_path, "");
917 assert_eq!(full_path_positions, &[0; 0]);
918 });
919
920 // Since the worktree root is a file, searching for its name followed by a slash does
921 // not match anything.
922 picker
923 .update_in(cx, |picker, window, cx| {
924 picker
925 .delegate
926 .spawn_search(test_path_position("thf/"), window, cx)
927 })
928 .await;
929 picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
930}
931
932#[gpui::test]
933async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
934 let app_state = init_test(cx);
935 app_state
936 .fs
937 .as_fake()
938 .insert_tree(
939 path!("/roota"),
940 json!({ "the-parent-dira": { "filea": "" } }),
941 )
942 .await;
943
944 app_state
945 .fs
946 .as_fake()
947 .insert_tree(
948 path!("/rootb"),
949 json!({ "the-parent-dirb": { "fileb": "" } }),
950 )
951 .await;
952
953 let project = Project::test(
954 app_state.fs.clone(),
955 [path!("/roota").as_ref(), path!("/rootb").as_ref()],
956 cx,
957 )
958 .await;
959
960 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
961 let (_worktree_id1, worktree_id2) = cx.read(|cx| {
962 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
963 (
964 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize),
965 WorktreeId::from_usize(worktrees[1].entity_id().as_u64() as usize),
966 )
967 });
968
969 let b_path = ProjectPath {
970 worktree_id: worktree_id2,
971 path: Arc::from(Path::new(path!("the-parent-dirb/fileb"))),
972 };
973 workspace
974 .update_in(cx, |workspace, window, cx| {
975 workspace.open_path(b_path, None, true, window, cx)
976 })
977 .await
978 .unwrap();
979
980 let finder = open_file_picker(&workspace, cx);
981
982 finder
983 .update_in(cx, |f, window, cx| {
984 f.delegate.spawn_search(
985 test_path_position(path!("the-parent-dirb/filec")),
986 window,
987 cx,
988 )
989 })
990 .await;
991 cx.run_until_parked();
992 finder.update_in(cx, |picker, window, cx| {
993 assert_eq!(picker.delegate.matches.len(), 1);
994 picker.delegate.confirm(false, window, cx)
995 });
996 cx.run_until_parked();
997 cx.read(|cx| {
998 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
999 let project_path = active_editor.read(cx).project_path(cx);
1000 assert_eq!(
1001 project_path,
1002 Some(ProjectPath {
1003 worktree_id: worktree_id2,
1004 path: Arc::from(Path::new(path!("the-parent-dirb/filec")))
1005 })
1006 );
1007 });
1008}
1009
1010#[gpui::test]
1011async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppContext) {
1012 let app_state = init_test(cx);
1013 app_state
1014 .fs
1015 .as_fake()
1016 .insert_tree(
1017 path!("/roota"),
1018 json!({ "the-parent-dira": { "filea": "" } }),
1019 )
1020 .await;
1021
1022 app_state
1023 .fs
1024 .as_fake()
1025 .insert_tree(
1026 path!("/rootb"),
1027 json!({ "the-parent-dirb": { "fileb": "" } }),
1028 )
1029 .await;
1030
1031 let project = Project::test(
1032 app_state.fs.clone(),
1033 [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1034 cx,
1035 )
1036 .await;
1037
1038 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1039 let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1040 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1041 (
1042 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize),
1043 WorktreeId::from_usize(worktrees[1].entity_id().as_u64() as usize),
1044 )
1045 });
1046
1047 let finder = open_file_picker(&workspace, cx);
1048
1049 finder
1050 .update_in(cx, |f, window, cx| {
1051 f.delegate
1052 .spawn_search(test_path_position(path!("rootb/filec")), window, cx)
1053 })
1054 .await;
1055 cx.run_until_parked();
1056 finder.update_in(cx, |picker, window, cx| {
1057 assert_eq!(picker.delegate.matches.len(), 1);
1058 picker.delegate.confirm(false, window, cx)
1059 });
1060 cx.run_until_parked();
1061 cx.read(|cx| {
1062 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1063 let project_path = active_editor.read(cx).project_path(cx);
1064 assert_eq!(
1065 project_path,
1066 Some(ProjectPath {
1067 worktree_id: worktree_id2,
1068 path: Arc::from(Path::new("filec"))
1069 })
1070 );
1071 });
1072}
1073
1074#[gpui::test]
1075async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1076 let app_state = init_test(cx);
1077 app_state
1078 .fs
1079 .as_fake()
1080 .insert_tree(
1081 path!("/root"),
1082 json!({
1083 "dir1": { "a.txt": "" },
1084 "dir2": {
1085 "a.txt": "",
1086 "b.txt": ""
1087 }
1088 }),
1089 )
1090 .await;
1091
1092 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1093 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1094
1095 let worktree_id = cx.read(|cx| {
1096 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1097 assert_eq!(worktrees.len(), 1);
1098 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1099 });
1100
1101 // When workspace has an active item, sort items which are closer to that item
1102 // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1103 // so that one should be sorted earlier
1104 let b_path = ProjectPath {
1105 worktree_id,
1106 path: Arc::from(Path::new("dir2/b.txt")),
1107 };
1108 workspace
1109 .update_in(cx, |workspace, window, cx| {
1110 workspace.open_path(b_path, None, true, window, cx)
1111 })
1112 .await
1113 .unwrap();
1114 let finder = open_file_picker(&workspace, cx);
1115 finder
1116 .update_in(cx, |f, window, cx| {
1117 f.delegate
1118 .spawn_search(test_path_position("a.txt"), window, cx)
1119 })
1120 .await;
1121
1122 finder.update(cx, |picker, _| {
1123 let matches = collect_search_matches(picker).search_paths_only();
1124 assert_eq!(matches[0].as_path(), Path::new("dir2/a.txt"));
1125 assert_eq!(matches[1].as_path(), Path::new("dir1/a.txt"));
1126 });
1127}
1128
1129#[gpui::test]
1130async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1131 let app_state = init_test(cx);
1132 app_state
1133 .fs
1134 .as_fake()
1135 .insert_tree(
1136 "/root",
1137 json!({
1138 "dir1": {},
1139 "dir2": {
1140 "dir3": {}
1141 }
1142 }),
1143 )
1144 .await;
1145
1146 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1147 let (picker, _workspace, cx) = build_find_picker(project, cx);
1148
1149 picker
1150 .update_in(cx, |f, window, cx| {
1151 f.delegate
1152 .spawn_search(test_path_position("dir"), window, cx)
1153 })
1154 .await;
1155 cx.read(|cx| {
1156 let finder = picker.read(cx);
1157 assert_eq!(finder.delegate.matches.len(), 1);
1158 assert_match_at_position(finder, 0, "dir");
1159 });
1160}
1161
1162#[gpui::test]
1163async fn test_query_history(cx: &mut gpui::TestAppContext) {
1164 let app_state = init_test(cx);
1165
1166 app_state
1167 .fs
1168 .as_fake()
1169 .insert_tree(
1170 path!("/src"),
1171 json!({
1172 "test": {
1173 "first.rs": "// First Rust file",
1174 "second.rs": "// Second Rust file",
1175 "third.rs": "// Third Rust file",
1176 }
1177 }),
1178 )
1179 .await;
1180
1181 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1182 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1183 let worktree_id = cx.read(|cx| {
1184 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1185 assert_eq!(worktrees.len(), 1);
1186 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1187 });
1188
1189 // Open and close panels, getting their history items afterwards.
1190 // Ensure history items get populated with opened items, and items are kept in a certain order.
1191 // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1192 //
1193 // TODO: without closing, the opened items do not propagate their history changes for some reason
1194 // it does work in real app though, only tests do not propagate.
1195 workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1196
1197 let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1198 assert!(
1199 initial_history.is_empty(),
1200 "Should have no history before opening any files"
1201 );
1202
1203 let history_after_first =
1204 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1205 assert_eq!(
1206 history_after_first,
1207 vec![FoundPath::new(
1208 ProjectPath {
1209 worktree_id,
1210 path: Arc::from(Path::new("test/first.rs")),
1211 },
1212 Some(PathBuf::from(path!("/src/test/first.rs")))
1213 )],
1214 "Should show 1st opened item in the history when opening the 2nd item"
1215 );
1216
1217 let history_after_second =
1218 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1219 assert_eq!(
1220 history_after_second,
1221 vec![
1222 FoundPath::new(
1223 ProjectPath {
1224 worktree_id,
1225 path: Arc::from(Path::new("test/second.rs")),
1226 },
1227 Some(PathBuf::from(path!("/src/test/second.rs")))
1228 ),
1229 FoundPath::new(
1230 ProjectPath {
1231 worktree_id,
1232 path: Arc::from(Path::new("test/first.rs")),
1233 },
1234 Some(PathBuf::from(path!("/src/test/first.rs")))
1235 ),
1236 ],
1237 "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1238 2nd item should be the first in the history, as the last opened."
1239 );
1240
1241 let history_after_third =
1242 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1243 assert_eq!(
1244 history_after_third,
1245 vec![
1246 FoundPath::new(
1247 ProjectPath {
1248 worktree_id,
1249 path: Arc::from(Path::new("test/third.rs")),
1250 },
1251 Some(PathBuf::from(path!("/src/test/third.rs")))
1252 ),
1253 FoundPath::new(
1254 ProjectPath {
1255 worktree_id,
1256 path: Arc::from(Path::new("test/second.rs")),
1257 },
1258 Some(PathBuf::from(path!("/src/test/second.rs")))
1259 ),
1260 FoundPath::new(
1261 ProjectPath {
1262 worktree_id,
1263 path: Arc::from(Path::new("test/first.rs")),
1264 },
1265 Some(PathBuf::from(path!("/src/test/first.rs")))
1266 ),
1267 ],
1268 "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1269 3rd item should be the first in the history, as the last opened."
1270 );
1271
1272 let history_after_second_again =
1273 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1274 assert_eq!(
1275 history_after_second_again,
1276 vec![
1277 FoundPath::new(
1278 ProjectPath {
1279 worktree_id,
1280 path: Arc::from(Path::new("test/second.rs")),
1281 },
1282 Some(PathBuf::from(path!("/src/test/second.rs")))
1283 ),
1284 FoundPath::new(
1285 ProjectPath {
1286 worktree_id,
1287 path: Arc::from(Path::new("test/third.rs")),
1288 },
1289 Some(PathBuf::from(path!("/src/test/third.rs")))
1290 ),
1291 FoundPath::new(
1292 ProjectPath {
1293 worktree_id,
1294 path: Arc::from(Path::new("test/first.rs")),
1295 },
1296 Some(PathBuf::from(path!("/src/test/first.rs")))
1297 ),
1298 ],
1299 "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1300 2nd item, as the last opened, 3rd item should go next as it was opened right before."
1301 );
1302}
1303
1304#[gpui::test]
1305async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1306 let app_state = init_test(cx);
1307
1308 app_state
1309 .fs
1310 .as_fake()
1311 .insert_tree(
1312 path!("/src"),
1313 json!({
1314 "test": {
1315 "first.rs": "// First Rust file",
1316 "second.rs": "// Second Rust file",
1317 }
1318 }),
1319 )
1320 .await;
1321
1322 app_state
1323 .fs
1324 .as_fake()
1325 .insert_tree(
1326 path!("/external-src"),
1327 json!({
1328 "test": {
1329 "third.rs": "// Third Rust file",
1330 "fourth.rs": "// Fourth Rust file",
1331 }
1332 }),
1333 )
1334 .await;
1335
1336 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1337 cx.update(|cx| {
1338 project.update(cx, |project, cx| {
1339 project.find_or_create_worktree(path!("/external-src"), false, cx)
1340 })
1341 })
1342 .detach();
1343 cx.background_executor.run_until_parked();
1344
1345 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1346 let worktree_id = cx.read(|cx| {
1347 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1348 assert_eq!(worktrees.len(), 1,);
1349
1350 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1351 });
1352 workspace
1353 .update_in(cx, |workspace, window, cx| {
1354 workspace.open_abs_path(
1355 PathBuf::from(path!("/external-src/test/third.rs")),
1356 OpenOptions {
1357 visible: Some(OpenVisible::None),
1358 ..Default::default()
1359 },
1360 window,
1361 cx,
1362 )
1363 })
1364 .detach();
1365 cx.background_executor.run_until_parked();
1366 let external_worktree_id = cx.read(|cx| {
1367 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1368 assert_eq!(
1369 worktrees.len(),
1370 2,
1371 "External file should get opened in a new worktree"
1372 );
1373
1374 WorktreeId::from_usize(
1375 worktrees
1376 .into_iter()
1377 .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
1378 .expect("New worktree should have a different id")
1379 .entity_id()
1380 .as_u64() as usize,
1381 )
1382 });
1383 cx.dispatch_action(workspace::CloseActiveItem {
1384 save_intent: None,
1385 close_pinned: false,
1386 });
1387
1388 let initial_history_items =
1389 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1390 assert_eq!(
1391 initial_history_items,
1392 vec![FoundPath::new(
1393 ProjectPath {
1394 worktree_id: external_worktree_id,
1395 path: Arc::from(Path::new("")),
1396 },
1397 Some(PathBuf::from(path!("/external-src/test/third.rs")))
1398 )],
1399 "Should show external file with its full path in the history after it was open"
1400 );
1401
1402 let updated_history_items =
1403 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1404 assert_eq!(
1405 updated_history_items,
1406 vec![
1407 FoundPath::new(
1408 ProjectPath {
1409 worktree_id,
1410 path: Arc::from(Path::new("test/second.rs")),
1411 },
1412 Some(PathBuf::from(path!("/src/test/second.rs")))
1413 ),
1414 FoundPath::new(
1415 ProjectPath {
1416 worktree_id: external_worktree_id,
1417 path: Arc::from(Path::new("")),
1418 },
1419 Some(PathBuf::from(path!("/external-src/test/third.rs")))
1420 ),
1421 ],
1422 "Should keep external file with history updates",
1423 );
1424}
1425
1426#[gpui::test]
1427async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1428 let app_state = init_test(cx);
1429
1430 app_state
1431 .fs
1432 .as_fake()
1433 .insert_tree(
1434 path!("/src"),
1435 json!({
1436 "test": {
1437 "first.rs": "// First Rust file",
1438 "second.rs": "// Second Rust file",
1439 "third.rs": "// Third Rust file",
1440 }
1441 }),
1442 )
1443 .await;
1444
1445 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1446 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1447
1448 // generate some history to select from
1449 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1450 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1451 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1452 let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1453
1454 for expected_selected_index in 0..current_history.len() {
1455 cx.dispatch_action(ToggleFileFinder::default());
1456 let picker = active_file_picker(&workspace, cx);
1457 let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
1458 assert_eq!(
1459 selected_index, expected_selected_index,
1460 "Should select the next item in the history"
1461 );
1462 }
1463
1464 cx.dispatch_action(ToggleFileFinder::default());
1465 let selected_index = workspace.update(cx, |workspace, cx| {
1466 workspace
1467 .active_modal::<FileFinder>(cx)
1468 .unwrap()
1469 .read(cx)
1470 .picker
1471 .read(cx)
1472 .delegate
1473 .selected_index()
1474 });
1475 assert_eq!(
1476 selected_index, 0,
1477 "Should wrap around the history and start all over"
1478 );
1479}
1480
1481#[gpui::test]
1482async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1483 let app_state = init_test(cx);
1484
1485 app_state
1486 .fs
1487 .as_fake()
1488 .insert_tree(
1489 path!("/src"),
1490 json!({
1491 "test": {
1492 "first.rs": "// First Rust file",
1493 "second.rs": "// Second Rust file",
1494 "third.rs": "// Third Rust file",
1495 "fourth.rs": "// Fourth Rust file",
1496 }
1497 }),
1498 )
1499 .await;
1500
1501 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1502 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1503 let worktree_id = cx.read(|cx| {
1504 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1505 assert_eq!(worktrees.len(), 1,);
1506
1507 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1508 });
1509
1510 // generate some history to select from
1511 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1512 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1513 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1514 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1515
1516 let finder = open_file_picker(&workspace, cx);
1517 let first_query = "f";
1518 finder
1519 .update_in(cx, |finder, window, cx| {
1520 finder
1521 .delegate
1522 .update_matches(first_query.to_string(), window, cx)
1523 })
1524 .await;
1525 finder.update(cx, |picker, _| {
1526 let matches = collect_search_matches(picker);
1527 assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1528 let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1529 assert_eq!(history_match, &FoundPath::new(
1530 ProjectPath {
1531 worktree_id,
1532 path: Arc::from(Path::new("test/first.rs")),
1533 },
1534 Some(PathBuf::from(path!("/src/test/first.rs")))
1535 ));
1536 assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1537 assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
1538 });
1539
1540 let second_query = "fsdasdsa";
1541 let finder = active_file_picker(&workspace, cx);
1542 finder
1543 .update_in(cx, |finder, window, cx| {
1544 finder
1545 .delegate
1546 .update_matches(second_query.to_string(), window, cx)
1547 })
1548 .await;
1549 finder.update(cx, |picker, _| {
1550 assert!(
1551 collect_search_matches(picker)
1552 .search_paths_only()
1553 .is_empty(),
1554 "No search entries should match {second_query}"
1555 );
1556 });
1557
1558 let first_query_again = first_query;
1559
1560 let finder = active_file_picker(&workspace, cx);
1561 finder
1562 .update_in(cx, |finder, window, cx| {
1563 finder
1564 .delegate
1565 .update_matches(first_query_again.to_string(), window, cx)
1566 })
1567 .await;
1568 finder.update(cx, |picker, _| {
1569 let matches = collect_search_matches(picker);
1570 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");
1571 let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1572 assert_eq!(history_match, &FoundPath::new(
1573 ProjectPath {
1574 worktree_id,
1575 path: Arc::from(Path::new("test/first.rs")),
1576 },
1577 Some(PathBuf::from(path!("/src/test/first.rs")))
1578 ));
1579 assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1580 assert_eq!(matches.search.first().unwrap(), Path::new("test/fourth.rs"));
1581 });
1582}
1583
1584#[gpui::test]
1585async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1586 let app_state = init_test(cx);
1587
1588 app_state
1589 .fs
1590 .as_fake()
1591 .insert_tree(
1592 path!("/root"),
1593 json!({
1594 "test": {
1595 "1_qw": "// First file that matches the query",
1596 "2_second": "// Second file",
1597 "3_third": "// Third file",
1598 "4_fourth": "// Fourth file",
1599 "5_qwqwqw": "// A file with 3 more matches than the first one",
1600 "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1601 "7_qwqwqw": "// One more, same amount of query matches as above",
1602 }
1603 }),
1604 )
1605 .await;
1606
1607 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1608 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1609 // generate some history to select from
1610 open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1611 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1612 open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1613 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1614 open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1615
1616 let finder = open_file_picker(&workspace, cx);
1617 let query = "qw";
1618 finder
1619 .update_in(cx, |finder, window, cx| {
1620 finder
1621 .delegate
1622 .update_matches(query.to_string(), window, cx)
1623 })
1624 .await;
1625 finder.update(cx, |finder, _| {
1626 let search_matches = collect_search_matches(finder);
1627 assert_eq!(
1628 search_matches.history,
1629 vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
1630 );
1631 assert_eq!(
1632 search_matches.search,
1633 vec![
1634 PathBuf::from("test/5_qwqwqw"),
1635 PathBuf::from("test/7_qwqwqw"),
1636 ],
1637 );
1638 });
1639}
1640
1641#[gpui::test]
1642async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1643 let app_state = init_test(cx);
1644
1645 app_state
1646 .fs
1647 .as_fake()
1648 .insert_tree(
1649 path!("/root"),
1650 json!({
1651 "test": {
1652 "1_qw": "",
1653 }
1654 }),
1655 )
1656 .await;
1657
1658 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1659 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1660 // Open new buffer
1661 open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1662
1663 let picker = open_file_picker(&workspace, cx);
1664 picker.update(cx, |finder, _| {
1665 assert_match_selection(finder, 0, "1_qw");
1666 });
1667}
1668
1669#[gpui::test]
1670async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1671 cx: &mut TestAppContext,
1672) {
1673 let app_state = init_test(cx);
1674
1675 app_state
1676 .fs
1677 .as_fake()
1678 .insert_tree(
1679 path!("/src"),
1680 json!({
1681 "test": {
1682 "bar.rs": "// Bar file",
1683 "lib.rs": "// Lib file",
1684 "maaa.rs": "// Maaaaaaa",
1685 "main.rs": "// Main file",
1686 "moo.rs": "// Moooooo",
1687 }
1688 }),
1689 )
1690 .await;
1691
1692 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1693 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1694
1695 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1696 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1697 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1698
1699 // main.rs is on top, previously used is selected
1700 let picker = open_file_picker(&workspace, cx);
1701 picker.update(cx, |finder, _| {
1702 assert_eq!(finder.delegate.matches.len(), 3);
1703 assert_match_selection(finder, 0, "main.rs");
1704 assert_match_at_position(finder, 1, "lib.rs");
1705 assert_match_at_position(finder, 2, "bar.rs");
1706 });
1707
1708 // all files match, main.rs is still on top, but the second item is selected
1709 picker
1710 .update_in(cx, |finder, window, cx| {
1711 finder
1712 .delegate
1713 .update_matches(".rs".to_string(), window, cx)
1714 })
1715 .await;
1716 picker.update(cx, |finder, _| {
1717 assert_eq!(finder.delegate.matches.len(), 6);
1718 assert_match_at_position(finder, 0, "main.rs");
1719 assert_match_selection(finder, 1, "bar.rs");
1720 assert_match_at_position(finder, 2, "lib.rs");
1721 assert_match_at_position(finder, 3, "moo.rs");
1722 assert_match_at_position(finder, 4, "maaa.rs");
1723 assert_match_at_position(finder, 5, ".rs");
1724 });
1725
1726 // main.rs is not among matches, select top item
1727 picker
1728 .update_in(cx, |finder, window, cx| {
1729 finder.delegate.update_matches("b".to_string(), window, cx)
1730 })
1731 .await;
1732 picker.update(cx, |finder, _| {
1733 assert_eq!(finder.delegate.matches.len(), 3);
1734 assert_match_at_position(finder, 0, "bar.rs");
1735 assert_match_at_position(finder, 1, "lib.rs");
1736 assert_match_at_position(finder, 2, "b");
1737 });
1738
1739 // main.rs is back, put it on top and select next item
1740 picker
1741 .update_in(cx, |finder, window, cx| {
1742 finder.delegate.update_matches("m".to_string(), window, cx)
1743 })
1744 .await;
1745 picker.update(cx, |finder, _| {
1746 assert_eq!(finder.delegate.matches.len(), 4);
1747 assert_match_at_position(finder, 0, "main.rs");
1748 assert_match_selection(finder, 1, "moo.rs");
1749 assert_match_at_position(finder, 2, "maaa.rs");
1750 assert_match_at_position(finder, 3, "m");
1751 });
1752
1753 // get back to the initial state
1754 picker
1755 .update_in(cx, |finder, window, cx| {
1756 finder.delegate.update_matches("".to_string(), window, cx)
1757 })
1758 .await;
1759 picker.update(cx, |finder, _| {
1760 assert_eq!(finder.delegate.matches.len(), 3);
1761 assert_match_selection(finder, 0, "main.rs");
1762 assert_match_at_position(finder, 1, "lib.rs");
1763 assert_match_at_position(finder, 2, "bar.rs");
1764 });
1765}
1766
1767#[gpui::test]
1768async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
1769 let app_state = init_test(cx);
1770
1771 cx.update(|cx| {
1772 let settings = *FileFinderSettings::get_global(cx);
1773
1774 FileFinderSettings::override_global(
1775 FileFinderSettings {
1776 skip_focus_for_active_in_search: false,
1777 ..settings
1778 },
1779 cx,
1780 );
1781 });
1782
1783 app_state
1784 .fs
1785 .as_fake()
1786 .insert_tree(
1787 path!("/src"),
1788 json!({
1789 "test": {
1790 "bar.rs": "// Bar file",
1791 "lib.rs": "// Lib file",
1792 "maaa.rs": "// Maaaaaaa",
1793 "main.rs": "// Main file",
1794 "moo.rs": "// Moooooo",
1795 }
1796 }),
1797 )
1798 .await;
1799
1800 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1801 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1802
1803 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1804 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1805 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1806
1807 // main.rs is on top, previously used is selected
1808 let picker = open_file_picker(&workspace, cx);
1809 picker.update(cx, |finder, _| {
1810 assert_eq!(finder.delegate.matches.len(), 3);
1811 assert_match_selection(finder, 0, "main.rs");
1812 assert_match_at_position(finder, 1, "lib.rs");
1813 assert_match_at_position(finder, 2, "bar.rs");
1814 });
1815
1816 // all files match, main.rs is on top, and is selected
1817 picker
1818 .update_in(cx, |finder, window, cx| {
1819 finder
1820 .delegate
1821 .update_matches(".rs".to_string(), window, cx)
1822 })
1823 .await;
1824 picker.update(cx, |finder, _| {
1825 assert_eq!(finder.delegate.matches.len(), 6);
1826 assert_match_selection(finder, 0, "main.rs");
1827 assert_match_at_position(finder, 1, "bar.rs");
1828 assert_match_at_position(finder, 2, "lib.rs");
1829 assert_match_at_position(finder, 3, "moo.rs");
1830 assert_match_at_position(finder, 4, "maaa.rs");
1831 assert_match_at_position(finder, 5, ".rs");
1832 });
1833}
1834
1835#[gpui::test]
1836async fn test_non_separate_history_items(cx: &mut TestAppContext) {
1837 let app_state = init_test(cx);
1838
1839 app_state
1840 .fs
1841 .as_fake()
1842 .insert_tree(
1843 path!("/src"),
1844 json!({
1845 "test": {
1846 "bar.rs": "// Bar file",
1847 "lib.rs": "// Lib file",
1848 "maaa.rs": "// Maaaaaaa",
1849 "main.rs": "// Main file",
1850 "moo.rs": "// Moooooo",
1851 }
1852 }),
1853 )
1854 .await;
1855
1856 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1857 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1858
1859 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1860 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1861 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1862
1863 cx.dispatch_action(ToggleFileFinder::default());
1864 let picker = active_file_picker(&workspace, cx);
1865 // main.rs is on top, previously used is selected
1866 picker.update(cx, |finder, _| {
1867 assert_eq!(finder.delegate.matches.len(), 3);
1868 assert_match_selection(finder, 0, "main.rs");
1869 assert_match_at_position(finder, 1, "lib.rs");
1870 assert_match_at_position(finder, 2, "bar.rs");
1871 });
1872
1873 // all files match, main.rs is still on top, but the second item is selected
1874 picker
1875 .update_in(cx, |finder, window, cx| {
1876 finder
1877 .delegate
1878 .update_matches(".rs".to_string(), window, cx)
1879 })
1880 .await;
1881 picker.update(cx, |finder, _| {
1882 assert_eq!(finder.delegate.matches.len(), 6);
1883 assert_match_at_position(finder, 0, "main.rs");
1884 assert_match_selection(finder, 1, "moo.rs");
1885 assert_match_at_position(finder, 2, "bar.rs");
1886 assert_match_at_position(finder, 3, "lib.rs");
1887 assert_match_at_position(finder, 4, "maaa.rs");
1888 assert_match_at_position(finder, 5, ".rs");
1889 });
1890
1891 // main.rs is not among matches, select top item
1892 picker
1893 .update_in(cx, |finder, window, cx| {
1894 finder.delegate.update_matches("b".to_string(), window, cx)
1895 })
1896 .await;
1897 picker.update(cx, |finder, _| {
1898 assert_eq!(finder.delegate.matches.len(), 3);
1899 assert_match_at_position(finder, 0, "bar.rs");
1900 assert_match_at_position(finder, 1, "lib.rs");
1901 assert_match_at_position(finder, 2, "b");
1902 });
1903
1904 // main.rs is back, put it on top and select next item
1905 picker
1906 .update_in(cx, |finder, window, cx| {
1907 finder.delegate.update_matches("m".to_string(), window, cx)
1908 })
1909 .await;
1910 picker.update(cx, |finder, _| {
1911 assert_eq!(finder.delegate.matches.len(), 4);
1912 assert_match_at_position(finder, 0, "main.rs");
1913 assert_match_selection(finder, 1, "moo.rs");
1914 assert_match_at_position(finder, 2, "maaa.rs");
1915 assert_match_at_position(finder, 3, "m");
1916 });
1917
1918 // get back to the initial state
1919 picker
1920 .update_in(cx, |finder, window, cx| {
1921 finder.delegate.update_matches("".to_string(), window, cx)
1922 })
1923 .await;
1924 picker.update(cx, |finder, _| {
1925 assert_eq!(finder.delegate.matches.len(), 3);
1926 assert_match_selection(finder, 0, "main.rs");
1927 assert_match_at_position(finder, 1, "lib.rs");
1928 assert_match_at_position(finder, 2, "bar.rs");
1929 });
1930}
1931
1932#[gpui::test]
1933async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1934 let app_state = init_test(cx);
1935
1936 app_state
1937 .fs
1938 .as_fake()
1939 .insert_tree(
1940 path!("/test"),
1941 json!({
1942 "test": {
1943 "1.txt": "// One",
1944 "2.txt": "// Two",
1945 "3.txt": "// Three",
1946 }
1947 }),
1948 )
1949 .await;
1950
1951 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
1952 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1953
1954 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1955 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1956 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1957
1958 let picker = open_file_picker(&workspace, cx);
1959 picker.update(cx, |finder, _| {
1960 assert_eq!(finder.delegate.matches.len(), 3);
1961 assert_match_selection(finder, 0, "3.txt");
1962 assert_match_at_position(finder, 1, "2.txt");
1963 assert_match_at_position(finder, 2, "1.txt");
1964 });
1965
1966 cx.dispatch_action(SelectNext);
1967 cx.dispatch_action(Confirm); // Open 2.txt
1968
1969 let picker = open_file_picker(&workspace, cx);
1970 picker.update(cx, |finder, _| {
1971 assert_eq!(finder.delegate.matches.len(), 3);
1972 assert_match_selection(finder, 0, "2.txt");
1973 assert_match_at_position(finder, 1, "3.txt");
1974 assert_match_at_position(finder, 2, "1.txt");
1975 });
1976
1977 cx.dispatch_action(SelectNext);
1978 cx.dispatch_action(SelectNext);
1979 cx.dispatch_action(Confirm); // Open 1.txt
1980
1981 let picker = open_file_picker(&workspace, cx);
1982 picker.update(cx, |finder, _| {
1983 assert_eq!(finder.delegate.matches.len(), 3);
1984 assert_match_selection(finder, 0, "1.txt");
1985 assert_match_at_position(finder, 1, "2.txt");
1986 assert_match_at_position(finder, 2, "3.txt");
1987 });
1988}
1989
1990#[gpui::test]
1991async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
1992 let app_state = init_test(cx);
1993
1994 app_state
1995 .fs
1996 .as_fake()
1997 .insert_tree(
1998 path!("/test"),
1999 json!({
2000 "test": {
2001 "1.txt": "// One",
2002 "2.txt": "// Two",
2003 "3.txt": "// Three",
2004 }
2005 }),
2006 )
2007 .await;
2008
2009 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2010 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2011
2012 open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2013 open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2014 open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2015
2016 let picker = open_file_picker(&workspace, cx);
2017 picker.update(cx, |finder, _| {
2018 assert_eq!(finder.delegate.matches.len(), 3);
2019 assert_match_selection(finder, 0, "3.txt");
2020 assert_match_at_position(finder, 1, "2.txt");
2021 assert_match_at_position(finder, 2, "1.txt");
2022 });
2023
2024 cx.dispatch_action(SelectNext);
2025
2026 // Add more files to the worktree to trigger update matches
2027 for i in 0..5 {
2028 let filename = if cfg!(windows) {
2029 format!("C:/test/{}.txt", 4 + i)
2030 } else {
2031 format!("/test/{}.txt", 4 + i)
2032 };
2033 app_state
2034 .fs
2035 .create_file(Path::new(&filename), Default::default())
2036 .await
2037 .expect("unable to create file");
2038 }
2039
2040 cx.executor().advance_clock(FS_WATCH_LATENCY);
2041
2042 picker.update(cx, |finder, _| {
2043 assert_eq!(finder.delegate.matches.len(), 3);
2044 assert_match_at_position(finder, 0, "3.txt");
2045 assert_match_selection(finder, 1, "2.txt");
2046 assert_match_at_position(finder, 2, "1.txt");
2047 });
2048}
2049
2050#[gpui::test]
2051async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
2052 let app_state = init_test(cx);
2053
2054 app_state
2055 .fs
2056 .as_fake()
2057 .insert_tree(
2058 path!("/src"),
2059 json!({
2060 "collab_ui": {
2061 "first.rs": "// First Rust file",
2062 "second.rs": "// Second Rust file",
2063 "third.rs": "// Third Rust file",
2064 "collab_ui.rs": "// Fourth Rust file",
2065 }
2066 }),
2067 )
2068 .await;
2069
2070 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2071 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2072 // generate some history to select from
2073 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2074 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2075 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2076 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2077
2078 let finder = open_file_picker(&workspace, cx);
2079 let query = "collab_ui";
2080 cx.simulate_input(query);
2081 finder.update(cx, |picker, _| {
2082 let search_entries = collect_search_matches(picker).search_paths_only();
2083 assert_eq!(
2084 search_entries,
2085 vec![
2086 PathBuf::from("collab_ui/collab_ui.rs"),
2087 PathBuf::from("collab_ui/first.rs"),
2088 PathBuf::from("collab_ui/third.rs"),
2089 PathBuf::from("collab_ui/second.rs"),
2090 ],
2091 "Despite all search results having the same directory name, the most matching one should be on top"
2092 );
2093 });
2094}
2095
2096#[gpui::test]
2097async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
2098 let app_state = init_test(cx);
2099
2100 app_state
2101 .fs
2102 .as_fake()
2103 .insert_tree(
2104 path!("/src"),
2105 json!({
2106 "test": {
2107 "first.rs": "// First Rust file",
2108 "nonexistent.rs": "// Second Rust file",
2109 "third.rs": "// Third Rust file",
2110 }
2111 }),
2112 )
2113 .await;
2114
2115 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2116 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from
2117 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2118 open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
2119 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2120 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2121 app_state
2122 .fs
2123 .remove_file(
2124 Path::new(path!("/src/test/nonexistent.rs")),
2125 RemoveOptions::default(),
2126 )
2127 .await
2128 .unwrap();
2129 cx.run_until_parked();
2130
2131 let picker = open_file_picker(&workspace, cx);
2132 cx.simulate_input("rs");
2133
2134 picker.update(cx, |picker, _| {
2135 assert_eq!(
2136 collect_search_matches(picker).history,
2137 vec![
2138 PathBuf::from("test/first.rs"),
2139 PathBuf::from("test/third.rs"),
2140 ],
2141 "Should have all opened files in the history, except the ones that do not exist on disk"
2142 );
2143 });
2144}
2145
2146#[gpui::test]
2147async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
2148 let app_state = init_test(cx);
2149
2150 app_state
2151 .fs
2152 .as_fake()
2153 .insert_tree(
2154 "/src",
2155 json!({
2156 "lib.rs": "// Lib file",
2157 "main.rs": "// Bar file",
2158 "read.me": "// Readme file",
2159 }),
2160 )
2161 .await;
2162
2163 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2164 let (workspace, cx) =
2165 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2166
2167 // Initial state
2168 let picker = open_file_picker(&workspace, cx);
2169 cx.simulate_input("rs");
2170 picker.update(cx, |finder, _| {
2171 assert_eq!(finder.delegate.matches.len(), 3);
2172 assert_match_at_position(finder, 0, "lib.rs");
2173 assert_match_at_position(finder, 1, "main.rs");
2174 assert_match_at_position(finder, 2, "rs");
2175 });
2176
2177 // Delete main.rs
2178 app_state
2179 .fs
2180 .remove_file("/src/main.rs".as_ref(), Default::default())
2181 .await
2182 .expect("unable to remove file");
2183 cx.executor().advance_clock(FS_WATCH_LATENCY);
2184
2185 // main.rs is in not among search results anymore
2186 picker.update(cx, |finder, _| {
2187 assert_eq!(finder.delegate.matches.len(), 2);
2188 assert_match_at_position(finder, 0, "lib.rs");
2189 assert_match_at_position(finder, 1, "rs");
2190 });
2191
2192 // Create util.rs
2193 app_state
2194 .fs
2195 .create_file("/src/util.rs".as_ref(), Default::default())
2196 .await
2197 .expect("unable to create file");
2198 cx.executor().advance_clock(FS_WATCH_LATENCY);
2199
2200 // util.rs is among search results
2201 picker.update(cx, |finder, _| {
2202 assert_eq!(finder.delegate.matches.len(), 3);
2203 assert_match_at_position(finder, 0, "lib.rs");
2204 assert_match_at_position(finder, 1, "util.rs");
2205 assert_match_at_position(finder, 2, "rs");
2206 });
2207}
2208
2209#[gpui::test]
2210async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
2211 cx: &mut gpui::TestAppContext,
2212) {
2213 let app_state = init_test(cx);
2214
2215 app_state
2216 .fs
2217 .as_fake()
2218 .insert_tree(
2219 "/test",
2220 json!({
2221 "project_1": {
2222 "bar.rs": "// Bar file",
2223 "lib.rs": "// Lib file",
2224 },
2225 "project_2": {
2226 "Cargo.toml": "// Cargo file",
2227 "main.rs": "// Main file",
2228 }
2229 }),
2230 )
2231 .await;
2232
2233 let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
2234 let (workspace, cx) =
2235 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2236 let worktree_1_id = project.update(cx, |project, cx| {
2237 let worktree = project.worktrees(cx).last().expect("worktree not found");
2238 worktree.read(cx).id()
2239 });
2240
2241 // Initial state
2242 let picker = open_file_picker(&workspace, cx);
2243 cx.simulate_input("rs");
2244 picker.update(cx, |finder, _| {
2245 assert_eq!(finder.delegate.matches.len(), 3);
2246 assert_match_at_position(finder, 0, "bar.rs");
2247 assert_match_at_position(finder, 1, "lib.rs");
2248 assert_match_at_position(finder, 2, "rs");
2249 });
2250
2251 // Add new worktree
2252 project
2253 .update(cx, |project, cx| {
2254 project
2255 .find_or_create_worktree("/test/project_2", true, cx)
2256 .into_future()
2257 })
2258 .await
2259 .expect("unable to create workdir");
2260 cx.executor().advance_clock(FS_WATCH_LATENCY);
2261
2262 // main.rs is among search results
2263 picker.update(cx, |finder, _| {
2264 assert_eq!(finder.delegate.matches.len(), 4);
2265 assert_match_at_position(finder, 0, "bar.rs");
2266 assert_match_at_position(finder, 1, "lib.rs");
2267 assert_match_at_position(finder, 2, "main.rs");
2268 assert_match_at_position(finder, 3, "rs");
2269 });
2270
2271 // Remove the first worktree
2272 project.update(cx, |project, cx| {
2273 project.remove_worktree(worktree_1_id, cx);
2274 });
2275 cx.executor().advance_clock(FS_WATCH_LATENCY);
2276
2277 // Files from the first worktree are not in the search results anymore
2278 picker.update(cx, |finder, _| {
2279 assert_eq!(finder.delegate.matches.len(), 2);
2280 assert_match_at_position(finder, 0, "main.rs");
2281 assert_match_at_position(finder, 1, "rs");
2282 });
2283}
2284
2285#[gpui::test]
2286async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
2287 let app_state = init_test(cx);
2288
2289 app_state.fs.as_fake().insert_tree("/src", json!({})).await;
2290
2291 app_state
2292 .fs
2293 .create_dir("/src/even".as_ref())
2294 .await
2295 .expect("unable to create dir");
2296
2297 let initial_files_num = 5;
2298 for i in 0..initial_files_num {
2299 let filename = format!("/src/even/file_{}.txt", 10 + i);
2300 app_state
2301 .fs
2302 .create_file(Path::new(&filename), Default::default())
2303 .await
2304 .expect("unable to create file");
2305 }
2306
2307 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2308 let (workspace, cx) =
2309 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2310
2311 // Initial state
2312 let picker = open_file_picker(&workspace, cx);
2313 cx.simulate_input("file");
2314 let selected_index = 3;
2315 // Checking only the filename, not the whole path
2316 let selected_file = format!("file_{}.txt", 10 + selected_index);
2317 // Select even/file_13.txt
2318 for _ in 0..selected_index {
2319 cx.dispatch_action(SelectNext);
2320 }
2321
2322 picker.update(cx, |finder, _| {
2323 assert_match_selection(finder, selected_index, &selected_file)
2324 });
2325
2326 // Add more matches to the search results
2327 let files_to_add = 10;
2328 for i in 0..files_to_add {
2329 let filename = format!("/src/file_{}.txt", 20 + i);
2330 app_state
2331 .fs
2332 .create_file(Path::new(&filename), Default::default())
2333 .await
2334 .expect("unable to create file");
2335 }
2336 cx.executor().advance_clock(FS_WATCH_LATENCY);
2337
2338 // file_13.txt is still selected
2339 picker.update(cx, |finder, _| {
2340 let expected_selected_index = selected_index + files_to_add;
2341 assert_match_selection(finder, expected_selected_index, &selected_file);
2342 });
2343}
2344
2345#[gpui::test]
2346async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
2347 cx: &mut gpui::TestAppContext,
2348) {
2349 let app_state = init_test(cx);
2350
2351 app_state
2352 .fs
2353 .as_fake()
2354 .insert_tree(
2355 "/src",
2356 json!({
2357 "file_1.txt": "// file_1",
2358 "file_2.txt": "// file_2",
2359 "file_3.txt": "// file_3",
2360 }),
2361 )
2362 .await;
2363
2364 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2365 let (workspace, cx) =
2366 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2367
2368 // Initial state
2369 let picker = open_file_picker(&workspace, cx);
2370 cx.simulate_input("file");
2371 // Select even/file_2.txt
2372 cx.dispatch_action(SelectNext);
2373
2374 // Remove the selected entry
2375 app_state
2376 .fs
2377 .remove_file("/src/file_2.txt".as_ref(), Default::default())
2378 .await
2379 .expect("unable to remove file");
2380 cx.executor().advance_clock(FS_WATCH_LATENCY);
2381
2382 // file_1.txt is now selected
2383 picker.update(cx, |finder, _| {
2384 assert_match_selection(finder, 0, "file_1.txt");
2385 });
2386}
2387
2388#[gpui::test]
2389async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2390 let app_state = init_test(cx);
2391
2392 app_state
2393 .fs
2394 .as_fake()
2395 .insert_tree(
2396 path!("/test"),
2397 json!({
2398 "1.txt": "// One",
2399 }),
2400 )
2401 .await;
2402
2403 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2404 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2405
2406 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2407
2408 cx.simulate_modifiers_change(Modifiers::secondary_key());
2409 open_file_picker(&workspace, cx);
2410
2411 cx.simulate_modifiers_change(Modifiers::none());
2412 active_file_picker(&workspace, cx);
2413}
2414
2415#[gpui::test]
2416async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2417 let app_state = init_test(cx);
2418
2419 app_state
2420 .fs
2421 .as_fake()
2422 .insert_tree(
2423 path!("/test"),
2424 json!({
2425 "1.txt": "// One",
2426 "2.txt": "// Two",
2427 }),
2428 )
2429 .await;
2430
2431 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2432 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2433
2434 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2435 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2436
2437 cx.simulate_modifiers_change(Modifiers::secondary_key());
2438 let picker = open_file_picker(&workspace, cx);
2439 picker.update(cx, |finder, _| {
2440 assert_eq!(finder.delegate.matches.len(), 2);
2441 assert_match_selection(finder, 0, "2.txt");
2442 assert_match_at_position(finder, 1, "1.txt");
2443 });
2444
2445 cx.dispatch_action(SelectNext);
2446 cx.simulate_modifiers_change(Modifiers::none());
2447 cx.read(|cx| {
2448 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2449 assert_eq!(active_editor.read(cx).title(cx), "1.txt");
2450 });
2451}
2452
2453#[gpui::test]
2454async fn test_switches_between_release_norelease_modes_on_forward_nav(
2455 cx: &mut gpui::TestAppContext,
2456) {
2457 let app_state = init_test(cx);
2458
2459 app_state
2460 .fs
2461 .as_fake()
2462 .insert_tree(
2463 path!("/test"),
2464 json!({
2465 "1.txt": "// One",
2466 "2.txt": "// Two",
2467 }),
2468 )
2469 .await;
2470
2471 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2472 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2473
2474 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2475 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2476
2477 // Open with a shortcut
2478 cx.simulate_modifiers_change(Modifiers::secondary_key());
2479 let picker = open_file_picker(&workspace, cx);
2480 picker.update(cx, |finder, _| {
2481 assert_eq!(finder.delegate.matches.len(), 2);
2482 assert_match_selection(finder, 0, "2.txt");
2483 assert_match_at_position(finder, 1, "1.txt");
2484 });
2485
2486 // Switch to navigating with other shortcuts
2487 // Don't open file on modifiers release
2488 cx.simulate_modifiers_change(Modifiers::control());
2489 cx.dispatch_action(SelectNext);
2490 cx.simulate_modifiers_change(Modifiers::none());
2491 picker.update(cx, |finder, _| {
2492 assert_eq!(finder.delegate.matches.len(), 2);
2493 assert_match_at_position(finder, 0, "2.txt");
2494 assert_match_selection(finder, 1, "1.txt");
2495 });
2496
2497 // Back to navigation with initial shortcut
2498 // Open file on modifiers release
2499 cx.simulate_modifiers_change(Modifiers::secondary_key());
2500 cx.dispatch_action(ToggleFileFinder::default());
2501 cx.simulate_modifiers_change(Modifiers::none());
2502 cx.read(|cx| {
2503 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2504 assert_eq!(active_editor.read(cx).title(cx), "2.txt");
2505 });
2506}
2507
2508#[gpui::test]
2509async fn test_switches_between_release_norelease_modes_on_backward_nav(
2510 cx: &mut gpui::TestAppContext,
2511) {
2512 let app_state = init_test(cx);
2513
2514 app_state
2515 .fs
2516 .as_fake()
2517 .insert_tree(
2518 path!("/test"),
2519 json!({
2520 "1.txt": "// One",
2521 "2.txt": "// Two",
2522 "3.txt": "// Three"
2523 }),
2524 )
2525 .await;
2526
2527 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2528 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2529
2530 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2531 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2532 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2533
2534 // Open with a shortcut
2535 cx.simulate_modifiers_change(Modifiers::secondary_key());
2536 let picker = open_file_picker(&workspace, cx);
2537 picker.update(cx, |finder, _| {
2538 assert_eq!(finder.delegate.matches.len(), 3);
2539 assert_match_selection(finder, 0, "3.txt");
2540 assert_match_at_position(finder, 1, "2.txt");
2541 assert_match_at_position(finder, 2, "1.txt");
2542 });
2543
2544 // Switch to navigating with other shortcuts
2545 // Don't open file on modifiers release
2546 cx.simulate_modifiers_change(Modifiers::control());
2547 cx.dispatch_action(menu::SelectPrevious);
2548 cx.simulate_modifiers_change(Modifiers::none());
2549 picker.update(cx, |finder, _| {
2550 assert_eq!(finder.delegate.matches.len(), 3);
2551 assert_match_at_position(finder, 0, "3.txt");
2552 assert_match_at_position(finder, 1, "2.txt");
2553 assert_match_selection(finder, 2, "1.txt");
2554 });
2555
2556 // Back to navigation with initial shortcut
2557 // Open file on modifiers release
2558 cx.simulate_modifiers_change(Modifiers::secondary_key());
2559 cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
2560 cx.simulate_modifiers_change(Modifiers::none());
2561 cx.read(|cx| {
2562 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2563 assert_eq!(active_editor.read(cx).title(cx), "3.txt");
2564 });
2565}
2566
2567#[gpui::test]
2568async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
2569 let app_state = init_test(cx);
2570
2571 app_state
2572 .fs
2573 .as_fake()
2574 .insert_tree(
2575 path!("/test"),
2576 json!({
2577 "1.txt": "// One",
2578 }),
2579 )
2580 .await;
2581
2582 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2583 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2584
2585 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2586
2587 cx.simulate_modifiers_change(Modifiers::secondary_key());
2588 open_file_picker(&workspace, cx);
2589
2590 cx.simulate_modifiers_change(Modifiers::command_shift());
2591 active_file_picker(&workspace, cx);
2592}
2593
2594#[gpui::test]
2595async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
2596 let app_state = init_test(cx);
2597 app_state
2598 .fs
2599 .as_fake()
2600 .insert_tree(
2601 "/test",
2602 json!({
2603 "00.txt": "",
2604 "01.txt": "",
2605 "02.txt": "",
2606 "03.txt": "",
2607 "04.txt": "",
2608 "05.txt": "",
2609 }),
2610 )
2611 .await;
2612
2613 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
2614 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2615
2616 cx.dispatch_action(ToggleFileFinder::default());
2617 let picker = active_file_picker(&workspace, cx);
2618
2619 picker.update_in(cx, |picker, window, cx| {
2620 picker.update_matches(".txt".to_string(), window, cx)
2621 });
2622
2623 cx.run_until_parked();
2624
2625 picker.update(cx, |picker, _| {
2626 assert_eq!(picker.delegate.matches.len(), 7);
2627 assert_eq!(picker.delegate.selected_index, 0);
2628 });
2629
2630 // When toggling repeatedly, the picker scrolls to reveal the selected item.
2631 cx.dispatch_action(ToggleFileFinder::default());
2632 cx.dispatch_action(ToggleFileFinder::default());
2633 cx.dispatch_action(ToggleFileFinder::default());
2634
2635 cx.run_until_parked();
2636
2637 picker.update(cx, |picker, _| {
2638 assert_eq!(picker.delegate.matches.len(), 7);
2639 assert_eq!(picker.delegate.selected_index, 3);
2640 });
2641}
2642
2643async fn open_close_queried_buffer(
2644 input: &str,
2645 expected_matches: usize,
2646 expected_editor_title: &str,
2647 workspace: &Entity<Workspace>,
2648 cx: &mut gpui::VisualTestContext,
2649) -> Vec<FoundPath> {
2650 let history_items = open_queried_buffer(
2651 input,
2652 expected_matches,
2653 expected_editor_title,
2654 workspace,
2655 cx,
2656 )
2657 .await;
2658
2659 cx.dispatch_action(workspace::CloseActiveItem {
2660 save_intent: None,
2661 close_pinned: false,
2662 });
2663
2664 history_items
2665}
2666
2667async fn open_queried_buffer(
2668 input: &str,
2669 expected_matches: usize,
2670 expected_editor_title: &str,
2671 workspace: &Entity<Workspace>,
2672 cx: &mut gpui::VisualTestContext,
2673) -> Vec<FoundPath> {
2674 let picker = open_file_picker(workspace, cx);
2675 cx.simulate_input(input);
2676
2677 let history_items = picker.update(cx, |finder, _| {
2678 assert_eq!(
2679 finder.delegate.matches.len(),
2680 expected_matches + 1, // +1 from CreateNew option
2681 "Unexpected number of matches found for query `{input}`, matches: {:?}",
2682 finder.delegate.matches
2683 );
2684 finder.delegate.history_items.clone()
2685 });
2686
2687 cx.dispatch_action(Confirm);
2688
2689 cx.read(|cx| {
2690 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2691 let active_editor_title = active_editor.read(cx).title(cx);
2692 assert_eq!(
2693 expected_editor_title, active_editor_title,
2694 "Unexpected editor title for query `{input}`"
2695 );
2696 });
2697
2698 history_items
2699}
2700
2701fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2702 cx.update(|cx| {
2703 let state = AppState::test(cx);
2704 theme::init(theme::LoadThemes::JustBase, cx);
2705 language::init(cx);
2706 super::init(cx);
2707 editor::init(cx);
2708 workspace::init_settings(cx);
2709 Project::init_settings(cx);
2710 state
2711 })
2712}
2713
2714fn test_path_position(test_str: &str) -> FileSearchQuery {
2715 let path_position = PathWithPosition::parse_str(test_str);
2716
2717 FileSearchQuery {
2718 raw_query: test_str.to_owned(),
2719 file_query_end: if path_position.path.to_str().unwrap() == test_str {
2720 None
2721 } else {
2722 Some(path_position.path.to_str().unwrap().len())
2723 },
2724 path_position,
2725 }
2726}
2727
2728fn build_find_picker(
2729 project: Entity<Project>,
2730 cx: &mut TestAppContext,
2731) -> (
2732 Entity<Picker<FileFinderDelegate>>,
2733 Entity<Workspace>,
2734 &mut VisualTestContext,
2735) {
2736 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2737 let picker = open_file_picker(&workspace, cx);
2738 (picker, workspace, cx)
2739}
2740
2741#[track_caller]
2742fn open_file_picker(
2743 workspace: &Entity<Workspace>,
2744 cx: &mut VisualTestContext,
2745) -> Entity<Picker<FileFinderDelegate>> {
2746 cx.dispatch_action(ToggleFileFinder {
2747 separate_history: true,
2748 });
2749 active_file_picker(workspace, cx)
2750}
2751
2752#[track_caller]
2753fn active_file_picker(
2754 workspace: &Entity<Workspace>,
2755 cx: &mut VisualTestContext,
2756) -> Entity<Picker<FileFinderDelegate>> {
2757 workspace.update(cx, |workspace, cx| {
2758 workspace
2759 .active_modal::<FileFinder>(cx)
2760 .expect("file finder is not open")
2761 .read(cx)
2762 .picker
2763 .clone()
2764 })
2765}
2766
2767#[derive(Debug, Default)]
2768struct SearchEntries {
2769 history: Vec<PathBuf>,
2770 history_found_paths: Vec<FoundPath>,
2771 search: Vec<PathBuf>,
2772 search_matches: Vec<PathMatch>,
2773}
2774
2775impl SearchEntries {
2776 #[track_caller]
2777 fn search_paths_only(self) -> Vec<PathBuf> {
2778 assert!(
2779 self.history.is_empty(),
2780 "Should have no history matches, but got: {:?}",
2781 self.history
2782 );
2783 self.search
2784 }
2785
2786 #[track_caller]
2787 fn search_matches_only(self) -> Vec<PathMatch> {
2788 assert!(
2789 self.history.is_empty(),
2790 "Should have no history matches, but got: {:?}",
2791 self.history
2792 );
2793 self.search_matches
2794 }
2795}
2796
2797fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
2798 let mut search_entries = SearchEntries::default();
2799 for m in &picker.delegate.matches.matches {
2800 match &m {
2801 Match::History {
2802 path: history_path,
2803 panel_match: path_match,
2804 } => {
2805 search_entries.history.push(
2806 path_match
2807 .as_ref()
2808 .map(|path_match| {
2809 Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
2810 })
2811 .unwrap_or_else(|| {
2812 history_path
2813 .absolute
2814 .as_deref()
2815 .unwrap_or_else(|| &history_path.project.path)
2816 .to_path_buf()
2817 }),
2818 );
2819 search_entries
2820 .history_found_paths
2821 .push(history_path.clone());
2822 }
2823 Match::Search(path_match) => {
2824 search_entries
2825 .search
2826 .push(Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path));
2827 search_entries.search_matches.push(path_match.0.clone());
2828 }
2829 Match::CreateNew(_) => {}
2830 }
2831 }
2832 search_entries
2833}
2834
2835#[track_caller]
2836fn assert_match_selection(
2837 finder: &Picker<FileFinderDelegate>,
2838 expected_selection_index: usize,
2839 expected_file_name: &str,
2840) {
2841 assert_eq!(
2842 finder.delegate.selected_index(),
2843 expected_selection_index,
2844 "Match is not selected"
2845 );
2846 assert_match_at_position(finder, expected_selection_index, expected_file_name);
2847}
2848
2849#[track_caller]
2850fn assert_match_at_position(
2851 finder: &Picker<FileFinderDelegate>,
2852 match_index: usize,
2853 expected_file_name: &str,
2854) {
2855 let match_item = finder
2856 .delegate
2857 .matches
2858 .get(match_index)
2859 .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
2860 let match_file_name = match &match_item {
2861 Match::History { path, .. } => path.absolute.as_deref().unwrap().file_name(),
2862 Match::Search(path_match) => path_match.0.path.file_name(),
2863 Match::CreateNew(project_path) => project_path.path.file_name(),
2864 }
2865 .unwrap()
2866 .to_string_lossy();
2867 assert_eq!(match_file_name, expected_file_name);
2868}
2869
2870#[gpui::test]
2871async fn test_filename_precedence(cx: &mut TestAppContext) {
2872 let app_state = init_test(cx);
2873
2874 app_state
2875 .fs
2876 .as_fake()
2877 .insert_tree(
2878 path!("/src"),
2879 json!({
2880 "layout": {
2881 "app.css": "",
2882 "app.d.ts": "",
2883 "app.html": "",
2884 "+page.svelte": "",
2885 },
2886 "routes": {
2887 "+layout.svelte": "",
2888 }
2889 }),
2890 )
2891 .await;
2892
2893 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2894 let (picker, _, cx) = build_find_picker(project, cx);
2895
2896 cx.simulate_input("layout");
2897
2898 picker.update(cx, |finder, _| {
2899 let search_matches = collect_search_matches(finder).search_paths_only();
2900
2901 assert_eq!(
2902 search_matches,
2903 vec![
2904 PathBuf::from("routes/+layout.svelte"),
2905 PathBuf::from("layout/app.css"),
2906 PathBuf::from("layout/app.d.ts"),
2907 PathBuf::from("layout/app.html"),
2908 PathBuf::from("layout/+page.svelte"),
2909 ],
2910 "File with 'layout' in filename should be prioritized over files in 'layout' directory"
2911 );
2912 });
2913}