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, assert_matches};
8use project::{FS_WATCH_LATENCY, RemoveOptions};
9use serde_json::json;
10use util::{path, rel_path::rel_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: rel_path("b0.5").into(),
81 path_prefix: rel_path("").into(),
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: rel_path("c1.0").into(),
90 path_prefix: rel_path("").into(),
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: rel_path("a1.0").into(),
99 path_prefix: rel_path("").into(),
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: rel_path("a0.5").into(),
108 path_prefix: rel_path("").into(),
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: rel_path("b1.0").into(),
117 path_prefix: rel_path("").into(),
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: rel_path("a1.0").into(),
132 path_prefix: rel_path("").into(),
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: rel_path("b1.0").into(),
141 path_prefix: rel_path("").into(),
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: rel_path("c1.0").into(),
150 path_prefix: rel_path("").into(),
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: rel_path("a0.5").into(),
159 path_prefix: rel_path("").into(),
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: rel_path("b0.5").into(),
168 path_prefix: rel_path("").into(),
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![rel_path("a/b/file2.txt").into()],
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::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![rel_path("其他/S数据表格/task.xlsx").into()],
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 rel_path("ignored-root/hi").into(),
717 rel_path("tracked-root/hi").into(),
718 rel_path("ignored-root/hiccup").into(),
719 rel_path("tracked-root/hiccup").into(),
720 rel_path("ignored-root/height").into(),
721 rel_path("ignored-root/happiness").into(),
722 rel_path("tracked-root/happiness").into(),
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 rel_path("ignored-root/hi").into(),
742 rel_path("tracked-root/hi").into(),
743 rel_path("ignored-root/hiccup").into(),
744 rel_path("tracked-root/hiccup").into(),
745 rel_path("ignored-root/height").into(),
746 rel_path("tracked-root/height").into(),
747 rel_path("ignored-root/happiness").into(),
748 rel_path("tracked-root/happiness").into(),
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 rel_path("tracked-root/hi").into(),
769 rel_path("tracked-root/hiccup").into(),
770 rel_path("tracked-root/happiness").into(),
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 rel_path("ignored-root/hi").into(),
816 rel_path("tracked-root/hi").into(),
817 rel_path("ignored-root/hiccup").into(),
818 rel_path("tracked-root/hiccup").into(),
819 rel_path("ignored-root/height").into(),
820 rel_path("ignored-root/happiness").into(),
821 rel_path("tracked-root/happiness").into(),
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 rel_path("ignored-root/hi").into(),
842 rel_path("tracked-root/hi").into(),
843 rel_path("ignored-root/hiccup").into(),
844 rel_path("tracked-root/hiccup").into(),
845 rel_path("ignored-root/height").into(),
846 rel_path("tracked-root/height").into(),
847 rel_path("tracked-root/heights/height_1").into(),
848 rel_path("tracked-root/heights/height_2").into(),
849 rel_path("ignored-root/happiness").into(),
850 rel_path("tracked-root/happiness").into(),
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 rel_path("tracked-root/hi").into(),
871 rel_path("tracked-root/hiccup").into(),
872 rel_path("tracked-root/happiness").into(),
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], PathStyle::local());
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: rel_path("the-parent-dirb/fileb").into(),
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: rel_path("the-parent-dirb/filec").into()
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 (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1042 });
1043
1044 let finder = open_file_picker(&workspace, cx);
1045
1046 finder
1047 .update_in(cx, |f, window, cx| {
1048 f.delegate
1049 .spawn_search(test_path_position(path!("rootb/filec")), window, cx)
1050 })
1051 .await;
1052 cx.run_until_parked();
1053 finder.update_in(cx, |picker, window, cx| {
1054 assert_eq!(picker.delegate.matches.len(), 1);
1055 picker.delegate.confirm(false, window, cx)
1056 });
1057 cx.run_until_parked();
1058 cx.read(|cx| {
1059 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1060 let project_path = active_editor.read(cx).project_path(cx);
1061 assert_eq!(
1062 project_path,
1063 Some(ProjectPath {
1064 worktree_id: worktree_id2,
1065 path: rel_path("filec").into()
1066 })
1067 );
1068 });
1069}
1070
1071#[gpui::test]
1072async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1073 let app_state = init_test(cx);
1074 app_state
1075 .fs
1076 .as_fake()
1077 .insert_tree(
1078 path!("/root"),
1079 json!({
1080 "dir1": { "a.txt": "" },
1081 "dir2": {
1082 "a.txt": "",
1083 "b.txt": ""
1084 }
1085 }),
1086 )
1087 .await;
1088
1089 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1090 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1091
1092 let worktree_id = cx.read(|cx| {
1093 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1094 assert_eq!(worktrees.len(), 1);
1095 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1096 });
1097
1098 // When workspace has an active item, sort items which are closer to that item
1099 // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1100 // so that one should be sorted earlier
1101 let b_path = ProjectPath {
1102 worktree_id,
1103 path: rel_path("dir2/b.txt").into(),
1104 };
1105 workspace
1106 .update_in(cx, |workspace, window, cx| {
1107 workspace.open_path(b_path, None, true, window, cx)
1108 })
1109 .await
1110 .unwrap();
1111 let finder = open_file_picker(&workspace, cx);
1112 finder
1113 .update_in(cx, |f, window, cx| {
1114 f.delegate
1115 .spawn_search(test_path_position("a.txt"), window, cx)
1116 })
1117 .await;
1118
1119 finder.update(cx, |picker, _| {
1120 let matches = collect_search_matches(picker).search_paths_only();
1121 assert_eq!(matches[0].as_ref(), rel_path("dir2/a.txt"));
1122 assert_eq!(matches[1].as_ref(), rel_path("dir1/a.txt"));
1123 });
1124}
1125
1126#[gpui::test]
1127async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1128 let app_state = init_test(cx);
1129 app_state
1130 .fs
1131 .as_fake()
1132 .insert_tree(
1133 "/root",
1134 json!({
1135 "dir1": {},
1136 "dir2": {
1137 "dir3": {}
1138 }
1139 }),
1140 )
1141 .await;
1142
1143 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1144 let (picker, _workspace, cx) = build_find_picker(project, cx);
1145
1146 picker
1147 .update_in(cx, |f, window, cx| {
1148 f.delegate
1149 .spawn_search(test_path_position("dir"), window, cx)
1150 })
1151 .await;
1152 cx.read(|cx| {
1153 let finder = picker.read(cx);
1154 assert_eq!(finder.delegate.matches.len(), 1);
1155 assert_match_at_position(finder, 0, "dir");
1156 });
1157}
1158
1159#[gpui::test]
1160async fn test_query_history(cx: &mut gpui::TestAppContext) {
1161 let app_state = init_test(cx);
1162
1163 app_state
1164 .fs
1165 .as_fake()
1166 .insert_tree(
1167 path!("/src"),
1168 json!({
1169 "test": {
1170 "first.rs": "// First Rust file",
1171 "second.rs": "// Second Rust file",
1172 "third.rs": "// Third Rust file",
1173 }
1174 }),
1175 )
1176 .await;
1177
1178 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1179 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1180 let worktree_id = cx.read(|cx| {
1181 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1182 assert_eq!(worktrees.len(), 1);
1183 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1184 });
1185
1186 // Open and close panels, getting their history items afterwards.
1187 // Ensure history items get populated with opened items, and items are kept in a certain order.
1188 // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1189 //
1190 // TODO: without closing, the opened items do not propagate their history changes for some reason
1191 // it does work in real app though, only tests do not propagate.
1192 workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1193
1194 let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1195 assert!(
1196 initial_history.is_empty(),
1197 "Should have no history before opening any files"
1198 );
1199
1200 let history_after_first =
1201 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1202 assert_eq!(
1203 history_after_first,
1204 vec![FoundPath::new(
1205 ProjectPath {
1206 worktree_id,
1207 path: rel_path("test/first.rs").into(),
1208 },
1209 PathBuf::from(path!("/src/test/first.rs"))
1210 )],
1211 "Should show 1st opened item in the history when opening the 2nd item"
1212 );
1213
1214 let history_after_second =
1215 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1216 assert_eq!(
1217 history_after_second,
1218 vec![
1219 FoundPath::new(
1220 ProjectPath {
1221 worktree_id,
1222 path: rel_path("test/second.rs").into(),
1223 },
1224 PathBuf::from(path!("/src/test/second.rs"))
1225 ),
1226 FoundPath::new(
1227 ProjectPath {
1228 worktree_id,
1229 path: rel_path("test/first.rs").into(),
1230 },
1231 PathBuf::from(path!("/src/test/first.rs"))
1232 ),
1233 ],
1234 "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1235 2nd item should be the first in the history, as the last opened."
1236 );
1237
1238 let history_after_third =
1239 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1240 assert_eq!(
1241 history_after_third,
1242 vec![
1243 FoundPath::new(
1244 ProjectPath {
1245 worktree_id,
1246 path: rel_path("test/third.rs").into(),
1247 },
1248 PathBuf::from(path!("/src/test/third.rs"))
1249 ),
1250 FoundPath::new(
1251 ProjectPath {
1252 worktree_id,
1253 path: rel_path("test/second.rs").into(),
1254 },
1255 PathBuf::from(path!("/src/test/second.rs"))
1256 ),
1257 FoundPath::new(
1258 ProjectPath {
1259 worktree_id,
1260 path: rel_path("test/first.rs").into(),
1261 },
1262 PathBuf::from(path!("/src/test/first.rs"))
1263 ),
1264 ],
1265 "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1266 3rd item should be the first in the history, as the last opened."
1267 );
1268
1269 let history_after_second_again =
1270 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1271 assert_eq!(
1272 history_after_second_again,
1273 vec![
1274 FoundPath::new(
1275 ProjectPath {
1276 worktree_id,
1277 path: rel_path("test/second.rs").into(),
1278 },
1279 PathBuf::from(path!("/src/test/second.rs"))
1280 ),
1281 FoundPath::new(
1282 ProjectPath {
1283 worktree_id,
1284 path: rel_path("test/third.rs").into(),
1285 },
1286 PathBuf::from(path!("/src/test/third.rs"))
1287 ),
1288 FoundPath::new(
1289 ProjectPath {
1290 worktree_id,
1291 path: rel_path("test/first.rs").into(),
1292 },
1293 PathBuf::from(path!("/src/test/first.rs"))
1294 ),
1295 ],
1296 "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1297 2nd item, as the last opened, 3rd item should go next as it was opened right before."
1298 );
1299}
1300
1301#[gpui::test]
1302async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
1303 let app_state = init_test(cx);
1304
1305 app_state
1306 .fs
1307 .as_fake()
1308 .insert_tree(
1309 path!("/src"),
1310 json!({
1311 "test": {
1312 "first.rs": "// First Rust file",
1313 "second.rs": "// Second Rust file",
1314 "third.rs": "// Third Rust file",
1315 }
1316 }),
1317 )
1318 .await;
1319
1320 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1321 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1322
1323 workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1324
1325 open_close_queried_buffer("efir", 1, "first.rs", &workspace, cx).await;
1326 let history = open_close_queried_buffer("second", 1, "second.rs", &workspace, cx).await;
1327 assert_eq!(history.len(), 1);
1328
1329 let picker = open_file_picker(&workspace, cx);
1330 cx.simulate_input("fir");
1331 picker.update_in(cx, |finder, window, cx| {
1332 let matches = &finder.delegate.matches.matches;
1333 assert_matches!(
1334 matches.as_slice(),
1335 [Match::History { .. }, Match::CreateNew { .. }]
1336 );
1337 assert_eq!(
1338 matches[0].panel_match().unwrap().0.path.as_ref(),
1339 rel_path("test/first.rs")
1340 );
1341 assert_eq!(matches[0].panel_match().unwrap().0.positions, &[5, 6, 7]);
1342
1343 let (file_label, path_label) =
1344 finder
1345 .delegate
1346 .labels_for_match(&finder.delegate.matches.matches[0], window, cx);
1347 assert_eq!(file_label.text(), "first.rs");
1348 assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
1349 assert_eq!(
1350 path_label.text(),
1351 format!("test{}", PathStyle::local().separator())
1352 );
1353 assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
1354 });
1355}
1356
1357#[gpui::test]
1358async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1359 let app_state = init_test(cx);
1360
1361 app_state
1362 .fs
1363 .as_fake()
1364 .insert_tree(
1365 path!("/src"),
1366 json!({
1367 "test": {
1368 "first.rs": "// First Rust file",
1369 "second.rs": "// Second Rust file",
1370 }
1371 }),
1372 )
1373 .await;
1374
1375 app_state
1376 .fs
1377 .as_fake()
1378 .insert_tree(
1379 path!("/external-src"),
1380 json!({
1381 "test": {
1382 "third.rs": "// Third Rust file",
1383 "fourth.rs": "// Fourth Rust file",
1384 }
1385 }),
1386 )
1387 .await;
1388
1389 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1390 cx.update(|cx| {
1391 project.update(cx, |project, cx| {
1392 project.find_or_create_worktree(path!("/external-src"), false, cx)
1393 })
1394 })
1395 .detach();
1396 cx.background_executor.run_until_parked();
1397
1398 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1399 let worktree_id = cx.read(|cx| {
1400 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1401 assert_eq!(worktrees.len(), 1,);
1402
1403 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1404 });
1405 workspace
1406 .update_in(cx, |workspace, window, cx| {
1407 workspace.open_abs_path(
1408 PathBuf::from(path!("/external-src/test/third.rs")),
1409 OpenOptions {
1410 visible: Some(OpenVisible::None),
1411 ..Default::default()
1412 },
1413 window,
1414 cx,
1415 )
1416 })
1417 .detach();
1418 cx.background_executor.run_until_parked();
1419 let external_worktree_id = cx.read(|cx| {
1420 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1421 assert_eq!(
1422 worktrees.len(),
1423 2,
1424 "External file should get opened in a new worktree"
1425 );
1426
1427 WorktreeId::from_usize(
1428 worktrees
1429 .into_iter()
1430 .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
1431 .expect("New worktree should have a different id")
1432 .entity_id()
1433 .as_u64() as usize,
1434 )
1435 });
1436 cx.dispatch_action(workspace::CloseActiveItem {
1437 save_intent: None,
1438 close_pinned: false,
1439 });
1440
1441 let initial_history_items =
1442 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1443 assert_eq!(
1444 initial_history_items,
1445 vec![FoundPath::new(
1446 ProjectPath {
1447 worktree_id: external_worktree_id,
1448 path: rel_path("").into(),
1449 },
1450 PathBuf::from(path!("/external-src/test/third.rs"))
1451 )],
1452 "Should show external file with its full path in the history after it was open"
1453 );
1454
1455 let updated_history_items =
1456 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1457 assert_eq!(
1458 updated_history_items,
1459 vec![
1460 FoundPath::new(
1461 ProjectPath {
1462 worktree_id,
1463 path: rel_path("test/second.rs").into(),
1464 },
1465 PathBuf::from(path!("/src/test/second.rs"))
1466 ),
1467 FoundPath::new(
1468 ProjectPath {
1469 worktree_id: external_worktree_id,
1470 path: rel_path("").into(),
1471 },
1472 PathBuf::from(path!("/external-src/test/third.rs"))
1473 ),
1474 ],
1475 "Should keep external file with history updates",
1476 );
1477}
1478
1479#[gpui::test]
1480async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1481 let app_state = init_test(cx);
1482
1483 app_state
1484 .fs
1485 .as_fake()
1486 .insert_tree(
1487 path!("/src"),
1488 json!({
1489 "test": {
1490 "first.rs": "// First Rust file",
1491 "second.rs": "// Second Rust file",
1492 "third.rs": "// Third Rust file",
1493 }
1494 }),
1495 )
1496 .await;
1497
1498 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1499 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1500
1501 // generate some history to select from
1502 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1503 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1504 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1505 let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1506
1507 for expected_selected_index in 0..current_history.len() {
1508 cx.dispatch_action(ToggleFileFinder::default());
1509 let picker = active_file_picker(&workspace, cx);
1510 let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
1511 assert_eq!(
1512 selected_index, expected_selected_index,
1513 "Should select the next item in the history"
1514 );
1515 }
1516
1517 cx.dispatch_action(ToggleFileFinder::default());
1518 let selected_index = workspace.update(cx, |workspace, cx| {
1519 workspace
1520 .active_modal::<FileFinder>(cx)
1521 .unwrap()
1522 .read(cx)
1523 .picker
1524 .read(cx)
1525 .delegate
1526 .selected_index()
1527 });
1528 assert_eq!(
1529 selected_index, 0,
1530 "Should wrap around the history and start all over"
1531 );
1532}
1533
1534#[gpui::test]
1535async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1536 let app_state = init_test(cx);
1537
1538 app_state
1539 .fs
1540 .as_fake()
1541 .insert_tree(
1542 path!("/src"),
1543 json!({
1544 "test": {
1545 "first.rs": "// First Rust file",
1546 "second.rs": "// Second Rust file",
1547 "third.rs": "// Third Rust file",
1548 "fourth.rs": "// Fourth Rust file",
1549 }
1550 }),
1551 )
1552 .await;
1553
1554 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1555 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1556 let worktree_id = cx.read(|cx| {
1557 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1558 assert_eq!(worktrees.len(), 1,);
1559
1560 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1561 });
1562
1563 // generate some history to select from
1564 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1565 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1566 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1567 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1568
1569 let finder = open_file_picker(&workspace, cx);
1570 let first_query = "f";
1571 finder
1572 .update_in(cx, |finder, window, cx| {
1573 finder
1574 .delegate
1575 .update_matches(first_query.to_string(), window, cx)
1576 })
1577 .await;
1578 finder.update(cx, |picker, _| {
1579 let matches = collect_search_matches(picker);
1580 assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1581 let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1582 assert_eq!(history_match, &FoundPath::new(
1583 ProjectPath {
1584 worktree_id,
1585 path: rel_path("test/first.rs").into(),
1586 },
1587 PathBuf::from(path!("/src/test/first.rs")),
1588 ));
1589 assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1590 assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1591 });
1592
1593 let second_query = "fsdasdsa";
1594 let finder = active_file_picker(&workspace, cx);
1595 finder
1596 .update_in(cx, |finder, window, cx| {
1597 finder
1598 .delegate
1599 .update_matches(second_query.to_string(), window, cx)
1600 })
1601 .await;
1602 finder.update(cx, |picker, _| {
1603 assert!(
1604 collect_search_matches(picker)
1605 .search_paths_only()
1606 .is_empty(),
1607 "No search entries should match {second_query}"
1608 );
1609 });
1610
1611 let first_query_again = first_query;
1612
1613 let finder = active_file_picker(&workspace, cx);
1614 finder
1615 .update_in(cx, |finder, window, cx| {
1616 finder
1617 .delegate
1618 .update_matches(first_query_again.to_string(), window, cx)
1619 })
1620 .await;
1621 finder.update(cx, |picker, _| {
1622 let matches = collect_search_matches(picker);
1623 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");
1624 let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1625 assert_eq!(history_match, &FoundPath::new(
1626 ProjectPath {
1627 worktree_id,
1628 path: rel_path("test/first.rs").into(),
1629 },
1630 PathBuf::from(path!("/src/test/first.rs"))
1631 ));
1632 assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1633 assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1634 });
1635}
1636
1637#[gpui::test]
1638async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1639 let app_state = init_test(cx);
1640
1641 app_state
1642 .fs
1643 .as_fake()
1644 .insert_tree(
1645 path!("/root"),
1646 json!({
1647 "test": {
1648 "1_qw": "// First file that matches the query",
1649 "2_second": "// Second file",
1650 "3_third": "// Third file",
1651 "4_fourth": "// Fourth file",
1652 "5_qwqwqw": "// A file with 3 more matches than the first one",
1653 "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1654 "7_qwqwqw": "// One more, same amount of query matches as above",
1655 }
1656 }),
1657 )
1658 .await;
1659
1660 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1661 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1662 // generate some history to select from
1663 open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1664 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1665 open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1666 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1667 open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1668
1669 let finder = open_file_picker(&workspace, cx);
1670 let query = "qw";
1671 finder
1672 .update_in(cx, |finder, window, cx| {
1673 finder
1674 .delegate
1675 .update_matches(query.to_string(), window, cx)
1676 })
1677 .await;
1678 finder.update(cx, |finder, _| {
1679 let search_matches = collect_search_matches(finder);
1680 assert_eq!(
1681 search_matches.history,
1682 vec![
1683 rel_path("test/1_qw").into(),
1684 rel_path("test/6_qwqwqw").into()
1685 ],
1686 );
1687 assert_eq!(
1688 search_matches.search,
1689 vec![
1690 rel_path("test/5_qwqwqw").into(),
1691 rel_path("test/7_qwqwqw").into()
1692 ],
1693 );
1694 });
1695}
1696
1697#[gpui::test]
1698async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1699 let app_state = init_test(cx);
1700
1701 app_state
1702 .fs
1703 .as_fake()
1704 .insert_tree(
1705 path!("/root"),
1706 json!({
1707 "test": {
1708 "1_qw": "",
1709 }
1710 }),
1711 )
1712 .await;
1713
1714 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1715 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1716 // Open new buffer
1717 open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1718
1719 let picker = open_file_picker(&workspace, cx);
1720 picker.update(cx, |finder, _| {
1721 assert_match_selection(finder, 0, "1_qw");
1722 });
1723}
1724
1725#[gpui::test]
1726async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1727 cx: &mut TestAppContext,
1728) {
1729 let app_state = init_test(cx);
1730
1731 app_state
1732 .fs
1733 .as_fake()
1734 .insert_tree(
1735 path!("/src"),
1736 json!({
1737 "test": {
1738 "bar.rs": "// Bar file",
1739 "lib.rs": "// Lib file",
1740 "maaa.rs": "// Maaaaaaa",
1741 "main.rs": "// Main file",
1742 "moo.rs": "// Moooooo",
1743 }
1744 }),
1745 )
1746 .await;
1747
1748 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1749 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1750
1751 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1752 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1753 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1754
1755 // main.rs is on top, previously used is selected
1756 let picker = open_file_picker(&workspace, cx);
1757 picker.update(cx, |finder, _| {
1758 assert_eq!(finder.delegate.matches.len(), 3);
1759 assert_match_selection(finder, 0, "main.rs");
1760 assert_match_at_position(finder, 1, "lib.rs");
1761 assert_match_at_position(finder, 2, "bar.rs");
1762 });
1763
1764 // all files match, main.rs is still on top, but the second item is selected
1765 picker
1766 .update_in(cx, |finder, window, cx| {
1767 finder
1768 .delegate
1769 .update_matches(".rs".to_string(), window, cx)
1770 })
1771 .await;
1772 picker.update(cx, |finder, _| {
1773 assert_eq!(finder.delegate.matches.len(), 6);
1774 assert_match_at_position(finder, 0, "main.rs");
1775 assert_match_selection(finder, 1, "bar.rs");
1776 assert_match_at_position(finder, 2, "lib.rs");
1777 assert_match_at_position(finder, 3, "moo.rs");
1778 assert_match_at_position(finder, 4, "maaa.rs");
1779 assert_match_at_position(finder, 5, ".rs");
1780 });
1781
1782 // main.rs is not among matches, select top item
1783 picker
1784 .update_in(cx, |finder, window, cx| {
1785 finder.delegate.update_matches("b".to_string(), window, cx)
1786 })
1787 .await;
1788 picker.update(cx, |finder, _| {
1789 assert_eq!(finder.delegate.matches.len(), 3);
1790 assert_match_at_position(finder, 0, "bar.rs");
1791 assert_match_at_position(finder, 1, "lib.rs");
1792 assert_match_at_position(finder, 2, "b");
1793 });
1794
1795 // main.rs is back, put it on top and select next item
1796 picker
1797 .update_in(cx, |finder, window, cx| {
1798 finder.delegate.update_matches("m".to_string(), window, cx)
1799 })
1800 .await;
1801 picker.update(cx, |finder, _| {
1802 assert_eq!(finder.delegate.matches.len(), 4);
1803 assert_match_at_position(finder, 0, "main.rs");
1804 assert_match_selection(finder, 1, "moo.rs");
1805 assert_match_at_position(finder, 2, "maaa.rs");
1806 assert_match_at_position(finder, 3, "m");
1807 });
1808
1809 // get back to the initial state
1810 picker
1811 .update_in(cx, |finder, window, cx| {
1812 finder.delegate.update_matches("".to_string(), window, cx)
1813 })
1814 .await;
1815 picker.update(cx, |finder, _| {
1816 assert_eq!(finder.delegate.matches.len(), 3);
1817 assert_match_selection(finder, 0, "main.rs");
1818 assert_match_at_position(finder, 1, "lib.rs");
1819 assert_match_at_position(finder, 2, "bar.rs");
1820 });
1821}
1822
1823#[gpui::test]
1824async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
1825 let app_state = init_test(cx);
1826
1827 cx.update(|cx| {
1828 let settings = *FileFinderSettings::get_global(cx);
1829
1830 FileFinderSettings::override_global(
1831 FileFinderSettings {
1832 skip_focus_for_active_in_search: false,
1833 ..settings
1834 },
1835 cx,
1836 );
1837 });
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 // main.rs is on top, previously used is selected
1864 let picker = open_file_picker(&workspace, cx);
1865 picker.update(cx, |finder, _| {
1866 assert_eq!(finder.delegate.matches.len(), 3);
1867 assert_match_selection(finder, 0, "main.rs");
1868 assert_match_at_position(finder, 1, "lib.rs");
1869 assert_match_at_position(finder, 2, "bar.rs");
1870 });
1871
1872 // all files match, main.rs is on top, and is selected
1873 picker
1874 .update_in(cx, |finder, window, cx| {
1875 finder
1876 .delegate
1877 .update_matches(".rs".to_string(), window, cx)
1878 })
1879 .await;
1880 picker.update(cx, |finder, _| {
1881 assert_eq!(finder.delegate.matches.len(), 6);
1882 assert_match_selection(finder, 0, "main.rs");
1883 assert_match_at_position(finder, 1, "bar.rs");
1884 assert_match_at_position(finder, 2, "lib.rs");
1885 assert_match_at_position(finder, 3, "moo.rs");
1886 assert_match_at_position(finder, 4, "maaa.rs");
1887 assert_match_at_position(finder, 5, ".rs");
1888 });
1889}
1890
1891#[gpui::test]
1892async fn test_non_separate_history_items(cx: &mut TestAppContext) {
1893 let app_state = init_test(cx);
1894
1895 app_state
1896 .fs
1897 .as_fake()
1898 .insert_tree(
1899 path!("/src"),
1900 json!({
1901 "test": {
1902 "bar.rs": "// Bar file",
1903 "lib.rs": "// Lib file",
1904 "maaa.rs": "// Maaaaaaa",
1905 "main.rs": "// Main file",
1906 "moo.rs": "// Moooooo",
1907 }
1908 }),
1909 )
1910 .await;
1911
1912 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1913 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1914
1915 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1916 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1917 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1918
1919 cx.dispatch_action(ToggleFileFinder::default());
1920 let picker = active_file_picker(&workspace, cx);
1921 // main.rs is on top, previously used is selected
1922 picker.update(cx, |finder, _| {
1923 assert_eq!(finder.delegate.matches.len(), 3);
1924 assert_match_selection(finder, 0, "main.rs");
1925 assert_match_at_position(finder, 1, "lib.rs");
1926 assert_match_at_position(finder, 2, "bar.rs");
1927 });
1928
1929 // all files match, main.rs is still on top, but the second item is selected
1930 picker
1931 .update_in(cx, |finder, window, cx| {
1932 finder
1933 .delegate
1934 .update_matches(".rs".to_string(), window, cx)
1935 })
1936 .await;
1937 picker.update(cx, |finder, _| {
1938 assert_eq!(finder.delegate.matches.len(), 6);
1939 assert_match_at_position(finder, 0, "main.rs");
1940 assert_match_selection(finder, 1, "moo.rs");
1941 assert_match_at_position(finder, 2, "bar.rs");
1942 assert_match_at_position(finder, 3, "lib.rs");
1943 assert_match_at_position(finder, 4, "maaa.rs");
1944 assert_match_at_position(finder, 5, ".rs");
1945 });
1946
1947 // main.rs is not among matches, select top item
1948 picker
1949 .update_in(cx, |finder, window, cx| {
1950 finder.delegate.update_matches("b".to_string(), window, cx)
1951 })
1952 .await;
1953 picker.update(cx, |finder, _| {
1954 assert_eq!(finder.delegate.matches.len(), 3);
1955 assert_match_at_position(finder, 0, "bar.rs");
1956 assert_match_at_position(finder, 1, "lib.rs");
1957 assert_match_at_position(finder, 2, "b");
1958 });
1959
1960 // main.rs is back, put it on top and select next item
1961 picker
1962 .update_in(cx, |finder, window, cx| {
1963 finder.delegate.update_matches("m".to_string(), window, cx)
1964 })
1965 .await;
1966 picker.update(cx, |finder, _| {
1967 assert_eq!(finder.delegate.matches.len(), 4);
1968 assert_match_at_position(finder, 0, "main.rs");
1969 assert_match_selection(finder, 1, "moo.rs");
1970 assert_match_at_position(finder, 2, "maaa.rs");
1971 assert_match_at_position(finder, 3, "m");
1972 });
1973
1974 // get back to the initial state
1975 picker
1976 .update_in(cx, |finder, window, cx| {
1977 finder.delegate.update_matches("".to_string(), window, cx)
1978 })
1979 .await;
1980 picker.update(cx, |finder, _| {
1981 assert_eq!(finder.delegate.matches.len(), 3);
1982 assert_match_selection(finder, 0, "main.rs");
1983 assert_match_at_position(finder, 1, "lib.rs");
1984 assert_match_at_position(finder, 2, "bar.rs");
1985 });
1986}
1987
1988#[gpui::test]
1989async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1990 let app_state = init_test(cx);
1991
1992 app_state
1993 .fs
1994 .as_fake()
1995 .insert_tree(
1996 path!("/test"),
1997 json!({
1998 "test": {
1999 "1.txt": "// One",
2000 "2.txt": "// Two",
2001 "3.txt": "// Three",
2002 }
2003 }),
2004 )
2005 .await;
2006
2007 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2008 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2009
2010 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2011 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2012 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2013
2014 let picker = open_file_picker(&workspace, cx);
2015 picker.update(cx, |finder, _| {
2016 assert_eq!(finder.delegate.matches.len(), 3);
2017 assert_match_selection(finder, 0, "3.txt");
2018 assert_match_at_position(finder, 1, "2.txt");
2019 assert_match_at_position(finder, 2, "1.txt");
2020 });
2021
2022 cx.dispatch_action(SelectNext);
2023 cx.dispatch_action(Confirm); // Open 2.txt
2024
2025 let picker = open_file_picker(&workspace, cx);
2026 picker.update(cx, |finder, _| {
2027 assert_eq!(finder.delegate.matches.len(), 3);
2028 assert_match_selection(finder, 0, "2.txt");
2029 assert_match_at_position(finder, 1, "3.txt");
2030 assert_match_at_position(finder, 2, "1.txt");
2031 });
2032
2033 cx.dispatch_action(SelectNext);
2034 cx.dispatch_action(SelectNext);
2035 cx.dispatch_action(Confirm); // Open 1.txt
2036
2037 let picker = open_file_picker(&workspace, cx);
2038 picker.update(cx, |finder, _| {
2039 assert_eq!(finder.delegate.matches.len(), 3);
2040 assert_match_selection(finder, 0, "1.txt");
2041 assert_match_at_position(finder, 1, "2.txt");
2042 assert_match_at_position(finder, 2, "3.txt");
2043 });
2044}
2045
2046#[gpui::test]
2047async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
2048 let app_state = init_test(cx);
2049
2050 app_state
2051 .fs
2052 .as_fake()
2053 .insert_tree(
2054 path!("/test"),
2055 json!({
2056 "test": {
2057 "1.txt": "// One",
2058 "2.txt": "// Two",
2059 "3.txt": "// Three",
2060 }
2061 }),
2062 )
2063 .await;
2064
2065 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2066 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2067
2068 open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2069 open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2070 open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2071
2072 let picker = open_file_picker(&workspace, cx);
2073 picker.update(cx, |finder, _| {
2074 assert_eq!(finder.delegate.matches.len(), 3);
2075 assert_match_selection(finder, 0, "3.txt");
2076 assert_match_at_position(finder, 1, "2.txt");
2077 assert_match_at_position(finder, 2, "1.txt");
2078 });
2079
2080 cx.dispatch_action(SelectNext);
2081
2082 // Add more files to the worktree to trigger update matches
2083 for i in 0..5 {
2084 let filename = if cfg!(windows) {
2085 format!("C:/test/{}.txt", 4 + i)
2086 } else {
2087 format!("/test/{}.txt", 4 + i)
2088 };
2089 app_state
2090 .fs
2091 .create_file(Path::new(&filename), Default::default())
2092 .await
2093 .expect("unable to create file");
2094 }
2095
2096 cx.executor().advance_clock(FS_WATCH_LATENCY);
2097
2098 picker.update(cx, |finder, _| {
2099 assert_eq!(finder.delegate.matches.len(), 3);
2100 assert_match_at_position(finder, 0, "3.txt");
2101 assert_match_selection(finder, 1, "2.txt");
2102 assert_match_at_position(finder, 2, "1.txt");
2103 });
2104}
2105
2106#[gpui::test]
2107async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
2108 let app_state = init_test(cx);
2109
2110 app_state
2111 .fs
2112 .as_fake()
2113 .insert_tree(
2114 path!("/src"),
2115 json!({
2116 "collab_ui": {
2117 "first.rs": "// First Rust file",
2118 "second.rs": "// Second Rust file",
2119 "third.rs": "// Third Rust file",
2120 "collab_ui.rs": "// Fourth Rust file",
2121 }
2122 }),
2123 )
2124 .await;
2125
2126 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2127 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2128 // generate some history to select from
2129 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2130 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2131 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2132 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2133
2134 let finder = open_file_picker(&workspace, cx);
2135 let query = "collab_ui";
2136 cx.simulate_input(query);
2137 finder.update(cx, |picker, _| {
2138 let search_entries = collect_search_matches(picker).search_paths_only();
2139 assert_eq!(
2140 search_entries,
2141 vec![
2142 rel_path("collab_ui/collab_ui.rs").into(),
2143 rel_path("collab_ui/first.rs").into(),
2144 rel_path("collab_ui/third.rs").into(),
2145 rel_path("collab_ui/second.rs").into(),
2146 ],
2147 "Despite all search results having the same directory name, the most matching one should be on top"
2148 );
2149 });
2150}
2151
2152#[gpui::test]
2153async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
2154 let app_state = init_test(cx);
2155
2156 app_state
2157 .fs
2158 .as_fake()
2159 .insert_tree(
2160 path!("/src"),
2161 json!({
2162 "test": {
2163 "first.rs": "// First Rust file",
2164 "nonexistent.rs": "// Second Rust file",
2165 "third.rs": "// Third Rust file",
2166 }
2167 }),
2168 )
2169 .await;
2170
2171 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2172 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from
2173 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2174 open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
2175 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2176 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2177 app_state
2178 .fs
2179 .remove_file(
2180 Path::new(path!("/src/test/nonexistent.rs")),
2181 RemoveOptions::default(),
2182 )
2183 .await
2184 .unwrap();
2185 cx.run_until_parked();
2186
2187 let picker = open_file_picker(&workspace, cx);
2188 cx.simulate_input("rs");
2189
2190 picker.update(cx, |picker, _| {
2191 assert_eq!(
2192 collect_search_matches(picker).history,
2193 vec![
2194 rel_path("test/first.rs").into(),
2195 rel_path("test/third.rs").into()
2196 ],
2197 "Should have all opened files in the history, except the ones that do not exist on disk"
2198 );
2199 });
2200}
2201
2202#[gpui::test]
2203async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
2204 let app_state = init_test(cx);
2205
2206 app_state
2207 .fs
2208 .as_fake()
2209 .insert_tree(
2210 "/src",
2211 json!({
2212 "lib.rs": "// Lib file",
2213 "main.rs": "// Bar file",
2214 "read.me": "// Readme file",
2215 }),
2216 )
2217 .await;
2218
2219 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2220 let (workspace, cx) =
2221 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2222
2223 // Initial state
2224 let picker = open_file_picker(&workspace, cx);
2225 cx.simulate_input("rs");
2226 picker.update(cx, |finder, _| {
2227 assert_eq!(finder.delegate.matches.len(), 3);
2228 assert_match_at_position(finder, 0, "lib.rs");
2229 assert_match_at_position(finder, 1, "main.rs");
2230 assert_match_at_position(finder, 2, "rs");
2231 });
2232
2233 // Delete main.rs
2234 app_state
2235 .fs
2236 .remove_file("/src/main.rs".as_ref(), Default::default())
2237 .await
2238 .expect("unable to remove file");
2239 cx.executor().advance_clock(FS_WATCH_LATENCY);
2240
2241 // main.rs is in not among search results anymore
2242 picker.update(cx, |finder, _| {
2243 assert_eq!(finder.delegate.matches.len(), 2);
2244 assert_match_at_position(finder, 0, "lib.rs");
2245 assert_match_at_position(finder, 1, "rs");
2246 });
2247
2248 // Create util.rs
2249 app_state
2250 .fs
2251 .create_file("/src/util.rs".as_ref(), Default::default())
2252 .await
2253 .expect("unable to create file");
2254 cx.executor().advance_clock(FS_WATCH_LATENCY);
2255
2256 // util.rs is among search results
2257 picker.update(cx, |finder, _| {
2258 assert_eq!(finder.delegate.matches.len(), 3);
2259 assert_match_at_position(finder, 0, "lib.rs");
2260 assert_match_at_position(finder, 1, "util.rs");
2261 assert_match_at_position(finder, 2, "rs");
2262 });
2263}
2264
2265#[gpui::test]
2266async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
2267 cx: &mut gpui::TestAppContext,
2268) {
2269 let app_state = init_test(cx);
2270
2271 app_state
2272 .fs
2273 .as_fake()
2274 .insert_tree(
2275 "/test",
2276 json!({
2277 "project_1": {
2278 "bar.rs": "// Bar file",
2279 "lib.rs": "// Lib file",
2280 },
2281 "project_2": {
2282 "Cargo.toml": "// Cargo file",
2283 "main.rs": "// Main file",
2284 }
2285 }),
2286 )
2287 .await;
2288
2289 let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
2290 let (workspace, cx) =
2291 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2292 let worktree_1_id = project.update(cx, |project, cx| {
2293 let worktree = project.worktrees(cx).last().expect("worktree not found");
2294 worktree.read(cx).id()
2295 });
2296
2297 // Initial state
2298 let picker = open_file_picker(&workspace, cx);
2299 cx.simulate_input("rs");
2300 picker.update(cx, |finder, _| {
2301 assert_eq!(finder.delegate.matches.len(), 3);
2302 assert_match_at_position(finder, 0, "bar.rs");
2303 assert_match_at_position(finder, 1, "lib.rs");
2304 assert_match_at_position(finder, 2, "rs");
2305 });
2306
2307 // Add new worktree
2308 project
2309 .update(cx, |project, cx| {
2310 project
2311 .find_or_create_worktree("/test/project_2", true, cx)
2312 .into_future()
2313 })
2314 .await
2315 .expect("unable to create workdir");
2316 cx.executor().advance_clock(FS_WATCH_LATENCY);
2317
2318 // main.rs is among search results
2319 picker.update(cx, |finder, _| {
2320 assert_eq!(finder.delegate.matches.len(), 4);
2321 assert_match_at_position(finder, 0, "bar.rs");
2322 assert_match_at_position(finder, 1, "lib.rs");
2323 assert_match_at_position(finder, 2, "main.rs");
2324 assert_match_at_position(finder, 3, "rs");
2325 });
2326
2327 // Remove the first worktree
2328 project.update(cx, |project, cx| {
2329 project.remove_worktree(worktree_1_id, cx);
2330 });
2331 cx.executor().advance_clock(FS_WATCH_LATENCY);
2332
2333 // Files from the first worktree are not in the search results anymore
2334 picker.update(cx, |finder, _| {
2335 assert_eq!(finder.delegate.matches.len(), 2);
2336 assert_match_at_position(finder, 0, "main.rs");
2337 assert_match_at_position(finder, 1, "rs");
2338 });
2339}
2340
2341#[gpui::test]
2342async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
2343 let app_state = init_test(cx);
2344
2345 app_state.fs.as_fake().insert_tree("/src", json!({})).await;
2346
2347 app_state
2348 .fs
2349 .create_dir("/src/even".as_ref())
2350 .await
2351 .expect("unable to create dir");
2352
2353 let initial_files_num = 5;
2354 for i in 0..initial_files_num {
2355 let filename = format!("/src/even/file_{}.txt", 10 + i);
2356 app_state
2357 .fs
2358 .create_file(Path::new(&filename), Default::default())
2359 .await
2360 .expect("unable to create file");
2361 }
2362
2363 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2364 let (workspace, cx) =
2365 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2366
2367 // Initial state
2368 let picker = open_file_picker(&workspace, cx);
2369 cx.simulate_input("file");
2370 let selected_index = 3;
2371 // Checking only the filename, not the whole path
2372 let selected_file = format!("file_{}.txt", 10 + selected_index);
2373 // Select even/file_13.txt
2374 for _ in 0..selected_index {
2375 cx.dispatch_action(SelectNext);
2376 }
2377
2378 picker.update(cx, |finder, _| {
2379 assert_match_selection(finder, selected_index, &selected_file)
2380 });
2381
2382 // Add more matches to the search results
2383 let files_to_add = 10;
2384 for i in 0..files_to_add {
2385 let filename = format!("/src/file_{}.txt", 20 + i);
2386 app_state
2387 .fs
2388 .create_file(Path::new(&filename), Default::default())
2389 .await
2390 .expect("unable to create file");
2391 }
2392 cx.executor().advance_clock(FS_WATCH_LATENCY);
2393
2394 // file_13.txt is still selected
2395 picker.update(cx, |finder, _| {
2396 let expected_selected_index = selected_index + files_to_add;
2397 assert_match_selection(finder, expected_selected_index, &selected_file);
2398 });
2399}
2400
2401#[gpui::test]
2402async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
2403 cx: &mut gpui::TestAppContext,
2404) {
2405 let app_state = init_test(cx);
2406
2407 app_state
2408 .fs
2409 .as_fake()
2410 .insert_tree(
2411 "/src",
2412 json!({
2413 "file_1.txt": "// file_1",
2414 "file_2.txt": "// file_2",
2415 "file_3.txt": "// file_3",
2416 }),
2417 )
2418 .await;
2419
2420 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2421 let (workspace, cx) =
2422 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2423
2424 // Initial state
2425 let picker = open_file_picker(&workspace, cx);
2426 cx.simulate_input("file");
2427 // Select even/file_2.txt
2428 cx.dispatch_action(SelectNext);
2429
2430 // Remove the selected entry
2431 app_state
2432 .fs
2433 .remove_file("/src/file_2.txt".as_ref(), Default::default())
2434 .await
2435 .expect("unable to remove file");
2436 cx.executor().advance_clock(FS_WATCH_LATENCY);
2437
2438 // file_1.txt is now selected
2439 picker.update(cx, |finder, _| {
2440 assert_match_selection(finder, 0, "file_1.txt");
2441 });
2442}
2443
2444#[gpui::test]
2445async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2446 let app_state = init_test(cx);
2447
2448 app_state
2449 .fs
2450 .as_fake()
2451 .insert_tree(
2452 path!("/test"),
2453 json!({
2454 "1.txt": "// One",
2455 }),
2456 )
2457 .await;
2458
2459 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2460 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2461
2462 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2463
2464 cx.simulate_modifiers_change(Modifiers::secondary_key());
2465 open_file_picker(&workspace, cx);
2466
2467 cx.simulate_modifiers_change(Modifiers::none());
2468 active_file_picker(&workspace, cx);
2469}
2470
2471#[gpui::test]
2472async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2473 let app_state = init_test(cx);
2474
2475 app_state
2476 .fs
2477 .as_fake()
2478 .insert_tree(
2479 path!("/test"),
2480 json!({
2481 "1.txt": "// One",
2482 "2.txt": "// Two",
2483 }),
2484 )
2485 .await;
2486
2487 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2488 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2489
2490 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2491 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2492
2493 cx.simulate_modifiers_change(Modifiers::secondary_key());
2494 let picker = open_file_picker(&workspace, cx);
2495 picker.update(cx, |finder, _| {
2496 assert_eq!(finder.delegate.matches.len(), 2);
2497 assert_match_selection(finder, 0, "2.txt");
2498 assert_match_at_position(finder, 1, "1.txt");
2499 });
2500
2501 cx.dispatch_action(SelectNext);
2502 cx.simulate_modifiers_change(Modifiers::none());
2503 cx.read(|cx| {
2504 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2505 assert_eq!(active_editor.read(cx).title(cx), "1.txt");
2506 });
2507}
2508
2509#[gpui::test]
2510async fn test_switches_between_release_norelease_modes_on_forward_nav(
2511 cx: &mut gpui::TestAppContext,
2512) {
2513 let app_state = init_test(cx);
2514
2515 app_state
2516 .fs
2517 .as_fake()
2518 .insert_tree(
2519 path!("/test"),
2520 json!({
2521 "1.txt": "// One",
2522 "2.txt": "// Two",
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
2533 // Open with a shortcut
2534 cx.simulate_modifiers_change(Modifiers::secondary_key());
2535 let picker = open_file_picker(&workspace, cx);
2536 picker.update(cx, |finder, _| {
2537 assert_eq!(finder.delegate.matches.len(), 2);
2538 assert_match_selection(finder, 0, "2.txt");
2539 assert_match_at_position(finder, 1, "1.txt");
2540 });
2541
2542 // Switch to navigating with other shortcuts
2543 // Don't open file on modifiers release
2544 cx.simulate_modifiers_change(Modifiers::control());
2545 cx.dispatch_action(SelectNext);
2546 cx.simulate_modifiers_change(Modifiers::none());
2547 picker.update(cx, |finder, _| {
2548 assert_eq!(finder.delegate.matches.len(), 2);
2549 assert_match_at_position(finder, 0, "2.txt");
2550 assert_match_selection(finder, 1, "1.txt");
2551 });
2552
2553 // Back to navigation with initial shortcut
2554 // Open file on modifiers release
2555 cx.simulate_modifiers_change(Modifiers::secondary_key());
2556 cx.dispatch_action(ToggleFileFinder::default());
2557 cx.simulate_modifiers_change(Modifiers::none());
2558 cx.read(|cx| {
2559 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2560 assert_eq!(active_editor.read(cx).title(cx), "2.txt");
2561 });
2562}
2563
2564#[gpui::test]
2565async fn test_switches_between_release_norelease_modes_on_backward_nav(
2566 cx: &mut gpui::TestAppContext,
2567) {
2568 let app_state = init_test(cx);
2569
2570 app_state
2571 .fs
2572 .as_fake()
2573 .insert_tree(
2574 path!("/test"),
2575 json!({
2576 "1.txt": "// One",
2577 "2.txt": "// Two",
2578 "3.txt": "// Three"
2579 }),
2580 )
2581 .await;
2582
2583 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2584 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2585
2586 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2587 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2588 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2589
2590 // Open with a shortcut
2591 cx.simulate_modifiers_change(Modifiers::secondary_key());
2592 let picker = open_file_picker(&workspace, cx);
2593 picker.update(cx, |finder, _| {
2594 assert_eq!(finder.delegate.matches.len(), 3);
2595 assert_match_selection(finder, 0, "3.txt");
2596 assert_match_at_position(finder, 1, "2.txt");
2597 assert_match_at_position(finder, 2, "1.txt");
2598 });
2599
2600 // Switch to navigating with other shortcuts
2601 // Don't open file on modifiers release
2602 cx.simulate_modifiers_change(Modifiers::control());
2603 cx.dispatch_action(menu::SelectPrevious);
2604 cx.simulate_modifiers_change(Modifiers::none());
2605 picker.update(cx, |finder, _| {
2606 assert_eq!(finder.delegate.matches.len(), 3);
2607 assert_match_at_position(finder, 0, "3.txt");
2608 assert_match_at_position(finder, 1, "2.txt");
2609 assert_match_selection(finder, 2, "1.txt");
2610 });
2611
2612 // Back to navigation with initial shortcut
2613 // Open file on modifiers release
2614 cx.simulate_modifiers_change(Modifiers::secondary_key());
2615 cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
2616 cx.simulate_modifiers_change(Modifiers::none());
2617 cx.read(|cx| {
2618 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2619 assert_eq!(active_editor.read(cx).title(cx), "3.txt");
2620 });
2621}
2622
2623#[gpui::test]
2624async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
2625 let app_state = init_test(cx);
2626
2627 app_state
2628 .fs
2629 .as_fake()
2630 .insert_tree(
2631 path!("/test"),
2632 json!({
2633 "1.txt": "// One",
2634 }),
2635 )
2636 .await;
2637
2638 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2639 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2640
2641 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2642
2643 cx.simulate_modifiers_change(Modifiers::secondary_key());
2644 open_file_picker(&workspace, cx);
2645
2646 cx.simulate_modifiers_change(Modifiers::command_shift());
2647 active_file_picker(&workspace, cx);
2648}
2649
2650#[gpui::test]
2651async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
2652 let app_state = init_test(cx);
2653 app_state
2654 .fs
2655 .as_fake()
2656 .insert_tree(
2657 "/test",
2658 json!({
2659 "00.txt": "",
2660 "01.txt": "",
2661 "02.txt": "",
2662 "03.txt": "",
2663 "04.txt": "",
2664 "05.txt": "",
2665 }),
2666 )
2667 .await;
2668
2669 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
2670 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2671
2672 cx.dispatch_action(ToggleFileFinder::default());
2673 let picker = active_file_picker(&workspace, cx);
2674
2675 picker.update_in(cx, |picker, window, cx| {
2676 picker.update_matches(".txt".to_string(), window, cx)
2677 });
2678
2679 cx.run_until_parked();
2680
2681 picker.update(cx, |picker, _| {
2682 assert_eq!(picker.delegate.matches.len(), 7);
2683 assert_eq!(picker.delegate.selected_index, 0);
2684 });
2685
2686 // When toggling repeatedly, the picker scrolls to reveal the selected item.
2687 cx.dispatch_action(ToggleFileFinder::default());
2688 cx.dispatch_action(ToggleFileFinder::default());
2689 cx.dispatch_action(ToggleFileFinder::default());
2690
2691 cx.run_until_parked();
2692
2693 picker.update(cx, |picker, _| {
2694 assert_eq!(picker.delegate.matches.len(), 7);
2695 assert_eq!(picker.delegate.selected_index, 3);
2696 });
2697}
2698
2699async fn open_close_queried_buffer(
2700 input: &str,
2701 expected_matches: usize,
2702 expected_editor_title: &str,
2703 workspace: &Entity<Workspace>,
2704 cx: &mut gpui::VisualTestContext,
2705) -> Vec<FoundPath> {
2706 let history_items = open_queried_buffer(
2707 input,
2708 expected_matches,
2709 expected_editor_title,
2710 workspace,
2711 cx,
2712 )
2713 .await;
2714
2715 cx.dispatch_action(workspace::CloseActiveItem {
2716 save_intent: None,
2717 close_pinned: false,
2718 });
2719
2720 history_items
2721}
2722
2723async fn open_queried_buffer(
2724 input: &str,
2725 expected_matches: usize,
2726 expected_editor_title: &str,
2727 workspace: &Entity<Workspace>,
2728 cx: &mut gpui::VisualTestContext,
2729) -> Vec<FoundPath> {
2730 let picker = open_file_picker(workspace, cx);
2731 cx.simulate_input(input);
2732
2733 let history_items = picker.update(cx, |finder, _| {
2734 assert_eq!(
2735 finder.delegate.matches.len(),
2736 expected_matches + 1, // +1 from CreateNew option
2737 "Unexpected number of matches found for query `{input}`, matches: {:?}",
2738 finder.delegate.matches
2739 );
2740 finder.delegate.history_items.clone()
2741 });
2742
2743 cx.dispatch_action(Confirm);
2744
2745 cx.read(|cx| {
2746 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2747 let active_editor_title = active_editor.read(cx).title(cx);
2748 assert_eq!(
2749 expected_editor_title, active_editor_title,
2750 "Unexpected editor title for query `{input}`"
2751 );
2752 });
2753
2754 history_items
2755}
2756
2757fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2758 cx.update(|cx| {
2759 let state = AppState::test(cx);
2760 theme::init(theme::LoadThemes::JustBase, cx);
2761 language::init(cx);
2762 super::init(cx);
2763 editor::init(cx);
2764 workspace::init_settings(cx);
2765 Project::init_settings(cx);
2766 state
2767 })
2768}
2769
2770fn test_path_position(test_str: &str) -> FileSearchQuery {
2771 let path_position = PathWithPosition::parse_str(test_str);
2772
2773 FileSearchQuery {
2774 raw_query: test_str.to_owned(),
2775 file_query_end: if path_position.path.to_str().unwrap() == test_str {
2776 None
2777 } else {
2778 Some(path_position.path.to_str().unwrap().len())
2779 },
2780 path_position,
2781 }
2782}
2783
2784fn build_find_picker(
2785 project: Entity<Project>,
2786 cx: &mut TestAppContext,
2787) -> (
2788 Entity<Picker<FileFinderDelegate>>,
2789 Entity<Workspace>,
2790 &mut VisualTestContext,
2791) {
2792 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2793 let picker = open_file_picker(&workspace, cx);
2794 (picker, workspace, cx)
2795}
2796
2797#[track_caller]
2798fn open_file_picker(
2799 workspace: &Entity<Workspace>,
2800 cx: &mut VisualTestContext,
2801) -> Entity<Picker<FileFinderDelegate>> {
2802 cx.dispatch_action(ToggleFileFinder {
2803 separate_history: true,
2804 });
2805 active_file_picker(workspace, cx)
2806}
2807
2808#[track_caller]
2809fn active_file_picker(
2810 workspace: &Entity<Workspace>,
2811 cx: &mut VisualTestContext,
2812) -> Entity<Picker<FileFinderDelegate>> {
2813 workspace.update(cx, |workspace, cx| {
2814 workspace
2815 .active_modal::<FileFinder>(cx)
2816 .expect("file finder is not open")
2817 .read(cx)
2818 .picker
2819 .clone()
2820 })
2821}
2822
2823#[derive(Debug, Default)]
2824struct SearchEntries {
2825 history: Vec<Arc<RelPath>>,
2826 history_found_paths: Vec<FoundPath>,
2827 search: Vec<Arc<RelPath>>,
2828 search_matches: Vec<PathMatch>,
2829}
2830
2831impl SearchEntries {
2832 #[track_caller]
2833 fn search_paths_only(self) -> Vec<Arc<RelPath>> {
2834 assert!(
2835 self.history.is_empty(),
2836 "Should have no history matches, but got: {:?}",
2837 self.history
2838 );
2839 self.search
2840 }
2841
2842 #[track_caller]
2843 fn search_matches_only(self) -> Vec<PathMatch> {
2844 assert!(
2845 self.history.is_empty(),
2846 "Should have no history matches, but got: {:?}",
2847 self.history
2848 );
2849 self.search_matches
2850 }
2851}
2852
2853fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
2854 let mut search_entries = SearchEntries::default();
2855 for m in &picker.delegate.matches.matches {
2856 match &m {
2857 Match::History {
2858 path: history_path,
2859 panel_match: path_match,
2860 } => {
2861 if let Some(path_match) = path_match.as_ref() {
2862 search_entries
2863 .history
2864 .push(path_match.0.path_prefix.join(&path_match.0.path));
2865 } else {
2866 // This occurs when the query is empty and we show history matches
2867 // that are outside the project.
2868 panic!("currently not exercised in tests");
2869 }
2870 search_entries
2871 .history_found_paths
2872 .push(history_path.clone());
2873 }
2874 Match::Search(path_match) => {
2875 search_entries
2876 .search
2877 .push(path_match.0.path_prefix.join(&path_match.0.path));
2878 search_entries.search_matches.push(path_match.0.clone());
2879 }
2880 Match::CreateNew(_) => {}
2881 }
2882 }
2883 search_entries
2884}
2885
2886#[track_caller]
2887fn assert_match_selection(
2888 finder: &Picker<FileFinderDelegate>,
2889 expected_selection_index: usize,
2890 expected_file_name: &str,
2891) {
2892 assert_eq!(
2893 finder.delegate.selected_index(),
2894 expected_selection_index,
2895 "Match is not selected"
2896 );
2897 assert_match_at_position(finder, expected_selection_index, expected_file_name);
2898}
2899
2900#[track_caller]
2901fn assert_match_at_position(
2902 finder: &Picker<FileFinderDelegate>,
2903 match_index: usize,
2904 expected_file_name: &str,
2905) {
2906 let match_item = finder
2907 .delegate
2908 .matches
2909 .get(match_index)
2910 .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
2911 let match_file_name = match &match_item {
2912 Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
2913 Match::Search(path_match) => path_match.0.path.file_name(),
2914 Match::CreateNew(project_path) => project_path.path.file_name(),
2915 }
2916 .unwrap();
2917 assert_eq!(match_file_name, expected_file_name);
2918}
2919
2920#[gpui::test]
2921async fn test_filename_precedence(cx: &mut TestAppContext) {
2922 let app_state = init_test(cx);
2923
2924 app_state
2925 .fs
2926 .as_fake()
2927 .insert_tree(
2928 path!("/src"),
2929 json!({
2930 "layout": {
2931 "app.css": "",
2932 "app.d.ts": "",
2933 "app.html": "",
2934 "+page.svelte": "",
2935 },
2936 "routes": {
2937 "+layout.svelte": "",
2938 }
2939 }),
2940 )
2941 .await;
2942
2943 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2944 let (picker, _, cx) = build_find_picker(project, cx);
2945
2946 cx.simulate_input("layout");
2947
2948 picker.update(cx, |finder, _| {
2949 let search_matches = collect_search_matches(finder).search_paths_only();
2950
2951 assert_eq!(
2952 search_matches,
2953 vec![
2954 rel_path("routes/+layout.svelte").into(),
2955 rel_path("layout/app.css").into(),
2956 rel_path("layout/app.d.ts").into(),
2957 rel_path("layout/app.html").into(),
2958 rel_path("layout/+page.svelte").into(),
2959 ],
2960 "File with 'layout' in filename should be prioritized over files in 'layout' directory"
2961 );
2962 });
2963}