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