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