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