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