1use std::{assert_eq, path::Path, time::Duration};
2
3use super::*;
4use editor::Editor;
5use gpui::{Entity, TestAppContext, VisualTestContext};
6use menu::{Confirm, SelectNext};
7use serde_json::json;
8use workspace::{AppState, Workspace};
9
10#[ctor::ctor]
11fn init_logger() {
12 if std::env::var("RUST_LOG").is_ok() {
13 env_logger::init();
14 }
15}
16
17#[gpui::test]
18async fn test_matching_paths(cx: &mut TestAppContext) {
19 let app_state = init_test(cx);
20 app_state
21 .fs
22 .as_fake()
23 .insert_tree(
24 "/root",
25 json!({
26 "a": {
27 "banana": "",
28 "bandana": "",
29 }
30 }),
31 )
32 .await;
33
34 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
35
36 let (picker, workspace, cx) = build_find_picker(project, cx);
37
38 cx.simulate_input("bna");
39 picker.update(cx, |picker, _| {
40 assert_eq!(picker.delegate.matches.len(), 2);
41 });
42 cx.dispatch_action(SelectNext);
43 cx.dispatch_action(Confirm);
44 cx.read(|cx| {
45 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
46 assert_eq!(active_editor.read(cx).title(cx), "bandana");
47 });
48
49 for bandana_query in [
50 "bandana",
51 " bandana",
52 "bandana ",
53 " bandana ",
54 " ndan ",
55 " band ",
56 "a bandana",
57 ] {
58 picker
59 .update(cx, |picker, cx| {
60 picker
61 .delegate
62 .update_matches(bandana_query.to_string(), cx)
63 })
64 .await;
65 picker.update(cx, |picker, _| {
66 assert_eq!(
67 picker.delegate.matches.len(),
68 1,
69 "Wrong number of matches for bandana query '{bandana_query}'"
70 );
71 });
72 cx.dispatch_action(SelectNext);
73 cx.dispatch_action(Confirm);
74 cx.read(|cx| {
75 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
76 assert_eq!(
77 active_editor.read(cx).title(cx),
78 "bandana",
79 "Wrong match for bandana query '{bandana_query}'"
80 );
81 });
82 }
83}
84
85#[gpui::test]
86async fn test_absolute_paths(cx: &mut TestAppContext) {
87 let app_state = init_test(cx);
88 app_state
89 .fs
90 .as_fake()
91 .insert_tree(
92 "/root",
93 json!({
94 "a": {
95 "file1.txt": "",
96 "b": {
97 "file2.txt": "",
98 },
99 }
100 }),
101 )
102 .await;
103
104 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
105
106 let (picker, workspace, cx) = build_find_picker(project, cx);
107
108 let matching_abs_path = "/root/a/b/file2.txt";
109 picker
110 .update(cx, |picker, cx| {
111 picker
112 .delegate
113 .update_matches(matching_abs_path.to_string(), cx)
114 })
115 .await;
116 picker.update(cx, |picker, _| {
117 assert_eq!(
118 collect_search_matches(picker).search_only(),
119 vec![PathBuf::from("a/b/file2.txt")],
120 "Matching abs path should be the only match"
121 )
122 });
123 cx.dispatch_action(SelectNext);
124 cx.dispatch_action(Confirm);
125 cx.read(|cx| {
126 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
127 assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
128 });
129
130 let mismatching_abs_path = "/root/a/b/file1.txt";
131 picker
132 .update(cx, |picker, cx| {
133 picker
134 .delegate
135 .update_matches(mismatching_abs_path.to_string(), cx)
136 })
137 .await;
138 picker.update(cx, |picker, _| {
139 assert_eq!(
140 collect_search_matches(picker).search_only(),
141 Vec::<PathBuf>::new(),
142 "Mismatching abs path should produce no matches"
143 )
144 });
145}
146
147#[gpui::test]
148async fn test_complex_path(cx: &mut TestAppContext) {
149 let app_state = init_test(cx);
150 app_state
151 .fs
152 .as_fake()
153 .insert_tree(
154 "/root",
155 json!({
156 "其他": {
157 "S数据表格": {
158 "task.xlsx": "some content",
159 },
160 }
161 }),
162 )
163 .await;
164
165 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
166
167 let (picker, workspace, cx) = build_find_picker(project, cx);
168
169 cx.simulate_input("t");
170 picker.update(cx, |picker, _| {
171 assert_eq!(picker.delegate.matches.len(), 1);
172 assert_eq!(
173 collect_search_matches(picker).search_only(),
174 vec![PathBuf::from("其他/S数据表格/task.xlsx")],
175 )
176 });
177 cx.dispatch_action(SelectNext);
178 cx.dispatch_action(Confirm);
179 cx.read(|cx| {
180 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
181 assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
182 });
183}
184
185#[gpui::test]
186async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
187 let app_state = init_test(cx);
188
189 let first_file_name = "first.rs";
190 let first_file_contents = "// First Rust file";
191 app_state
192 .fs
193 .as_fake()
194 .insert_tree(
195 "/src",
196 json!({
197 "test": {
198 first_file_name: first_file_contents,
199 "second.rs": "// Second Rust file",
200 }
201 }),
202 )
203 .await;
204
205 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
206
207 let (picker, workspace, cx) = build_find_picker(project, cx);
208
209 let file_query = &first_file_name[..3];
210 let file_row = 1;
211 let file_column = 3;
212 assert!(file_column <= first_file_contents.len());
213 let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
214 picker
215 .update(cx, |finder, cx| {
216 finder
217 .delegate
218 .update_matches(query_inside_file.to_string(), cx)
219 })
220 .await;
221 picker.update(cx, |finder, _| {
222 let finder = &finder.delegate;
223 assert_eq!(finder.matches.len(), 1);
224 let latest_search_query = finder
225 .latest_search_query
226 .as_ref()
227 .expect("Finder should have a query after the update_matches call");
228 assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
229 assert_eq!(
230 latest_search_query.path_like.file_query_end,
231 Some(file_query.len())
232 );
233 assert_eq!(latest_search_query.row, Some(file_row));
234 assert_eq!(latest_search_query.column, Some(file_column as u32));
235 });
236
237 cx.dispatch_action(SelectNext);
238 cx.dispatch_action(Confirm);
239
240 let editor = cx.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
241 cx.executor().advance_clock(Duration::from_secs(2));
242
243 editor.update(cx, |editor, cx| {
244 let all_selections = editor.selections.all_adjusted(cx);
245 assert_eq!(
246 all_selections.len(),
247 1,
248 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
249 );
250 let caret_selection = all_selections.into_iter().next().unwrap();
251 assert_eq!(caret_selection.start, caret_selection.end,
252 "Caret selection should have its start and end at the same position");
253 assert_eq!(file_row, caret_selection.start.row + 1,
254 "Query inside file should get caret with the same focus row");
255 assert_eq!(file_column, caret_selection.start.column as usize + 1,
256 "Query inside file should get caret with the same focus column");
257 });
258}
259
260#[gpui::test]
261async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
262 let app_state = init_test(cx);
263
264 let first_file_name = "first.rs";
265 let first_file_contents = "// First Rust file";
266 app_state
267 .fs
268 .as_fake()
269 .insert_tree(
270 "/src",
271 json!({
272 "test": {
273 first_file_name: first_file_contents,
274 "second.rs": "// Second Rust file",
275 }
276 }),
277 )
278 .await;
279
280 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
281
282 let (picker, workspace, cx) = build_find_picker(project, cx);
283
284 let file_query = &first_file_name[..3];
285 let file_row = 200;
286 let file_column = 300;
287 assert!(file_column > first_file_contents.len());
288 let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
289 picker
290 .update(cx, |picker, cx| {
291 picker
292 .delegate
293 .update_matches(query_outside_file.to_string(), cx)
294 })
295 .await;
296 picker.update(cx, |finder, _| {
297 let delegate = &finder.delegate;
298 assert_eq!(delegate.matches.len(), 1);
299 let latest_search_query = delegate
300 .latest_search_query
301 .as_ref()
302 .expect("Finder should have a query after the update_matches call");
303 assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
304 assert_eq!(
305 latest_search_query.path_like.file_query_end,
306 Some(file_query.len())
307 );
308 assert_eq!(latest_search_query.row, Some(file_row));
309 assert_eq!(latest_search_query.column, Some(file_column as u32));
310 });
311
312 cx.dispatch_action(SelectNext);
313 cx.dispatch_action(Confirm);
314
315 let editor = cx.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
316 cx.executor().advance_clock(Duration::from_secs(2));
317
318 editor.update(cx, |editor, cx| {
319 let all_selections = editor.selections.all_adjusted(cx);
320 assert_eq!(
321 all_selections.len(),
322 1,
323 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
324 );
325 let caret_selection = all_selections.into_iter().next().unwrap();
326 assert_eq!(caret_selection.start, caret_selection.end,
327 "Caret selection should have its start and end at the same position");
328 assert_eq!(0, caret_selection.start.row,
329 "Excessive rows (as in query outside file borders) should get trimmed to last file row");
330 assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
331 "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
332 });
333}
334
335#[gpui::test]
336async fn test_matching_cancellation(cx: &mut TestAppContext) {
337 let app_state = init_test(cx);
338 app_state
339 .fs
340 .as_fake()
341 .insert_tree(
342 "/dir",
343 json!({
344 "hello": "",
345 "goodbye": "",
346 "halogen-light": "",
347 "happiness": "",
348 "height": "",
349 "hi": "",
350 "hiccup": "",
351 }),
352 )
353 .await;
354
355 let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
356
357 let (picker, _, cx) = build_find_picker(project, cx);
358
359 let query = test_path_like("hi");
360 picker
361 .update(cx, |picker, cx| {
362 picker.delegate.spawn_search(query.clone(), cx)
363 })
364 .await;
365
366 picker.update(cx, |picker, _cx| {
367 assert_eq!(picker.delegate.matches.len(), 5)
368 });
369
370 picker.update(cx, |picker, cx| {
371 let delegate = &mut picker.delegate;
372 assert!(
373 delegate.matches.history.is_empty(),
374 "Search matches expected"
375 );
376 let matches = delegate.matches.search.clone();
377
378 // Simulate a search being cancelled after the time limit,
379 // returning only a subset of the matches that would have been found.
380 drop(delegate.spawn_search(query.clone(), cx));
381 delegate.set_search_matches(
382 delegate.latest_search_id,
383 true, // did-cancel
384 query.clone(),
385 vec![matches[1].clone(), matches[3].clone()],
386 cx,
387 );
388
389 // Simulate another cancellation.
390 drop(delegate.spawn_search(query.clone(), cx));
391 delegate.set_search_matches(
392 delegate.latest_search_id,
393 true, // did-cancel
394 query.clone(),
395 vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
396 cx,
397 );
398
399 assert!(
400 delegate.matches.history.is_empty(),
401 "Search matches expected"
402 );
403 assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
404 });
405}
406
407#[gpui::test]
408async fn test_ignored_root(cx: &mut TestAppContext) {
409 let app_state = init_test(cx);
410 app_state
411 .fs
412 .as_fake()
413 .insert_tree(
414 "/ancestor",
415 json!({
416 ".gitignore": "ignored-root",
417 "ignored-root": {
418 "happiness": "",
419 "height": "",
420 "hi": "",
421 "hiccup": "",
422 },
423 "tracked-root": {
424 ".gitignore": "height",
425 "happiness": "",
426 "height": "",
427 "hi": "",
428 "hiccup": "",
429 },
430 }),
431 )
432 .await;
433
434 let project = Project::test(
435 app_state.fs.clone(),
436 [
437 "/ancestor/tracked-root".as_ref(),
438 "/ancestor/ignored-root".as_ref(),
439 ],
440 cx,
441 )
442 .await;
443
444 let (picker, _, cx) = build_find_picker(project, cx);
445
446 picker
447 .update(cx, |picker, cx| {
448 picker.delegate.spawn_search(test_path_like("hi"), cx)
449 })
450 .await;
451 picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
452}
453
454#[gpui::test]
455async fn test_single_file_worktrees(cx: &mut TestAppContext) {
456 let app_state = init_test(cx);
457 app_state
458 .fs
459 .as_fake()
460 .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
461 .await;
462
463 let project = Project::test(
464 app_state.fs.clone(),
465 ["/root/the-parent-dir/the-file".as_ref()],
466 cx,
467 )
468 .await;
469
470 let (picker, _, cx) = build_find_picker(project, cx);
471
472 // Even though there is only one worktree, that worktree's filename
473 // is included in the matching, because the worktree is a single file.
474 picker
475 .update(cx, |picker, cx| {
476 picker.delegate.spawn_search(test_path_like("thf"), cx)
477 })
478 .await;
479 cx.read(|cx| {
480 let picker = picker.read(cx);
481 let delegate = &picker.delegate;
482 assert!(
483 delegate.matches.history.is_empty(),
484 "Search matches expected"
485 );
486 let matches = delegate.matches.search.clone();
487 assert_eq!(matches.len(), 1);
488
489 let (file_name, file_name_positions, full_path, full_path_positions) =
490 delegate.labels_for_path_match(&matches[0].0);
491 assert_eq!(file_name, "the-file");
492 assert_eq!(file_name_positions, &[0, 1, 4]);
493 assert_eq!(full_path, "");
494 assert_eq!(full_path_positions, &[0; 0]);
495 });
496
497 // Since the worktree root is a file, searching for its name followed by a slash does
498 // not match anything.
499 picker
500 .update(cx, |f, cx| {
501 f.delegate.spawn_search(test_path_like("thf/"), cx)
502 })
503 .await;
504 picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
505}
506
507#[gpui::test]
508async fn test_path_distance_ordering(cx: &mut TestAppContext) {
509 let app_state = init_test(cx);
510 app_state
511 .fs
512 .as_fake()
513 .insert_tree(
514 "/root",
515 json!({
516 "dir1": { "a.txt": "" },
517 "dir2": {
518 "a.txt": "",
519 "b.txt": ""
520 }
521 }),
522 )
523 .await;
524
525 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
526 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
527
528 let worktree_id = cx.read(|cx| {
529 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
530 assert_eq!(worktrees.len(), 1);
531 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
532 });
533
534 // When workspace has an active item, sort items which are closer to that item
535 // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
536 // so that one should be sorted earlier
537 let b_path = ProjectPath {
538 worktree_id,
539 path: Arc::from(Path::new("dir2/b.txt")),
540 };
541 workspace
542 .update(cx, |workspace, cx| {
543 workspace.open_path(b_path, None, true, cx)
544 })
545 .await
546 .unwrap();
547 let finder = open_file_picker(&workspace, cx);
548 finder
549 .update(cx, |f, cx| {
550 f.delegate.spawn_search(test_path_like("a.txt"), cx)
551 })
552 .await;
553
554 finder.update(cx, |f, _| {
555 let delegate = &f.delegate;
556 assert!(
557 delegate.matches.history.is_empty(),
558 "Search matches expected"
559 );
560 let matches = &delegate.matches.search;
561 assert_eq!(matches[0].0.path.as_ref(), Path::new("dir2/a.txt"));
562 assert_eq!(matches[1].0.path.as_ref(), Path::new("dir1/a.txt"));
563 });
564}
565
566#[gpui::test]
567async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
568 let app_state = init_test(cx);
569 app_state
570 .fs
571 .as_fake()
572 .insert_tree(
573 "/root",
574 json!({
575 "dir1": {},
576 "dir2": {
577 "dir3": {}
578 }
579 }),
580 )
581 .await;
582
583 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
584 let (picker, _workspace, cx) = build_find_picker(project, cx);
585
586 picker
587 .update(cx, |f, cx| {
588 f.delegate.spawn_search(test_path_like("dir"), cx)
589 })
590 .await;
591 cx.read(|cx| {
592 let finder = picker.read(cx);
593 assert_eq!(finder.delegate.matches.len(), 0);
594 });
595}
596
597#[gpui::test]
598async fn test_query_history(cx: &mut gpui::TestAppContext) {
599 let app_state = init_test(cx);
600
601 app_state
602 .fs
603 .as_fake()
604 .insert_tree(
605 "/src",
606 json!({
607 "test": {
608 "first.rs": "// First Rust file",
609 "second.rs": "// Second Rust file",
610 "third.rs": "// Third Rust file",
611 }
612 }),
613 )
614 .await;
615
616 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
617 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
618 let worktree_id = cx.read(|cx| {
619 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
620 assert_eq!(worktrees.len(), 1);
621 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
622 });
623
624 // Open and close panels, getting their history items afterwards.
625 // Ensure history items get populated with opened items, and items are kept in a certain order.
626 // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
627 //
628 // TODO: without closing, the opened items do not propagate their history changes for some reason
629 // it does work in real app though, only tests do not propagate.
630 workspace.update(cx, |_, cx| cx.focused());
631
632 let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
633 assert!(
634 initial_history.is_empty(),
635 "Should have no history before opening any files"
636 );
637
638 let history_after_first =
639 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
640 assert_eq!(
641 history_after_first,
642 vec![FoundPath::new(
643 ProjectPath {
644 worktree_id,
645 path: Arc::from(Path::new("test/first.rs")),
646 },
647 Some(PathBuf::from("/src/test/first.rs"))
648 )],
649 "Should show 1st opened item in the history when opening the 2nd item"
650 );
651
652 let history_after_second =
653 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
654 assert_eq!(
655 history_after_second,
656 vec![
657 FoundPath::new(
658 ProjectPath {
659 worktree_id,
660 path: Arc::from(Path::new("test/second.rs")),
661 },
662 Some(PathBuf::from("/src/test/second.rs"))
663 ),
664 FoundPath::new(
665 ProjectPath {
666 worktree_id,
667 path: Arc::from(Path::new("test/first.rs")),
668 },
669 Some(PathBuf::from("/src/test/first.rs"))
670 ),
671 ],
672 "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
673 2nd item should be the first in the history, as the last opened."
674 );
675
676 let history_after_third =
677 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
678 assert_eq!(
679 history_after_third,
680 vec![
681 FoundPath::new(
682 ProjectPath {
683 worktree_id,
684 path: Arc::from(Path::new("test/third.rs")),
685 },
686 Some(PathBuf::from("/src/test/third.rs"))
687 ),
688 FoundPath::new(
689 ProjectPath {
690 worktree_id,
691 path: Arc::from(Path::new("test/second.rs")),
692 },
693 Some(PathBuf::from("/src/test/second.rs"))
694 ),
695 FoundPath::new(
696 ProjectPath {
697 worktree_id,
698 path: Arc::from(Path::new("test/first.rs")),
699 },
700 Some(PathBuf::from("/src/test/first.rs"))
701 ),
702 ],
703 "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
704 3rd item should be the first in the history, as the last opened."
705 );
706
707 let history_after_second_again =
708 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
709 assert_eq!(
710 history_after_second_again,
711 vec![
712 FoundPath::new(
713 ProjectPath {
714 worktree_id,
715 path: Arc::from(Path::new("test/second.rs")),
716 },
717 Some(PathBuf::from("/src/test/second.rs"))
718 ),
719 FoundPath::new(
720 ProjectPath {
721 worktree_id,
722 path: Arc::from(Path::new("test/third.rs")),
723 },
724 Some(PathBuf::from("/src/test/third.rs"))
725 ),
726 FoundPath::new(
727 ProjectPath {
728 worktree_id,
729 path: Arc::from(Path::new("test/first.rs")),
730 },
731 Some(PathBuf::from("/src/test/first.rs"))
732 ),
733 ],
734 "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
735 2nd item, as the last opened, 3rd item should go next as it was opened right before."
736 );
737}
738
739#[gpui::test]
740async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
741 let app_state = init_test(cx);
742
743 app_state
744 .fs
745 .as_fake()
746 .insert_tree(
747 "/src",
748 json!({
749 "test": {
750 "first.rs": "// First Rust file",
751 "second.rs": "// Second Rust file",
752 }
753 }),
754 )
755 .await;
756
757 app_state
758 .fs
759 .as_fake()
760 .insert_tree(
761 "/external-src",
762 json!({
763 "test": {
764 "third.rs": "// Third Rust file",
765 "fourth.rs": "// Fourth Rust file",
766 }
767 }),
768 )
769 .await;
770
771 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
772 cx.update(|cx| {
773 project.update(cx, |project, cx| {
774 project.find_or_create_local_worktree("/external-src", false, cx)
775 })
776 })
777 .detach();
778 cx.background_executor.run_until_parked();
779
780 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
781 let worktree_id = cx.read(|cx| {
782 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
783 assert_eq!(worktrees.len(), 1,);
784
785 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
786 });
787 workspace
788 .update(cx, |workspace, cx| {
789 workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
790 })
791 .detach();
792 cx.background_executor.run_until_parked();
793 let external_worktree_id = cx.read(|cx| {
794 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
795 assert_eq!(
796 worktrees.len(),
797 2,
798 "External file should get opened in a new worktree"
799 );
800
801 WorktreeId::from_usize(
802 worktrees
803 .into_iter()
804 .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
805 .expect("New worktree should have a different id")
806 .entity_id()
807 .as_u64() as usize,
808 )
809 });
810 cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
811
812 let initial_history_items =
813 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
814 assert_eq!(
815 initial_history_items,
816 vec![FoundPath::new(
817 ProjectPath {
818 worktree_id: external_worktree_id,
819 path: Arc::from(Path::new("")),
820 },
821 Some(PathBuf::from("/external-src/test/third.rs"))
822 )],
823 "Should show external file with its full path in the history after it was open"
824 );
825
826 let updated_history_items =
827 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
828 assert_eq!(
829 updated_history_items,
830 vec![
831 FoundPath::new(
832 ProjectPath {
833 worktree_id,
834 path: Arc::from(Path::new("test/second.rs")),
835 },
836 Some(PathBuf::from("/src/test/second.rs"))
837 ),
838 FoundPath::new(
839 ProjectPath {
840 worktree_id: external_worktree_id,
841 path: Arc::from(Path::new("")),
842 },
843 Some(PathBuf::from("/external-src/test/third.rs"))
844 ),
845 ],
846 "Should keep external file with history updates",
847 );
848}
849
850#[gpui::test]
851async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
852 let app_state = init_test(cx);
853
854 app_state
855 .fs
856 .as_fake()
857 .insert_tree(
858 "/src",
859 json!({
860 "test": {
861 "first.rs": "// First Rust file",
862 "second.rs": "// Second Rust file",
863 "third.rs": "// Third Rust file",
864 }
865 }),
866 )
867 .await;
868
869 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
870 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
871
872 // generate some history to select from
873 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
874 cx.executor().run_until_parked();
875 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
876 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
877 let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
878
879 for expected_selected_index in 0..current_history.len() {
880 cx.dispatch_action(Toggle);
881 let picker = active_file_picker(&workspace, cx);
882 let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
883 assert_eq!(
884 selected_index, expected_selected_index,
885 "Should select the next item in the history"
886 );
887 }
888
889 cx.dispatch_action(Toggle);
890 let selected_index = workspace.update(cx, |workspace, cx| {
891 workspace
892 .active_modal::<FileFinder>(cx)
893 .unwrap()
894 .read(cx)
895 .picker
896 .read(cx)
897 .delegate
898 .selected_index()
899 });
900 assert_eq!(
901 selected_index, 0,
902 "Should wrap around the history and start all over"
903 );
904}
905
906#[gpui::test]
907async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
908 let app_state = init_test(cx);
909
910 app_state
911 .fs
912 .as_fake()
913 .insert_tree(
914 "/src",
915 json!({
916 "test": {
917 "first.rs": "// First Rust file",
918 "second.rs": "// Second Rust file",
919 "third.rs": "// Third Rust file",
920 "fourth.rs": "// Fourth Rust file",
921 }
922 }),
923 )
924 .await;
925
926 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
927 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
928 let worktree_id = cx.read(|cx| {
929 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
930 assert_eq!(worktrees.len(), 1,);
931
932 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
933 });
934
935 // generate some history to select from
936 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
937 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
938 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
939 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
940
941 let finder = open_file_picker(&workspace, cx);
942 let first_query = "f";
943 finder
944 .update(cx, |finder, cx| {
945 finder.delegate.update_matches(first_query.to_string(), cx)
946 })
947 .await;
948 finder.update(cx, |finder, _| {
949 let delegate = &finder.delegate;
950 assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
951 let history_match = delegate.matches.history.first().unwrap();
952 assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
953 assert_eq!(history_match.0, FoundPath::new(
954 ProjectPath {
955 worktree_id,
956 path: Arc::from(Path::new("test/first.rs")),
957 },
958 Some(PathBuf::from("/src/test/first.rs"))
959 ));
960 assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
961 assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
962 });
963
964 let second_query = "fsdasdsa";
965 let finder = active_file_picker(&workspace, cx);
966 finder
967 .update(cx, |finder, cx| {
968 finder.delegate.update_matches(second_query.to_string(), cx)
969 })
970 .await;
971 finder.update(cx, |finder, _| {
972 let delegate = &finder.delegate;
973 assert!(
974 delegate.matches.history.is_empty(),
975 "No history entries should match {second_query}"
976 );
977 assert!(
978 delegate.matches.search.is_empty(),
979 "No search entries should match {second_query}"
980 );
981 });
982
983 let first_query_again = first_query;
984
985 let finder = active_file_picker(&workspace, cx);
986 finder
987 .update(cx, |finder, cx| {
988 finder
989 .delegate
990 .update_matches(first_query_again.to_string(), cx)
991 })
992 .await;
993 finder.update(cx, |finder, _| {
994 let delegate = &finder.delegate;
995 assert_eq!(delegate.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");
996 let history_match = delegate.matches.history.first().unwrap();
997 assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
998 assert_eq!(history_match.0, FoundPath::new(
999 ProjectPath {
1000 worktree_id,
1001 path: Arc::from(Path::new("test/first.rs")),
1002 },
1003 Some(PathBuf::from("/src/test/first.rs"))
1004 ));
1005 assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1006 assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
1007 });
1008}
1009
1010#[gpui::test]
1011async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1012 let app_state = init_test(cx);
1013
1014 app_state
1015 .fs
1016 .as_fake()
1017 .insert_tree(
1018 "/root",
1019 json!({
1020 "test": {
1021 "1_qw": "// First file that matches the query",
1022 "2_second": "// Second file",
1023 "3_third": "// Third file",
1024 "4_fourth": "// Fourth file",
1025 "5_qwqwqw": "// A file with 3 more matches than the first one",
1026 "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1027 "7_qwqwqw": "// One more, same amount of query matches as above",
1028 }
1029 }),
1030 )
1031 .await;
1032
1033 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1034 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1035 // generate some history to select from
1036 open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1037 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1038 open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1039 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1040 open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1041
1042 let finder = open_file_picker(&workspace, cx);
1043 let query = "qw";
1044 finder
1045 .update(cx, |finder, cx| {
1046 finder.delegate.update_matches(query.to_string(), cx)
1047 })
1048 .await;
1049 finder.update(cx, |finder, _| {
1050 let search_matches = collect_search_matches(finder);
1051 assert_eq!(
1052 search_matches.history,
1053 vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
1054 );
1055 assert_eq!(
1056 search_matches.search,
1057 vec![
1058 PathBuf::from("test/5_qwqwqw"),
1059 PathBuf::from("test/7_qwqwqw"),
1060 ],
1061 );
1062 });
1063}
1064
1065#[gpui::test]
1066async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1067 let app_state = init_test(cx);
1068
1069 app_state
1070 .fs
1071 .as_fake()
1072 .insert_tree(
1073 "/root",
1074 json!({
1075 "test": {
1076 "1_qw": "",
1077 }
1078 }),
1079 )
1080 .await;
1081
1082 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1083 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1084 // Open new buffer
1085 open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1086
1087 let picker = open_file_picker(&workspace, cx);
1088 picker.update(cx, |finder, _| {
1089 assert_match_selection(&finder, 0, "1_qw");
1090 });
1091}
1092
1093#[gpui::test]
1094async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1095 cx: &mut TestAppContext,
1096) {
1097 let app_state = init_test(cx);
1098
1099 app_state
1100 .fs
1101 .as_fake()
1102 .insert_tree(
1103 "/src",
1104 json!({
1105 "test": {
1106 "bar.rs": "// Bar file",
1107 "lib.rs": "// Lib file",
1108 "maaa.rs": "// Maaaaaaa",
1109 "main.rs": "// Main file",
1110 "moo.rs": "// Moooooo",
1111 }
1112 }),
1113 )
1114 .await;
1115
1116 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1117 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1118
1119 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1120 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1121 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1122
1123 // main.rs is on top, previously used is selected
1124 let picker = open_file_picker(&workspace, cx);
1125 picker.update(cx, |finder, _| {
1126 assert_eq!(finder.delegate.matches.len(), 3);
1127 assert_match_at_position(finder, 0, "main.rs");
1128 assert_match_selection(finder, 1, "lib.rs");
1129 assert_match_at_position(finder, 2, "bar.rs");
1130 });
1131
1132 // all files match, main.rs is still on top
1133 picker
1134 .update(cx, |finder, cx| {
1135 finder.delegate.update_matches(".rs".to_string(), cx)
1136 })
1137 .await;
1138 picker.update(cx, |finder, _| {
1139 assert_eq!(finder.delegate.matches.len(), 5);
1140 assert_match_at_position(finder, 0, "main.rs");
1141 assert_match_selection(finder, 1, "bar.rs");
1142 });
1143
1144 // main.rs is not among matches, select top item
1145 picker
1146 .update(cx, |finder, cx| {
1147 finder.delegate.update_matches("b".to_string(), cx)
1148 })
1149 .await;
1150 picker.update(cx, |finder, _| {
1151 assert_eq!(finder.delegate.matches.len(), 2);
1152 assert_match_at_position(finder, 0, "bar.rs");
1153 });
1154
1155 // main.rs is back, put it on top and select next item
1156 picker
1157 .update(cx, |finder, cx| {
1158 finder.delegate.update_matches("m".to_string(), cx)
1159 })
1160 .await;
1161 picker.update(cx, |finder, _| {
1162 assert_eq!(finder.delegate.matches.len(), 3);
1163 assert_match_at_position(finder, 0, "main.rs");
1164 assert_match_selection(finder, 1, "moo.rs");
1165 });
1166
1167 // get back to the initial state
1168 picker
1169 .update(cx, |finder, cx| {
1170 finder.delegate.update_matches("".to_string(), cx)
1171 })
1172 .await;
1173 picker.update(cx, |finder, _| {
1174 assert_eq!(finder.delegate.matches.len(), 3);
1175 assert_match_at_position(finder, 0, "main.rs");
1176 assert_match_selection(finder, 1, "lib.rs");
1177 });
1178}
1179
1180#[gpui::test]
1181async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1182 let app_state = init_test(cx);
1183
1184 app_state
1185 .fs
1186 .as_fake()
1187 .insert_tree(
1188 "/test",
1189 json!({
1190 "test": {
1191 "1.txt": "// One",
1192 "2.txt": "// Two",
1193 "3.txt": "// Three",
1194 }
1195 }),
1196 )
1197 .await;
1198
1199 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1200 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1201
1202 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1203 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1204 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1205
1206 let picker = open_file_picker(&workspace, cx);
1207 picker.update(cx, |finder, _| {
1208 assert_eq!(finder.delegate.matches.len(), 3);
1209 assert_match_at_position(finder, 0, "3.txt");
1210 assert_match_selection(finder, 1, "2.txt");
1211 assert_match_at_position(finder, 2, "1.txt");
1212 });
1213
1214 cx.dispatch_action(Confirm); // Open 2.txt
1215
1216 let picker = open_file_picker(&workspace, cx);
1217 picker.update(cx, |finder, _| {
1218 assert_eq!(finder.delegate.matches.len(), 3);
1219 assert_match_at_position(finder, 0, "2.txt");
1220 assert_match_selection(finder, 1, "3.txt");
1221 assert_match_at_position(finder, 2, "1.txt");
1222 });
1223
1224 cx.dispatch_action(SelectNext);
1225 cx.dispatch_action(Confirm); // Open 1.txt
1226
1227 let picker = open_file_picker(&workspace, cx);
1228 picker.update(cx, |finder, _| {
1229 assert_eq!(finder.delegate.matches.len(), 3);
1230 assert_match_at_position(finder, 0, "1.txt");
1231 assert_match_selection(finder, 1, "2.txt");
1232 assert_match_at_position(finder, 2, "3.txt");
1233 });
1234}
1235
1236#[gpui::test]
1237async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1238 let app_state = init_test(cx);
1239
1240 app_state
1241 .fs
1242 .as_fake()
1243 .insert_tree(
1244 "/src",
1245 json!({
1246 "collab_ui": {
1247 "first.rs": "// First Rust file",
1248 "second.rs": "// Second Rust file",
1249 "third.rs": "// Third Rust file",
1250 "collab_ui.rs": "// Fourth Rust file",
1251 }
1252 }),
1253 )
1254 .await;
1255
1256 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1257 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1258 // generate some history to select from
1259 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1260 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1261 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1262 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1263
1264 let finder = open_file_picker(&workspace, cx);
1265 let query = "collab_ui";
1266 cx.simulate_input(query);
1267 finder.update(cx, |finder, _| {
1268 let delegate = &finder.delegate;
1269 assert!(
1270 delegate.matches.history.is_empty(),
1271 "History items should not math query {query}, they should be matched by name only"
1272 );
1273
1274 let search_entries = delegate
1275 .matches
1276 .search
1277 .iter()
1278 .map(|path_match| path_match.0.path.to_path_buf())
1279 .collect::<Vec<_>>();
1280 assert_eq!(
1281 search_entries,
1282 vec![
1283 PathBuf::from("collab_ui/collab_ui.rs"),
1284 PathBuf::from("collab_ui/first.rs"),
1285 PathBuf::from("collab_ui/third.rs"),
1286 PathBuf::from("collab_ui/second.rs"),
1287 ],
1288 "Despite all search results having the same directory name, the most matching one should be on top"
1289 );
1290 });
1291}
1292
1293#[gpui::test]
1294async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1295 let app_state = init_test(cx);
1296
1297 app_state
1298 .fs
1299 .as_fake()
1300 .insert_tree(
1301 "/src",
1302 json!({
1303 "test": {
1304 "first.rs": "// First Rust file",
1305 "nonexistent.rs": "// Second Rust file",
1306 "third.rs": "// Third Rust file",
1307 }
1308 }),
1309 )
1310 .await;
1311
1312 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1313 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from
1314 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1315 open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1316 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1317 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1318
1319 let picker = open_file_picker(&workspace, cx);
1320 cx.simulate_input("rs");
1321
1322 picker.update(cx, |finder, _| {
1323 let history_entries = finder.delegate
1324 .matches
1325 .history
1326 .iter()
1327 .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").0.path.to_path_buf())
1328 .collect::<Vec<_>>();
1329 assert_eq!(
1330 history_entries,
1331 vec![
1332 PathBuf::from("test/first.rs"),
1333 PathBuf::from("test/third.rs"),
1334 ],
1335 "Should have all opened files in the history, except the ones that do not exist on disk"
1336 );
1337 });
1338}
1339
1340async fn open_close_queried_buffer(
1341 input: &str,
1342 expected_matches: usize,
1343 expected_editor_title: &str,
1344 workspace: &View<Workspace>,
1345 cx: &mut gpui::VisualTestContext,
1346) -> Vec<FoundPath> {
1347 let history_items = open_queried_buffer(
1348 input,
1349 expected_matches,
1350 expected_editor_title,
1351 workspace,
1352 cx,
1353 )
1354 .await;
1355
1356 cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
1357
1358 history_items
1359}
1360
1361async fn open_queried_buffer(
1362 input: &str,
1363 expected_matches: usize,
1364 expected_editor_title: &str,
1365 workspace: &View<Workspace>,
1366 cx: &mut gpui::VisualTestContext,
1367) -> Vec<FoundPath> {
1368 let picker = open_file_picker(&workspace, cx);
1369 cx.simulate_input(input);
1370
1371 let history_items = picker.update(cx, |finder, _| {
1372 assert_eq!(
1373 finder.delegate.matches.len(),
1374 expected_matches,
1375 "Unexpected number of matches found for query `{input}`, matches: {:?}",
1376 finder.delegate.matches
1377 );
1378 finder.delegate.history_items.clone()
1379 });
1380
1381 cx.dispatch_action(Confirm);
1382
1383 cx.read(|cx| {
1384 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1385 let active_editor_title = active_editor.read(cx).title(cx);
1386 assert_eq!(
1387 expected_editor_title, active_editor_title,
1388 "Unexpected editor title for query `{input}`"
1389 );
1390 });
1391
1392 history_items
1393}
1394
1395fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1396 cx.update(|cx| {
1397 let state = AppState::test(cx);
1398 theme::init(theme::LoadThemes::JustBase, cx);
1399 language::init(cx);
1400 super::init(cx);
1401 editor::init(cx);
1402 workspace::init_settings(cx);
1403 Project::init_settings(cx);
1404 state
1405 })
1406}
1407
1408fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1409 PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1410 Ok::<_, std::convert::Infallible>(FileSearchQuery {
1411 raw_query: test_str.to_owned(),
1412 file_query_end: if path_like_str == test_str {
1413 None
1414 } else {
1415 Some(path_like_str.len())
1416 },
1417 })
1418 })
1419 .unwrap()
1420}
1421
1422fn build_find_picker(
1423 project: Model<Project>,
1424 cx: &mut TestAppContext,
1425) -> (
1426 View<Picker<FileFinderDelegate>>,
1427 View<Workspace>,
1428 &mut VisualTestContext,
1429) {
1430 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1431 let picker = open_file_picker(&workspace, cx);
1432 (picker, workspace, cx)
1433}
1434
1435#[track_caller]
1436fn open_file_picker(
1437 workspace: &View<Workspace>,
1438 cx: &mut VisualTestContext,
1439) -> View<Picker<FileFinderDelegate>> {
1440 cx.dispatch_action(Toggle);
1441 active_file_picker(workspace, cx)
1442}
1443
1444#[track_caller]
1445fn active_file_picker(
1446 workspace: &View<Workspace>,
1447 cx: &mut VisualTestContext,
1448) -> View<Picker<FileFinderDelegate>> {
1449 workspace.update(cx, |workspace, cx| {
1450 workspace
1451 .active_modal::<FileFinder>(cx)
1452 .unwrap()
1453 .read(cx)
1454 .picker
1455 .clone()
1456 })
1457}
1458
1459#[derive(Debug)]
1460struct SearchEntries {
1461 history: Vec<PathBuf>,
1462 search: Vec<PathBuf>,
1463}
1464
1465impl SearchEntries {
1466 #[track_caller]
1467 fn search_only(self) -> Vec<PathBuf> {
1468 assert!(
1469 self.history.is_empty(),
1470 "Should have no history matches, but got: {:?}",
1471 self.history
1472 );
1473 self.search
1474 }
1475}
1476
1477fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
1478 let matches = &picker.delegate.matches;
1479 SearchEntries {
1480 history: matches
1481 .history
1482 .iter()
1483 .map(|(history_path, path_match)| {
1484 path_match
1485 .as_ref()
1486 .map(|path_match| {
1487 Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
1488 })
1489 .unwrap_or_else(|| {
1490 history_path
1491 .absolute
1492 .as_deref()
1493 .unwrap_or_else(|| &history_path.project.path)
1494 .to_path_buf()
1495 })
1496 })
1497 .collect(),
1498 search: matches
1499 .search
1500 .iter()
1501 .map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path))
1502 .collect(),
1503 }
1504}
1505
1506#[track_caller]
1507fn assert_match_selection(
1508 finder: &Picker<FileFinderDelegate>,
1509 expected_selection_index: usize,
1510 expected_file_name: &str,
1511) {
1512 assert_eq!(
1513 finder.delegate.selected_index(),
1514 expected_selection_index,
1515 "Match is not selected"
1516 );
1517 assert_match_at_position(finder, expected_selection_index, expected_file_name);
1518}
1519
1520#[track_caller]
1521fn assert_match_at_position(
1522 finder: &Picker<FileFinderDelegate>,
1523 match_index: usize,
1524 expected_file_name: &str,
1525) {
1526 let match_item = finder
1527 .delegate
1528 .matches
1529 .get(match_index)
1530 .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
1531 let match_file_name = match match_item {
1532 Match::History(found_path, _) => found_path.absolute.as_deref().unwrap().file_name(),
1533 Match::Search(path_match) => path_match.0.path.file_name(),
1534 }
1535 .unwrap()
1536 .to_string_lossy();
1537 assert_eq!(match_file_name, expected_file_name);
1538}