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::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 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
876 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
877 let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
878
879 for expected_selected_index in 0..current_history.len() {
880 cx.dispatch_action(Toggle);
881 let picker = active_file_picker(&workspace, cx);
882 let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
883 assert_eq!(
884 selected_index, expected_selected_index,
885 "Should select the next item in the history"
886 );
887 }
888
889 cx.dispatch_action(Toggle);
890 let selected_index = workspace.update(cx, |workspace, cx| {
891 workspace
892 .active_modal::<FileFinder>(cx)
893 .unwrap()
894 .read(cx)
895 .picker
896 .read(cx)
897 .delegate
898 .selected_index()
899 });
900 assert_eq!(
901 selected_index, 0,
902 "Should wrap around the history and start all over"
903 );
904}
905
906#[gpui::test]
907async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
908 let app_state = init_test(cx);
909
910 app_state
911 .fs
912 .as_fake()
913 .insert_tree(
914 "/src",
915 json!({
916 "test": {
917 "first.rs": "// First Rust file",
918 "second.rs": "// Second Rust file",
919 "third.rs": "// Third Rust file",
920 "fourth.rs": "// Fourth Rust file",
921 }
922 }),
923 )
924 .await;
925
926 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
927 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
928 let worktree_id = cx.read(|cx| {
929 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
930 assert_eq!(worktrees.len(), 1,);
931
932 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
933 });
934
935 // generate some history to select from
936 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
937 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
938 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
939 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
940
941 let finder = open_file_picker(&workspace, cx);
942 let first_query = "f";
943 finder
944 .update(cx, |finder, cx| {
945 finder.delegate.update_matches(first_query.to_string(), cx)
946 })
947 .await;
948 finder.update(cx, |finder, _| {
949 let delegate = &finder.delegate;
950 assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
951 let history_match = delegate.matches.history.first().unwrap();
952 assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
953 assert_eq!(history_match.0, FoundPath::new(
954 ProjectPath {
955 worktree_id,
956 path: Arc::from(Path::new("test/first.rs")),
957 },
958 Some(PathBuf::from("/src/test/first.rs"))
959 ));
960 assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
961 assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
962 });
963
964 let second_query = "fsdasdsa";
965 let finder = active_file_picker(&workspace, cx);
966 finder
967 .update(cx, |finder, cx| {
968 finder.delegate.update_matches(second_query.to_string(), cx)
969 })
970 .await;
971 finder.update(cx, |finder, _| {
972 let delegate = &finder.delegate;
973 assert!(
974 delegate.matches.history.is_empty(),
975 "No history entries should match {second_query}"
976 );
977 assert!(
978 delegate.matches.search.is_empty(),
979 "No search entries should match {second_query}"
980 );
981 });
982
983 let first_query_again = first_query;
984
985 let finder = active_file_picker(&workspace, cx);
986 finder
987 .update(cx, |finder, cx| {
988 finder
989 .delegate
990 .update_matches(first_query_again.to_string(), cx)
991 })
992 .await;
993 finder.update(cx, |finder, _| {
994 let delegate = &finder.delegate;
995 assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
996 let history_match = delegate.matches.history.first().unwrap();
997 assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
998 assert_eq!(history_match.0, FoundPath::new(
999 ProjectPath {
1000 worktree_id,
1001 path: Arc::from(Path::new("test/first.rs")),
1002 },
1003 Some(PathBuf::from("/src/test/first.rs"))
1004 ));
1005 assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1006 assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
1007 });
1008}
1009
1010#[gpui::test]
1011async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1012 let app_state = init_test(cx);
1013
1014 app_state
1015 .fs
1016 .as_fake()
1017 .insert_tree(
1018 "/root",
1019 json!({
1020 "test": {
1021 "1_qw": "// First file that matches the query",
1022 "2_second": "// Second file",
1023 "3_third": "// Third file",
1024 "4_fourth": "// Fourth file",
1025 "5_qwqwqw": "// A file with 3 more matches than the first one",
1026 "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1027 "7_qwqwqw": "// One more, same amount of query matches as above",
1028 }
1029 }),
1030 )
1031 .await;
1032
1033 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1034 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1035 // generate some history to select from
1036 open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1037 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1038 open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1039 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1040 open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1041
1042 let finder = open_file_picker(&workspace, cx);
1043 let query = "qw";
1044 finder
1045 .update(cx, |finder, cx| {
1046 finder.delegate.update_matches(query.to_string(), cx)
1047 })
1048 .await;
1049 finder.update(cx, |finder, _| {
1050 let search_matches = collect_search_matches(finder);
1051 assert_eq!(
1052 search_matches.history,
1053 vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
1054 );
1055 assert_eq!(
1056 search_matches.search,
1057 vec![
1058 PathBuf::from("test/5_qwqwqw"),
1059 PathBuf::from("test/7_qwqwqw"),
1060 ],
1061 );
1062 });
1063}
1064
1065#[gpui::test]
1066async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1067 let app_state = init_test(cx);
1068
1069 app_state
1070 .fs
1071 .as_fake()
1072 .insert_tree(
1073 "/root",
1074 json!({
1075 "test": {
1076 "1_qw": "",
1077 }
1078 }),
1079 )
1080 .await;
1081
1082 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1083 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1084 // Open new buffer
1085 open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1086
1087 let picker = open_file_picker(&workspace, cx);
1088 picker.update(cx, |finder, _| {
1089 assert_match_selection(&finder, 0, "1_qw");
1090 });
1091}
1092
1093#[gpui::test]
1094async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1095 cx: &mut TestAppContext,
1096) {
1097 let app_state = init_test(cx);
1098
1099 app_state
1100 .fs
1101 .as_fake()
1102 .insert_tree(
1103 "/src",
1104 json!({
1105 "test": {
1106 "bar.rs": "// Bar file",
1107 "lib.rs": "// Lib file",
1108 "maaa.rs": "// Maaaaaaa",
1109 "main.rs": "// Main file",
1110 "moo.rs": "// Moooooo",
1111 }
1112 }),
1113 )
1114 .await;
1115
1116 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1117 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1118
1119 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1120 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1121 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1122
1123 // main.rs is on top, previously used is selected
1124 let picker = open_file_picker(&workspace, cx);
1125 picker.update(cx, |finder, _| {
1126 assert_eq!(finder.delegate.matches.len(), 3);
1127 assert_match_selection(finder, 0, "main.rs");
1128 assert_match_at_position(finder, 1, "lib.rs");
1129 assert_match_at_position(finder, 2, "bar.rs");
1130 });
1131
1132 // all files match, main.rs is still on top, but the second item is selected
1133 picker
1134 .update(cx, |finder, cx| {
1135 finder.delegate.update_matches(".rs".to_string(), cx)
1136 })
1137 .await;
1138 picker.update(cx, |finder, _| {
1139 assert_eq!(finder.delegate.matches.len(), 5);
1140 assert_match_at_position(finder, 0, "main.rs");
1141 assert_match_selection(finder, 1, "bar.rs");
1142 });
1143
1144 // main.rs is not among matches, select top item
1145 picker
1146 .update(cx, |finder, cx| {
1147 finder.delegate.update_matches("b".to_string(), cx)
1148 })
1149 .await;
1150 picker.update(cx, |finder, _| {
1151 assert_eq!(finder.delegate.matches.len(), 2);
1152 assert_match_at_position(finder, 0, "bar.rs");
1153 });
1154
1155 // main.rs is back, put it on top and select next item
1156 picker
1157 .update(cx, |finder, cx| {
1158 finder.delegate.update_matches("m".to_string(), cx)
1159 })
1160 .await;
1161 picker.update(cx, |finder, _| {
1162 assert_eq!(finder.delegate.matches.len(), 3);
1163 assert_match_at_position(finder, 0, "main.rs");
1164 assert_match_selection(finder, 1, "moo.rs");
1165 });
1166
1167 // get back to the initial state
1168 picker
1169 .update(cx, |finder, cx| {
1170 finder.delegate.update_matches("".to_string(), cx)
1171 })
1172 .await;
1173 picker.update(cx, |finder, _| {
1174 assert_eq!(finder.delegate.matches.len(), 3);
1175 assert_match_selection(finder, 0, "main.rs");
1176 assert_match_at_position(finder, 1, "lib.rs");
1177 });
1178}
1179
1180#[gpui::test]
1181async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
1182 let app_state = init_test(cx);
1183
1184 app_state
1185 .fs
1186 .as_fake()
1187 .insert_tree(
1188 "/test",
1189 json!({
1190 "test": {
1191 "1.txt": "// One",
1192 "2.txt": "// Two",
1193 "3.txt": "// Three",
1194 }
1195 }),
1196 )
1197 .await;
1198
1199 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1200 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1201
1202 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1203 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1204 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1205
1206 let picker = open_file_picker(&workspace, cx);
1207 picker.update(cx, |finder, _| {
1208 assert_eq!(finder.delegate.matches.len(), 3);
1209 assert_match_selection(finder, 0, "3.txt");
1210 assert_match_at_position(finder, 1, "2.txt");
1211 assert_match_at_position(finder, 2, "1.txt");
1212 });
1213
1214 cx.dispatch_action(SelectNext);
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_selection(finder, 0, "2.txt");
1221 assert_match_at_position(finder, 1, "3.txt");
1222 assert_match_at_position(finder, 2, "1.txt");
1223 });
1224
1225 cx.dispatch_action(SelectNext);
1226 cx.dispatch_action(SelectNext);
1227 cx.dispatch_action(Confirm); // Open 1.txt
1228
1229 let picker = open_file_picker(&workspace, cx);
1230 picker.update(cx, |finder, _| {
1231 assert_eq!(finder.delegate.matches.len(), 3);
1232 assert_match_selection(finder, 0, "1.txt");
1233 assert_match_at_position(finder, 1, "2.txt");
1234 assert_match_at_position(finder, 2, "3.txt");
1235 });
1236}
1237
1238#[gpui::test]
1239async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1240 let app_state = init_test(cx);
1241
1242 app_state
1243 .fs
1244 .as_fake()
1245 .insert_tree(
1246 "/src",
1247 json!({
1248 "collab_ui": {
1249 "first.rs": "// First Rust file",
1250 "second.rs": "// Second Rust file",
1251 "third.rs": "// Third Rust file",
1252 "collab_ui.rs": "// Fourth Rust file",
1253 }
1254 }),
1255 )
1256 .await;
1257
1258 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1259 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1260 // generate some history to select from
1261 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1262 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1263 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1264 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1265
1266 let finder = open_file_picker(&workspace, cx);
1267 let query = "collab_ui";
1268 cx.simulate_input(query);
1269 finder.update(cx, |finder, _| {
1270 let delegate = &finder.delegate;
1271 assert!(
1272 delegate.matches.history.is_empty(),
1273 "History items should not math query {query}, they should be matched by name only"
1274 );
1275
1276 let search_entries = delegate
1277 .matches
1278 .search
1279 .iter()
1280 .map(|path_match| path_match.0.path.to_path_buf())
1281 .collect::<Vec<_>>();
1282 assert_eq!(
1283 search_entries,
1284 vec![
1285 PathBuf::from("collab_ui/collab_ui.rs"),
1286 PathBuf::from("collab_ui/first.rs"),
1287 PathBuf::from("collab_ui/third.rs"),
1288 PathBuf::from("collab_ui/second.rs"),
1289 ],
1290 "Despite all search results having the same directory name, the most matching one should be on top"
1291 );
1292 });
1293}
1294
1295#[gpui::test]
1296async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1297 let app_state = init_test(cx);
1298
1299 app_state
1300 .fs
1301 .as_fake()
1302 .insert_tree(
1303 "/src",
1304 json!({
1305 "test": {
1306 "first.rs": "// First Rust file",
1307 "nonexistent.rs": "// Second Rust file",
1308 "third.rs": "// Third Rust file",
1309 }
1310 }),
1311 )
1312 .await;
1313
1314 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1315 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from
1316 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1317 open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1318 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1319 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1320
1321 let picker = open_file_picker(&workspace, cx);
1322 cx.simulate_input("rs");
1323
1324 picker.update(cx, |finder, _| {
1325 let history_entries = finder.delegate
1326 .matches
1327 .history
1328 .iter()
1329 .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").0.path.to_path_buf())
1330 .collect::<Vec<_>>();
1331 assert_eq!(
1332 history_entries,
1333 vec![
1334 PathBuf::from("test/first.rs"),
1335 PathBuf::from("test/third.rs"),
1336 ],
1337 "Should have all opened files in the history, except the ones that do not exist on disk"
1338 );
1339 });
1340}
1341
1342#[gpui::test]
1343async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
1344 let app_state = init_test(cx);
1345
1346 app_state
1347 .fs
1348 .as_fake()
1349 .insert_tree(
1350 "/src",
1351 json!({
1352 "lib.rs": "// Lib file",
1353 "main.rs": "// Bar file",
1354 "read.me": "// Readme file",
1355 }),
1356 )
1357 .await;
1358
1359 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1360 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1361
1362 // Initial state
1363 let picker = open_file_picker(&workspace, cx);
1364 cx.simulate_input("rs");
1365 picker.update(cx, |finder, _| {
1366 assert_eq!(finder.delegate.matches.len(), 2);
1367 assert_match_at_position(finder, 0, "lib.rs");
1368 assert_match_at_position(finder, 1, "main.rs");
1369 });
1370
1371 // Delete main.rs
1372 app_state
1373 .fs
1374 .remove_file("/src/main.rs".as_ref(), Default::default())
1375 .await
1376 .expect("unable to remove file");
1377 cx.executor().advance_clock(FS_WATCH_LATENCY);
1378
1379 // main.rs is in not among search results anymore
1380 picker.update(cx, |finder, _| {
1381 assert_eq!(finder.delegate.matches.len(), 1);
1382 assert_match_at_position(finder, 0, "lib.rs");
1383 });
1384
1385 // Create util.rs
1386 app_state
1387 .fs
1388 .create_file("/src/util.rs".as_ref(), Default::default())
1389 .await
1390 .expect("unable to create file");
1391 cx.executor().advance_clock(FS_WATCH_LATENCY);
1392
1393 // util.rs is among search results
1394 picker.update(cx, |finder, _| {
1395 assert_eq!(finder.delegate.matches.len(), 2);
1396 assert_match_at_position(finder, 0, "lib.rs");
1397 assert_match_at_position(finder, 1, "util.rs");
1398 });
1399}
1400
1401#[gpui::test]
1402async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
1403 cx: &mut gpui::TestAppContext,
1404) {
1405 let app_state = init_test(cx);
1406
1407 app_state
1408 .fs
1409 .as_fake()
1410 .insert_tree(
1411 "/test",
1412 json!({
1413 "project_1": {
1414 "bar.rs": "// Bar file",
1415 "lib.rs": "// Lib file",
1416 },
1417 "project_2": {
1418 "Cargo.toml": "// Cargo file",
1419 "main.rs": "// Main file",
1420 }
1421 }),
1422 )
1423 .await;
1424
1425 let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
1426 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
1427 let worktree_1_id = project.update(cx, |project, cx| {
1428 let worktree = project.worktrees().last().expect("worktree not found");
1429 worktree.read(cx).id()
1430 });
1431
1432 // Initial state
1433 let picker = open_file_picker(&workspace, cx);
1434 cx.simulate_input("rs");
1435 picker.update(cx, |finder, _| {
1436 assert_eq!(finder.delegate.matches.len(), 2);
1437 assert_match_at_position(finder, 0, "bar.rs");
1438 assert_match_at_position(finder, 1, "lib.rs");
1439 });
1440
1441 // Add new worktree
1442 project
1443 .update(cx, |project, cx| {
1444 project
1445 .find_or_create_local_worktree("/test/project_2", true, cx)
1446 .into_future()
1447 })
1448 .await
1449 .expect("unable to create workdir");
1450 cx.executor().advance_clock(FS_WATCH_LATENCY);
1451
1452 // main.rs is among search results
1453 picker.update(cx, |finder, _| {
1454 assert_eq!(finder.delegate.matches.len(), 3);
1455 assert_match_at_position(finder, 0, "bar.rs");
1456 assert_match_at_position(finder, 1, "lib.rs");
1457 assert_match_at_position(finder, 2, "main.rs");
1458 });
1459
1460 // Remove the first worktree
1461 project.update(cx, |project, cx| {
1462 project.remove_worktree(worktree_1_id, cx);
1463 });
1464 cx.executor().advance_clock(FS_WATCH_LATENCY);
1465
1466 // Files from the first worktree are not in the search results anymore
1467 picker.update(cx, |finder, _| {
1468 assert_eq!(finder.delegate.matches.len(), 1);
1469 assert_match_at_position(finder, 0, "main.rs");
1470 });
1471}
1472
1473#[gpui::test]
1474async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1475 let app_state = init_test(cx);
1476
1477 app_state
1478 .fs
1479 .as_fake()
1480 .insert_tree(
1481 "/test",
1482 json!({
1483 "1.txt": "// One",
1484 }),
1485 )
1486 .await;
1487
1488 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1489 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1490
1491 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1492
1493 cx.simulate_modifiers_change(Modifiers::command());
1494 open_file_picker(&workspace, cx);
1495
1496 cx.simulate_modifiers_change(Modifiers::none());
1497 active_file_picker(&workspace, cx);
1498}
1499
1500#[gpui::test]
1501async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
1502 let app_state = init_test(cx);
1503
1504 app_state
1505 .fs
1506 .as_fake()
1507 .insert_tree(
1508 "/test",
1509 json!({
1510 "1.txt": "// One",
1511 "2.txt": "// Two",
1512 }),
1513 )
1514 .await;
1515
1516 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1517 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1518
1519 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1520 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1521
1522 cx.simulate_modifiers_change(Modifiers::command());
1523 let picker = open_file_picker(&workspace, cx);
1524 picker.update(cx, |finder, _| {
1525 assert_eq!(finder.delegate.matches.len(), 2);
1526 assert_match_selection(finder, 0, "2.txt");
1527 assert_match_at_position(finder, 1, "1.txt");
1528 });
1529
1530 cx.dispatch_action(SelectNext);
1531 cx.simulate_modifiers_change(Modifiers::none());
1532 cx.read(|cx| {
1533 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1534 assert_eq!(active_editor.read(cx).title(cx), "1.txt");
1535 });
1536}
1537
1538#[gpui::test]
1539async fn test_switches_between_release_norelease_modes_on_forward_nav(
1540 cx: &mut gpui::TestAppContext,
1541) {
1542 let app_state = init_test(cx);
1543
1544 app_state
1545 .fs
1546 .as_fake()
1547 .insert_tree(
1548 "/test",
1549 json!({
1550 "1.txt": "// One",
1551 "2.txt": "// Two",
1552 }),
1553 )
1554 .await;
1555
1556 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1557 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1558
1559 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1560 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1561
1562 // Open with a shortcut
1563 cx.simulate_modifiers_change(Modifiers::command());
1564 let picker = open_file_picker(&workspace, cx);
1565 picker.update(cx, |finder, _| {
1566 assert_eq!(finder.delegate.matches.len(), 2);
1567 assert_match_selection(finder, 0, "2.txt");
1568 assert_match_at_position(finder, 1, "1.txt");
1569 });
1570
1571 // Switch to navigating with other shortcuts
1572 // Don't open file on modifiers release
1573 cx.simulate_modifiers_change(Modifiers::control());
1574 cx.dispatch_action(SelectNext);
1575 cx.simulate_modifiers_change(Modifiers::none());
1576 picker.update(cx, |finder, _| {
1577 assert_eq!(finder.delegate.matches.len(), 2);
1578 assert_match_at_position(finder, 0, "2.txt");
1579 assert_match_selection(finder, 1, "1.txt");
1580 });
1581
1582 // Back to navigation with initial shortcut
1583 // Open file on modifiers release
1584 cx.simulate_modifiers_change(Modifiers::command());
1585 cx.dispatch_action(Toggle);
1586 cx.simulate_modifiers_change(Modifiers::none());
1587 cx.read(|cx| {
1588 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1589 assert_eq!(active_editor.read(cx).title(cx), "2.txt");
1590 });
1591}
1592
1593#[gpui::test]
1594async fn test_switches_between_release_norelease_modes_on_backward_nav(
1595 cx: &mut gpui::TestAppContext,
1596) {
1597 let app_state = init_test(cx);
1598
1599 app_state
1600 .fs
1601 .as_fake()
1602 .insert_tree(
1603 "/test",
1604 json!({
1605 "1.txt": "// One",
1606 "2.txt": "// Two",
1607 "3.txt": "// Three"
1608 }),
1609 )
1610 .await;
1611
1612 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1613 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1614
1615 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1616 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
1617 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
1618
1619 // Open with a shortcut
1620 cx.simulate_modifiers_change(Modifiers::command());
1621 let picker = open_file_picker(&workspace, cx);
1622 picker.update(cx, |finder, _| {
1623 assert_eq!(finder.delegate.matches.len(), 3);
1624 assert_match_selection(finder, 0, "3.txt");
1625 assert_match_at_position(finder, 1, "2.txt");
1626 assert_match_at_position(finder, 2, "1.txt");
1627 });
1628
1629 // Switch to navigating with other shortcuts
1630 // Don't open file on modifiers release
1631 cx.simulate_modifiers_change(Modifiers::control());
1632 cx.dispatch_action(menu::SelectPrev);
1633 cx.simulate_modifiers_change(Modifiers::none());
1634 picker.update(cx, |finder, _| {
1635 assert_eq!(finder.delegate.matches.len(), 3);
1636 assert_match_at_position(finder, 0, "3.txt");
1637 assert_match_at_position(finder, 1, "2.txt");
1638 assert_match_selection(finder, 2, "1.txt");
1639 });
1640
1641 // Back to navigation with initial shortcut
1642 // Open file on modifiers release
1643 cx.simulate_modifiers_change(Modifiers::command());
1644 cx.dispatch_action(SelectPrev); // <-- File Finder's SelectPrev, not menu's
1645 cx.simulate_modifiers_change(Modifiers::none());
1646 cx.read(|cx| {
1647 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1648 assert_eq!(active_editor.read(cx).title(cx), "3.txt");
1649 });
1650}
1651
1652#[gpui::test]
1653async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
1654 let app_state = init_test(cx);
1655
1656 app_state
1657 .fs
1658 .as_fake()
1659 .insert_tree(
1660 "/test",
1661 json!({
1662 "1.txt": "// One",
1663 }),
1664 )
1665 .await;
1666
1667 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
1668 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1669
1670 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
1671
1672 cx.simulate_modifiers_change(Modifiers::command());
1673 open_file_picker(&workspace, cx);
1674
1675 cx.simulate_modifiers_change(Modifiers::command_shift());
1676 active_file_picker(&workspace, cx);
1677}
1678
1679async fn open_close_queried_buffer(
1680 input: &str,
1681 expected_matches: usize,
1682 expected_editor_title: &str,
1683 workspace: &View<Workspace>,
1684 cx: &mut gpui::VisualTestContext,
1685) -> Vec<FoundPath> {
1686 let history_items = open_queried_buffer(
1687 input,
1688 expected_matches,
1689 expected_editor_title,
1690 workspace,
1691 cx,
1692 )
1693 .await;
1694
1695 cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
1696
1697 history_items
1698}
1699
1700async fn open_queried_buffer(
1701 input: &str,
1702 expected_matches: usize,
1703 expected_editor_title: &str,
1704 workspace: &View<Workspace>,
1705 cx: &mut gpui::VisualTestContext,
1706) -> Vec<FoundPath> {
1707 let picker = open_file_picker(&workspace, cx);
1708 cx.simulate_input(input);
1709
1710 let history_items = picker.update(cx, |finder, _| {
1711 assert_eq!(
1712 finder.delegate.matches.len(),
1713 expected_matches,
1714 "Unexpected number of matches found for query `{input}`, matches: {:?}",
1715 finder.delegate.matches
1716 );
1717 finder.delegate.history_items.clone()
1718 });
1719
1720 cx.dispatch_action(Confirm);
1721
1722 cx.read(|cx| {
1723 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1724 let active_editor_title = active_editor.read(cx).title(cx);
1725 assert_eq!(
1726 expected_editor_title, active_editor_title,
1727 "Unexpected editor title for query `{input}`"
1728 );
1729 });
1730
1731 history_items
1732}
1733
1734fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1735 cx.update(|cx| {
1736 let state = AppState::test(cx);
1737 theme::init(theme::LoadThemes::JustBase, cx);
1738 language::init(cx);
1739 super::init(cx);
1740 editor::init(cx);
1741 workspace::init_settings(cx);
1742 Project::init_settings(cx);
1743 state
1744 })
1745}
1746
1747fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1748 PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1749 Ok::<_, std::convert::Infallible>(FileSearchQuery {
1750 raw_query: test_str.to_owned(),
1751 file_query_end: if path_like_str == test_str {
1752 None
1753 } else {
1754 Some(path_like_str.len())
1755 },
1756 })
1757 })
1758 .unwrap()
1759}
1760
1761fn build_find_picker(
1762 project: Model<Project>,
1763 cx: &mut TestAppContext,
1764) -> (
1765 View<Picker<FileFinderDelegate>>,
1766 View<Workspace>,
1767 &mut VisualTestContext,
1768) {
1769 let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1770 let picker = open_file_picker(&workspace, cx);
1771 (picker, workspace, cx)
1772}
1773
1774#[track_caller]
1775fn open_file_picker(
1776 workspace: &View<Workspace>,
1777 cx: &mut VisualTestContext,
1778) -> View<Picker<FileFinderDelegate>> {
1779 cx.dispatch_action(Toggle);
1780 active_file_picker(workspace, cx)
1781}
1782
1783#[track_caller]
1784fn active_file_picker(
1785 workspace: &View<Workspace>,
1786 cx: &mut VisualTestContext,
1787) -> View<Picker<FileFinderDelegate>> {
1788 workspace.update(cx, |workspace, cx| {
1789 workspace
1790 .active_modal::<FileFinder>(cx)
1791 .expect("file finder is not open")
1792 .read(cx)
1793 .picker
1794 .clone()
1795 })
1796}
1797
1798#[derive(Debug)]
1799struct SearchEntries {
1800 history: Vec<PathBuf>,
1801 search: Vec<PathBuf>,
1802}
1803
1804impl SearchEntries {
1805 #[track_caller]
1806 fn search_only(self) -> Vec<PathBuf> {
1807 assert!(
1808 self.history.is_empty(),
1809 "Should have no history matches, but got: {:?}",
1810 self.history
1811 );
1812 self.search
1813 }
1814}
1815
1816fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
1817 let matches = &picker.delegate.matches;
1818 SearchEntries {
1819 history: matches
1820 .history
1821 .iter()
1822 .map(|(history_path, path_match)| {
1823 path_match
1824 .as_ref()
1825 .map(|path_match| {
1826 Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
1827 })
1828 .unwrap_or_else(|| {
1829 history_path
1830 .absolute
1831 .as_deref()
1832 .unwrap_or_else(|| &history_path.project.path)
1833 .to_path_buf()
1834 })
1835 })
1836 .collect(),
1837 search: matches
1838 .search
1839 .iter()
1840 .map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path))
1841 .collect(),
1842 }
1843}
1844
1845#[track_caller]
1846fn assert_match_selection(
1847 finder: &Picker<FileFinderDelegate>,
1848 expected_selection_index: usize,
1849 expected_file_name: &str,
1850) {
1851 assert_eq!(
1852 finder.delegate.selected_index(),
1853 expected_selection_index,
1854 "Match is not selected"
1855 );
1856 assert_match_at_position(finder, expected_selection_index, expected_file_name);
1857}
1858
1859#[track_caller]
1860fn assert_match_at_position(
1861 finder: &Picker<FileFinderDelegate>,
1862 match_index: usize,
1863 expected_file_name: &str,
1864) {
1865 let match_item = finder
1866 .delegate
1867 .matches
1868 .get(match_index)
1869 .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
1870 let match_file_name = match match_item {
1871 Match::History(found_path, _) => found_path.absolute.as_deref().unwrap().file_name(),
1872 Match::Search(path_match) => path_match.0.path.file_name(),
1873 }
1874 .unwrap()
1875 .to_string_lossy();
1876 assert_eq!(match_file_name, expected_file_name);
1877}