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