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