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