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