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