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