1use std::{future::IntoFuture, path::Path, time::Duration};
2
3use super::*;
4use editor::Editor;
5use gpui::{Entity, TestAppContext, VisualTestContext};
6use menu::{Confirm, SelectNext, SelectPrevious};
7use pretty_assertions::{assert_eq, assert_matches};
8use project::{FS_WATCH_LATENCY, RemoveOptions};
9use serde_json::json;
10use util::{path, rel_path::rel_path};
11use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths};
12
13#[ctor::ctor]
14fn init_logger() {
15 zlog::init_test();
16}
17
18#[test]
19fn test_path_elision() {
20 #[track_caller]
21 fn check(path: &str, budget: usize, matches: impl IntoIterator<Item = usize>, expected: &str) {
22 let mut path = path.to_owned();
23 let slice = PathComponentSlice::new(&path);
24 let matches = Vec::from_iter(matches);
25 if let Some(range) = slice.elision_range(budget - 1, &matches) {
26 path.replace_range(range, "…");
27 }
28 assert_eq!(path, expected);
29 }
30
31 // Simple cases, mostly to check that different path shapes are handled gracefully.
32 check("p/a/b/c/d/", 6, [], "p/…/d/");
33 check("p/a/b/c/d/", 1, [2, 4, 6], "p/a/b/c/d/");
34 check("p/a/b/c/d/", 10, [2, 6], "p/a/…/c/d/");
35 check("p/a/b/c/d/", 8, [6], "p/…/c/d/");
36
37 check("p/a/b/c/d", 5, [], "p/…/d");
38 check("p/a/b/c/d", 9, [2, 4, 6], "p/a/b/c/d");
39 check("p/a/b/c/d", 9, [2, 6], "p/a/…/c/d");
40 check("p/a/b/c/d", 7, [6], "p/…/c/d");
41
42 check("/p/a/b/c/d/", 7, [], "/p/…/d/");
43 check("/p/a/b/c/d/", 11, [3, 5, 7], "/p/a/b/c/d/");
44 check("/p/a/b/c/d/", 11, [3, 7], "/p/a/…/c/d/");
45 check("/p/a/b/c/d/", 9, [7], "/p/…/c/d/");
46
47 // If the budget can't be met, no elision is done.
48 check(
49 "project/dir/child/grandchild",
50 5,
51 [],
52 "project/dir/child/grandchild",
53 );
54
55 // The longest unmatched segment is picked for elision.
56 check(
57 "project/one/two/X/three/sub",
58 21,
59 [16],
60 "project/…/X/three/sub",
61 );
62
63 // Elision stops when the budget is met, even though there are more components in the chosen segment.
64 // It proceeds from the end of the unmatched segment that is closer to the midpoint of the path.
65 check(
66 "project/one/two/three/X/sub",
67 21,
68 [22],
69 "project/…/three/X/sub",
70 )
71}
72
73#[test]
74fn test_custom_project_search_ordering_in_file_finder() {
75 let mut file_finder_sorted_output = vec![
76 ProjectPanelOrdMatch(PathMatch {
77 score: 0.5,
78 positions: Vec::new(),
79 worktree_id: 0,
80 path: rel_path("b0.5").into(),
81 path_prefix: rel_path("").into(),
82 distance_to_relative_ancestor: 0,
83 is_dir: false,
84 }),
85 ProjectPanelOrdMatch(PathMatch {
86 score: 1.0,
87 positions: Vec::new(),
88 worktree_id: 0,
89 path: rel_path("c1.0").into(),
90 path_prefix: rel_path("").into(),
91 distance_to_relative_ancestor: 0,
92 is_dir: false,
93 }),
94 ProjectPanelOrdMatch(PathMatch {
95 score: 1.0,
96 positions: Vec::new(),
97 worktree_id: 0,
98 path: rel_path("a1.0").into(),
99 path_prefix: rel_path("").into(),
100 distance_to_relative_ancestor: 0,
101 is_dir: false,
102 }),
103 ProjectPanelOrdMatch(PathMatch {
104 score: 0.5,
105 positions: Vec::new(),
106 worktree_id: 0,
107 path: rel_path("a0.5").into(),
108 path_prefix: rel_path("").into(),
109 distance_to_relative_ancestor: 0,
110 is_dir: false,
111 }),
112 ProjectPanelOrdMatch(PathMatch {
113 score: 1.0,
114 positions: Vec::new(),
115 worktree_id: 0,
116 path: rel_path("b1.0").into(),
117 path_prefix: rel_path("").into(),
118 distance_to_relative_ancestor: 0,
119 is_dir: false,
120 }),
121 ];
122 file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
123
124 assert_eq!(
125 file_finder_sorted_output,
126 vec![
127 ProjectPanelOrdMatch(PathMatch {
128 score: 1.0,
129 positions: Vec::new(),
130 worktree_id: 0,
131 path: rel_path("a1.0").into(),
132 path_prefix: rel_path("").into(),
133 distance_to_relative_ancestor: 0,
134 is_dir: false,
135 }),
136 ProjectPanelOrdMatch(PathMatch {
137 score: 1.0,
138 positions: Vec::new(),
139 worktree_id: 0,
140 path: rel_path("b1.0").into(),
141 path_prefix: rel_path("").into(),
142 distance_to_relative_ancestor: 0,
143 is_dir: false,
144 }),
145 ProjectPanelOrdMatch(PathMatch {
146 score: 1.0,
147 positions: Vec::new(),
148 worktree_id: 0,
149 path: rel_path("c1.0").into(),
150 path_prefix: rel_path("").into(),
151 distance_to_relative_ancestor: 0,
152 is_dir: false,
153 }),
154 ProjectPanelOrdMatch(PathMatch {
155 score: 0.5,
156 positions: Vec::new(),
157 worktree_id: 0,
158 path: rel_path("a0.5").into(),
159 path_prefix: rel_path("").into(),
160 distance_to_relative_ancestor: 0,
161 is_dir: false,
162 }),
163 ProjectPanelOrdMatch(PathMatch {
164 score: 0.5,
165 positions: Vec::new(),
166 worktree_id: 0,
167 path: rel_path("b0.5").into(),
168 path_prefix: rel_path("").into(),
169 distance_to_relative_ancestor: 0,
170 is_dir: false,
171 }),
172 ]
173 );
174}
175
176#[gpui::test]
177async fn test_matching_paths(cx: &mut TestAppContext) {
178 let app_state = init_test(cx);
179 app_state
180 .fs
181 .as_fake()
182 .insert_tree(
183 path!("/root"),
184 json!({
185 "a": {
186 "banana": "",
187 "bandana": "",
188 }
189 }),
190 )
191 .await;
192
193 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
194
195 let (picker, workspace, cx) = build_find_picker(project, cx);
196
197 cx.simulate_input("bna");
198 picker.update(cx, |picker, _| {
199 assert_eq!(picker.delegate.matches.len(), 3);
200 });
201 cx.dispatch_action(SelectNext);
202 cx.dispatch_action(Confirm);
203 cx.read(|cx| {
204 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
205 assert_eq!(active_editor.read(cx).title(cx), "bandana");
206 });
207
208 for bandana_query in [
209 "bandana",
210 "./bandana",
211 ".\\bandana",
212 util::path!("a/bandana"),
213 "b/bandana",
214 "b\\bandana",
215 " bandana",
216 "bandana ",
217 " bandana ",
218 " ndan ",
219 " band ",
220 "a bandana",
221 "bandana:",
222 ] {
223 picker
224 .update_in(cx, |picker, window, cx| {
225 picker
226 .delegate
227 .update_matches(bandana_query.to_string(), window, cx)
228 })
229 .await;
230 picker.update(cx, |picker, _| {
231 assert_eq!(
232 picker.delegate.matches.len(),
233 // existence of CreateNew option depends on whether path already exists
234 if bandana_query == util::path!("a/bandana") {
235 1
236 } else {
237 2
238 },
239 "Wrong number of matches for bandana query '{bandana_query}'. Matches: {:?}",
240 picker.delegate.matches
241 );
242 });
243 cx.dispatch_action(SelectNext);
244 cx.dispatch_action(Confirm);
245 cx.read(|cx| {
246 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
247 assert_eq!(
248 active_editor.read(cx).title(cx),
249 "bandana",
250 "Wrong match for bandana query '{bandana_query}'"
251 );
252 });
253 }
254}
255
256#[gpui::test]
257async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
258 let app_state = init_test(cx);
259 app_state
260 .fs
261 .as_fake()
262 .insert_tree(
263 path!("/root"),
264 json!({
265 "a": {
266 "foo:bar.rs": "",
267 "foo.rs": "",
268 }
269 }),
270 )
271 .await;
272
273 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
274
275 let (picker, _, cx) = build_find_picker(project, cx);
276
277 // 'foo:' matches both files
278 cx.simulate_input("foo:");
279 picker.update(cx, |picker, _| {
280 assert_eq!(picker.delegate.matches.len(), 3);
281 assert_match_at_position(picker, 0, "foo.rs");
282 assert_match_at_position(picker, 1, "foo:bar.rs");
283 });
284
285 // 'foo:b' matches one of the files
286 cx.simulate_input("b");
287 picker.update(cx, |picker, _| {
288 assert_eq!(picker.delegate.matches.len(), 2);
289 assert_match_at_position(picker, 0, "foo:bar.rs");
290 });
291
292 cx.dispatch_action(editor::actions::Backspace);
293
294 // 'foo:1' matches both files, specifying which row to jump to
295 cx.simulate_input("1");
296 picker.update(cx, |picker, _| {
297 assert_eq!(picker.delegate.matches.len(), 3);
298 assert_match_at_position(picker, 0, "foo.rs");
299 assert_match_at_position(picker, 1, "foo:bar.rs");
300 });
301}
302
303#[gpui::test]
304async fn test_unicode_paths(cx: &mut TestAppContext) {
305 let app_state = init_test(cx);
306 app_state
307 .fs
308 .as_fake()
309 .insert_tree(
310 path!("/root"),
311 json!({
312 "a": {
313 "İg": " ",
314 }
315 }),
316 )
317 .await;
318
319 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
320
321 let (picker, workspace, cx) = build_find_picker(project, cx);
322
323 cx.simulate_input("g");
324 picker.update(cx, |picker, _| {
325 assert_eq!(picker.delegate.matches.len(), 2);
326 assert_match_at_position(picker, 1, "g");
327 });
328 cx.dispatch_action(Confirm);
329 cx.read(|cx| {
330 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
331 assert_eq!(active_editor.read(cx).title(cx), "İg");
332 });
333}
334
335#[gpui::test]
336async fn test_absolute_paths(cx: &mut TestAppContext) {
337 let app_state = init_test(cx);
338 app_state
339 .fs
340 .as_fake()
341 .insert_tree(
342 path!("/root"),
343 json!({
344 "a": {
345 "file1.txt": "",
346 "b": {
347 "file2.txt": "",
348 },
349 }
350 }),
351 )
352 .await;
353
354 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
355
356 let (picker, workspace, cx) = build_find_picker(project, cx);
357
358 let matching_abs_path = path!("/root/a/b/file2.txt").to_string();
359 picker
360 .update_in(cx, |picker, window, cx| {
361 picker
362 .delegate
363 .update_matches(matching_abs_path, window, cx)
364 })
365 .await;
366 picker.update(cx, |picker, _| {
367 assert_eq!(
368 collect_search_matches(picker).search_paths_only(),
369 vec![rel_path("a/b/file2.txt").into()],
370 "Matching abs path should be the only match"
371 )
372 });
373 cx.dispatch_action(SelectNext);
374 cx.dispatch_action(Confirm);
375 cx.read(|cx| {
376 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
377 assert_eq!(active_editor.read(cx).title(cx), "file2.txt");
378 });
379
380 let mismatching_abs_path = path!("/root/a/b/file1.txt").to_string();
381 picker
382 .update_in(cx, |picker, window, cx| {
383 picker
384 .delegate
385 .update_matches(mismatching_abs_path, window, cx)
386 })
387 .await;
388 picker.update(cx, |picker, _| {
389 assert_eq!(
390 collect_search_matches(picker).search_paths_only(),
391 Vec::new(),
392 "Mismatching abs path should produce no matches"
393 )
394 });
395}
396
397#[gpui::test]
398async fn test_complex_path(cx: &mut TestAppContext) {
399 let app_state = init_test(cx);
400 app_state
401 .fs
402 .as_fake()
403 .insert_tree(
404 path!("/root"),
405 json!({
406 "其他": {
407 "S数据表格": {
408 "task.xlsx": "some content",
409 },
410 }
411 }),
412 )
413 .await;
414
415 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
416
417 let (picker, workspace, cx) = build_find_picker(project, cx);
418
419 cx.simulate_input("t");
420 picker.update(cx, |picker, _| {
421 assert_eq!(picker.delegate.matches.len(), 2);
422 assert_eq!(
423 collect_search_matches(picker).search_paths_only(),
424 vec![rel_path("其他/S数据表格/task.xlsx").into()],
425 )
426 });
427 cx.dispatch_action(Confirm);
428 cx.read(|cx| {
429 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
430 assert_eq!(active_editor.read(cx).title(cx), "task.xlsx");
431 });
432}
433
434#[gpui::test]
435async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
436 let app_state = init_test(cx);
437
438 let first_file_name = "first.rs";
439 let first_file_contents = "// First Rust file";
440 app_state
441 .fs
442 .as_fake()
443 .insert_tree(
444 path!("/src"),
445 json!({
446 "test": {
447 first_file_name: first_file_contents,
448 "second.rs": "// Second Rust file",
449 }
450 }),
451 )
452 .await;
453
454 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
455
456 let (picker, workspace, cx) = build_find_picker(project, cx);
457
458 let file_query = &first_file_name[..3];
459 let file_row = 1;
460 let file_column = 3;
461 assert!(file_column <= first_file_contents.len());
462 let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
463 picker
464 .update_in(cx, |finder, window, cx| {
465 finder
466 .delegate
467 .update_matches(query_inside_file.to_string(), window, cx)
468 })
469 .await;
470 picker.update(cx, |finder, _| {
471 assert_match_at_position(finder, 1, &query_inside_file.to_string());
472 let finder = &finder.delegate;
473 assert_eq!(finder.matches.len(), 2);
474 let latest_search_query = finder
475 .latest_search_query
476 .as_ref()
477 .expect("Finder should have a query after the update_matches call");
478 assert_eq!(latest_search_query.raw_query, query_inside_file);
479 assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
480 assert_eq!(latest_search_query.path_position.row, Some(file_row));
481 assert_eq!(
482 latest_search_query.path_position.column,
483 Some(file_column as u32)
484 );
485 });
486
487 cx.dispatch_action(Confirm);
488
489 let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
490 cx.executor().advance_clock(Duration::from_secs(2));
491
492 editor.update(cx, |editor, cx| {
493 let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
494 assert_eq!(
495 all_selections.len(),
496 1,
497 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
498 );
499 let caret_selection = all_selections.into_iter().next().unwrap();
500 assert_eq!(caret_selection.start, caret_selection.end,
501 "Caret selection should have its start and end at the same position");
502 assert_eq!(file_row, caret_selection.start.row + 1,
503 "Query inside file should get caret with the same focus row");
504 assert_eq!(file_column, caret_selection.start.column as usize + 1,
505 "Query inside file should get caret with the same focus column");
506 });
507}
508
509#[gpui::test]
510async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
511 let app_state = init_test(cx);
512
513 let first_file_name = "first.rs";
514 let first_file_contents = "// First Rust file";
515 app_state
516 .fs
517 .as_fake()
518 .insert_tree(
519 path!("/src"),
520 json!({
521 "test": {
522 first_file_name: first_file_contents,
523 "second.rs": "// Second Rust file",
524 }
525 }),
526 )
527 .await;
528
529 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
530
531 let (picker, workspace, cx) = build_find_picker(project, cx);
532
533 let file_query = &first_file_name[..3];
534 let file_row = 200;
535 let file_column = 300;
536 assert!(file_column > first_file_contents.len());
537 let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
538 picker
539 .update_in(cx, |picker, window, cx| {
540 picker
541 .delegate
542 .update_matches(query_outside_file.to_string(), window, cx)
543 })
544 .await;
545 picker.update(cx, |finder, _| {
546 assert_match_at_position(finder, 1, &query_outside_file.to_string());
547 let delegate = &finder.delegate;
548 assert_eq!(delegate.matches.len(), 2);
549 let latest_search_query = delegate
550 .latest_search_query
551 .as_ref()
552 .expect("Finder should have a query after the update_matches call");
553 assert_eq!(latest_search_query.raw_query, query_outside_file);
554 assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
555 assert_eq!(latest_search_query.path_position.row, Some(file_row));
556 assert_eq!(
557 latest_search_query.path_position.column,
558 Some(file_column as u32)
559 );
560 });
561
562 cx.dispatch_action(Confirm);
563
564 let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
565 cx.executor().advance_clock(Duration::from_secs(2));
566
567 editor.update(cx, |editor, cx| {
568 let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
569 assert_eq!(
570 all_selections.len(),
571 1,
572 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
573 );
574 let caret_selection = all_selections.into_iter().next().unwrap();
575 assert_eq!(caret_selection.start, caret_selection.end,
576 "Caret selection should have its start and end at the same position");
577 assert_eq!(0, caret_selection.start.row,
578 "Excessive rows (as in query outside file borders) should get trimmed to last file row");
579 assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
580 "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
581 });
582}
583
584#[gpui::test]
585async fn test_matching_cancellation(cx: &mut TestAppContext) {
586 let app_state = init_test(cx);
587 app_state
588 .fs
589 .as_fake()
590 .insert_tree(
591 "/dir",
592 json!({
593 "hello": "",
594 "goodbye": "",
595 "halogen-light": "",
596 "happiness": "",
597 "height": "",
598 "hi": "",
599 "hiccup": "",
600 }),
601 )
602 .await;
603
604 let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
605
606 let (picker, _, cx) = build_find_picker(project, cx);
607
608 let query = test_path_position("hi");
609 picker
610 .update_in(cx, |picker, window, cx| {
611 picker.delegate.spawn_search(query.clone(), window, cx)
612 })
613 .await;
614
615 picker.update(cx, |picker, _cx| {
616 // CreateNew option not shown in this case since file already exists
617 assert_eq!(picker.delegate.matches.len(), 5);
618 });
619
620 picker.update_in(cx, |picker, window, cx| {
621 let matches = collect_search_matches(picker).search_matches_only();
622 let delegate = &mut picker.delegate;
623
624 // Simulate a search being cancelled after the time limit,
625 // returning only a subset of the matches that would have been found.
626 drop(delegate.spawn_search(query.clone(), window, cx));
627 delegate.set_search_matches(
628 delegate.latest_search_id,
629 true, // did-cancel
630 query.clone(),
631 vec![
632 ProjectPanelOrdMatch(matches[1].clone()),
633 ProjectPanelOrdMatch(matches[3].clone()),
634 ],
635 cx,
636 );
637
638 // Simulate another cancellation.
639 drop(delegate.spawn_search(query.clone(), window, cx));
640 delegate.set_search_matches(
641 delegate.latest_search_id,
642 true, // did-cancel
643 query.clone(),
644 vec![
645 ProjectPanelOrdMatch(matches[0].clone()),
646 ProjectPanelOrdMatch(matches[2].clone()),
647 ProjectPanelOrdMatch(matches[3].clone()),
648 ],
649 cx,
650 );
651
652 assert_eq!(
653 collect_search_matches(picker)
654 .search_matches_only()
655 .as_slice(),
656 &matches[0..4]
657 );
658 });
659}
660
661#[gpui::test]
662async fn test_ignored_root(cx: &mut TestAppContext) {
663 let app_state = init_test(cx);
664 app_state
665 .fs
666 .as_fake()
667 .insert_tree(
668 "/ancestor",
669 json!({
670 ".gitignore": "ignored-root",
671 "ignored-root": {
672 "happiness": "",
673 "height": "",
674 "hi": "",
675 "hiccup": "",
676 },
677 "tracked-root": {
678 ".gitignore": "height*",
679 "happiness": "",
680 "height": "",
681 "heights": {
682 "height_1": "",
683 "height_2": "",
684 },
685 "hi": "",
686 "hiccup": "",
687 },
688 }),
689 )
690 .await;
691
692 let project = Project::test(
693 app_state.fs.clone(),
694 [
695 Path::new(path!("/ancestor/tracked-root")),
696 Path::new(path!("/ancestor/ignored-root")),
697 ],
698 cx,
699 )
700 .await;
701 let (picker, workspace, cx) = build_find_picker(project, cx);
702
703 picker
704 .update_in(cx, |picker, window, cx| {
705 picker
706 .delegate
707 .spawn_search(test_path_position("hi"), window, cx)
708 })
709 .await;
710 picker.update(cx, |picker, _| {
711 let matches = collect_search_matches(picker);
712 assert_eq!(matches.history.len(), 0);
713 assert_eq!(
714 matches.search,
715 vec![
716 rel_path("ignored-root/hi").into(),
717 rel_path("tracked-root/hi").into(),
718 rel_path("ignored-root/hiccup").into(),
719 rel_path("tracked-root/hiccup").into(),
720 rel_path("ignored-root/height").into(),
721 rel_path("ignored-root/happiness").into(),
722 rel_path("tracked-root/happiness").into(),
723 ],
724 "All ignored files that were indexed are found for default ignored mode"
725 );
726 });
727 cx.dispatch_action(ToggleIncludeIgnored);
728 picker
729 .update_in(cx, |picker, window, cx| {
730 picker
731 .delegate
732 .spawn_search(test_path_position("hi"), window, cx)
733 })
734 .await;
735 picker.update(cx, |picker, _| {
736 let matches = collect_search_matches(picker);
737 assert_eq!(matches.history.len(), 0);
738 assert_eq!(
739 matches.search,
740 vec![
741 rel_path("ignored-root/hi").into(),
742 rel_path("tracked-root/hi").into(),
743 rel_path("ignored-root/hiccup").into(),
744 rel_path("tracked-root/hiccup").into(),
745 rel_path("ignored-root/height").into(),
746 rel_path("tracked-root/height").into(),
747 rel_path("ignored-root/happiness").into(),
748 rel_path("tracked-root/happiness").into(),
749 ],
750 "All ignored files should be found, for the toggled on ignored mode"
751 );
752 });
753
754 picker
755 .update_in(cx, |picker, window, cx| {
756 picker.delegate.include_ignored = Some(false);
757 picker
758 .delegate
759 .spawn_search(test_path_position("hi"), window, cx)
760 })
761 .await;
762 picker.update(cx, |picker, _| {
763 let matches = collect_search_matches(picker);
764 assert_eq!(matches.history.len(), 0);
765 assert_eq!(
766 matches.search,
767 vec![
768 rel_path("tracked-root/hi").into(),
769 rel_path("tracked-root/hiccup").into(),
770 rel_path("tracked-root/happiness").into(),
771 ],
772 "Only non-ignored files should be found for the turned off ignored mode"
773 );
774 });
775
776 workspace
777 .update_in(cx, |workspace, window, cx| {
778 workspace.open_abs_path(
779 PathBuf::from(path!("/ancestor/tracked-root/heights/height_1")),
780 OpenOptions {
781 visible: Some(OpenVisible::None),
782 ..OpenOptions::default()
783 },
784 window,
785 cx,
786 )
787 })
788 .await
789 .unwrap();
790 cx.run_until_parked();
791 workspace
792 .update_in(cx, |workspace, window, cx| {
793 workspace.active_pane().update(cx, |pane, cx| {
794 pane.close_active_item(&CloseActiveItem::default(), window, cx)
795 })
796 })
797 .await
798 .unwrap();
799 cx.run_until_parked();
800
801 picker
802 .update_in(cx, |picker, window, cx| {
803 picker.delegate.include_ignored = None;
804 picker
805 .delegate
806 .spawn_search(test_path_position("hi"), window, cx)
807 })
808 .await;
809 picker.update(cx, |picker, _| {
810 let matches = collect_search_matches(picker);
811 assert_eq!(matches.history.len(), 0);
812 assert_eq!(
813 matches.search,
814 vec![
815 rel_path("ignored-root/hi").into(),
816 rel_path("tracked-root/hi").into(),
817 rel_path("ignored-root/hiccup").into(),
818 rel_path("tracked-root/hiccup").into(),
819 rel_path("ignored-root/height").into(),
820 rel_path("ignored-root/happiness").into(),
821 rel_path("tracked-root/happiness").into(),
822 ],
823 "Only for the worktree with the ignored root, all indexed ignored files are found in the auto ignored mode"
824 );
825 });
826
827 picker
828 .update_in(cx, |picker, window, cx| {
829 picker.delegate.include_ignored = Some(true);
830 picker
831 .delegate
832 .spawn_search(test_path_position("hi"), window, cx)
833 })
834 .await;
835 picker.update(cx, |picker, _| {
836 let matches = collect_search_matches(picker);
837 assert_eq!(matches.history.len(), 0);
838 assert_eq!(
839 matches.search,
840 vec![
841 rel_path("ignored-root/hi").into(),
842 rel_path("tracked-root/hi").into(),
843 rel_path("ignored-root/hiccup").into(),
844 rel_path("tracked-root/hiccup").into(),
845 rel_path("ignored-root/height").into(),
846 rel_path("tracked-root/height").into(),
847 rel_path("tracked-root/heights/height_1").into(),
848 rel_path("tracked-root/heights/height_2").into(),
849 rel_path("ignored-root/happiness").into(),
850 rel_path("tracked-root/happiness").into(),
851 ],
852 "All ignored files that were indexed are found in the turned on ignored mode"
853 );
854 });
855
856 picker
857 .update_in(cx, |picker, window, cx| {
858 picker.delegate.include_ignored = Some(false);
859 picker
860 .delegate
861 .spawn_search(test_path_position("hi"), window, cx)
862 })
863 .await;
864 picker.update(cx, |picker, _| {
865 let matches = collect_search_matches(picker);
866 assert_eq!(matches.history.len(), 0);
867 assert_eq!(
868 matches.search,
869 vec![
870 rel_path("tracked-root/hi").into(),
871 rel_path("tracked-root/hiccup").into(),
872 rel_path("tracked-root/happiness").into(),
873 ],
874 "Only non-ignored files should be found for the turned off ignored mode"
875 );
876 });
877}
878
879#[gpui::test]
880async fn test_single_file_worktrees(cx: &mut TestAppContext) {
881 let app_state = init_test(cx);
882 app_state
883 .fs
884 .as_fake()
885 .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
886 .await;
887
888 let project = Project::test(
889 app_state.fs.clone(),
890 ["/root/the-parent-dir/the-file".as_ref()],
891 cx,
892 )
893 .await;
894
895 let (picker, _, cx) = build_find_picker(project, cx);
896
897 // Even though there is only one worktree, that worktree's filename
898 // is included in the matching, because the worktree is a single file.
899 picker
900 .update_in(cx, |picker, window, cx| {
901 picker
902 .delegate
903 .spawn_search(test_path_position("thf"), window, cx)
904 })
905 .await;
906 cx.read(|cx| {
907 let picker = picker.read(cx);
908 let delegate = &picker.delegate;
909 let matches = collect_search_matches(picker).search_matches_only();
910 assert_eq!(matches.len(), 1);
911
912 let (file_name, file_name_positions, full_path, full_path_positions) =
913 delegate.labels_for_path_match(&matches[0], PathStyle::local());
914 assert_eq!(file_name, "the-file");
915 assert_eq!(file_name_positions, &[0, 1, 4]);
916 assert_eq!(full_path, "");
917 assert_eq!(full_path_positions, &[0; 0]);
918 });
919
920 // Since the worktree root is a file, searching for its name followed by a slash does
921 // not match anything.
922 picker
923 .update_in(cx, |picker, window, cx| {
924 picker
925 .delegate
926 .spawn_search(test_path_position("thf/"), window, cx)
927 })
928 .await;
929 picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
930}
931
932#[gpui::test]
933async fn test_history_items_uniqueness_for_multiple_worktree(cx: &mut TestAppContext) {
934 let app_state = init_test(cx);
935 app_state
936 .fs
937 .as_fake()
938 .insert_tree(
939 path!("/repo1"),
940 json!({
941 "package.json": r#"{"name": "repo1"}"#,
942 "src": {
943 "index.js": "// Repo 1 index",
944 }
945 }),
946 )
947 .await;
948
949 app_state
950 .fs
951 .as_fake()
952 .insert_tree(
953 path!("/repo2"),
954 json!({
955 "package.json": r#"{"name": "repo2"}"#,
956 "src": {
957 "index.js": "// Repo 2 index",
958 }
959 }),
960 )
961 .await;
962
963 let project = Project::test(
964 app_state.fs.clone(),
965 [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
966 cx,
967 )
968 .await;
969
970 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
971 let (worktree_id1, worktree_id2) = cx.read(|cx| {
972 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
973 (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
974 });
975
976 workspace
977 .update_in(cx, |workspace, window, cx| {
978 workspace.open_path(
979 ProjectPath {
980 worktree_id: worktree_id1,
981 path: rel_path("package.json").into(),
982 },
983 None,
984 true,
985 window,
986 cx,
987 )
988 })
989 .await
990 .unwrap();
991
992 cx.dispatch_action(workspace::CloseActiveItem {
993 save_intent: None,
994 close_pinned: false,
995 });
996
997 let picker = open_file_picker(&workspace, cx);
998 cx.simulate_input("package.json");
999
1000 picker.update(cx, |finder, _| {
1001 let matches = &finder.delegate.matches.matches;
1002
1003 assert_eq!(
1004 matches.len(),
1005 2,
1006 "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
1007 matches.len(),
1008 matches
1009 );
1010
1011 assert_matches!(matches[0], Match::History { .. });
1012
1013 let search_matches = collect_search_matches(finder);
1014 assert_eq!(
1015 search_matches.history.len(),
1016 1,
1017 "Should have exactly 1 history match"
1018 );
1019 assert_eq!(
1020 search_matches.search.len(),
1021 1,
1022 "Should have exactly 1 search match (the other package.json)"
1023 );
1024
1025 if let Match::History { path, .. } = &matches[0] {
1026 assert_eq!(path.project.worktree_id, worktree_id1);
1027 assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
1028 }
1029
1030 if let Match::Search(path_match) = &matches[1] {
1031 assert_eq!(
1032 WorktreeId::from_usize(path_match.0.worktree_id),
1033 worktree_id2
1034 );
1035 assert_eq!(path_match.0.path.as_ref(), rel_path("package.json"));
1036 }
1037 });
1038}
1039
1040#[gpui::test]
1041async fn test_create_file_for_multiple_worktrees(cx: &mut TestAppContext) {
1042 let app_state = init_test(cx);
1043 app_state
1044 .fs
1045 .as_fake()
1046 .insert_tree(
1047 path!("/roota"),
1048 json!({ "the-parent-dira": { "filea": "" } }),
1049 )
1050 .await;
1051
1052 app_state
1053 .fs
1054 .as_fake()
1055 .insert_tree(
1056 path!("/rootb"),
1057 json!({ "the-parent-dirb": { "fileb": "" } }),
1058 )
1059 .await;
1060
1061 let project = Project::test(
1062 app_state.fs.clone(),
1063 [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1064 cx,
1065 )
1066 .await;
1067
1068 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1069 let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1070 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1071 (
1072 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize),
1073 WorktreeId::from_usize(worktrees[1].entity_id().as_u64() as usize),
1074 )
1075 });
1076
1077 let b_path = ProjectPath {
1078 worktree_id: worktree_id2,
1079 path: rel_path("the-parent-dirb/fileb").into(),
1080 };
1081 workspace
1082 .update_in(cx, |workspace, window, cx| {
1083 workspace.open_path(b_path, None, true, window, cx)
1084 })
1085 .await
1086 .unwrap();
1087
1088 let finder = open_file_picker(&workspace, cx);
1089
1090 finder
1091 .update_in(cx, |f, window, cx| {
1092 f.delegate.spawn_search(
1093 test_path_position(path!("the-parent-dirb/filec")),
1094 window,
1095 cx,
1096 )
1097 })
1098 .await;
1099 cx.run_until_parked();
1100 finder.update_in(cx, |picker, window, cx| {
1101 assert_eq!(picker.delegate.matches.len(), 1);
1102 picker.delegate.confirm(false, window, cx)
1103 });
1104 cx.run_until_parked();
1105 cx.read(|cx| {
1106 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1107 let project_path = active_editor.read(cx).project_path(cx);
1108 assert_eq!(
1109 project_path,
1110 Some(ProjectPath {
1111 worktree_id: worktree_id2,
1112 path: rel_path("the-parent-dirb/filec").into()
1113 })
1114 );
1115 });
1116}
1117
1118#[gpui::test]
1119async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppContext) {
1120 let app_state = init_test(cx);
1121 app_state
1122 .fs
1123 .as_fake()
1124 .insert_tree(
1125 path!("/roota"),
1126 json!({ "the-parent-dira": { "filea": "" } }),
1127 )
1128 .await;
1129
1130 app_state
1131 .fs
1132 .as_fake()
1133 .insert_tree(
1134 path!("/rootb"),
1135 json!({ "the-parent-dirb": { "fileb": "" } }),
1136 )
1137 .await;
1138
1139 let project = Project::test(
1140 app_state.fs.clone(),
1141 [path!("/roota").as_ref(), path!("/rootb").as_ref()],
1142 cx,
1143 )
1144 .await;
1145
1146 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1147 let (_worktree_id1, worktree_id2) = cx.read(|cx| {
1148 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1149 (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
1150 });
1151
1152 let finder = open_file_picker(&workspace, cx);
1153
1154 finder
1155 .update_in(cx, |f, window, cx| {
1156 f.delegate
1157 .spawn_search(test_path_position(path!("rootb/filec")), window, cx)
1158 })
1159 .await;
1160 cx.run_until_parked();
1161 finder.update_in(cx, |picker, window, cx| {
1162 assert_eq!(picker.delegate.matches.len(), 1);
1163 picker.delegate.confirm(false, window, cx)
1164 });
1165 cx.run_until_parked();
1166 cx.read(|cx| {
1167 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
1168 let project_path = active_editor.read(cx).project_path(cx);
1169 assert_eq!(
1170 project_path,
1171 Some(ProjectPath {
1172 worktree_id: worktree_id2,
1173 path: rel_path("filec").into()
1174 })
1175 );
1176 });
1177}
1178
1179#[gpui::test]
1180async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1181 let app_state = init_test(cx);
1182 app_state
1183 .fs
1184 .as_fake()
1185 .insert_tree(
1186 path!("/root"),
1187 json!({
1188 "dir1": { "a.txt": "" },
1189 "dir2": {
1190 "a.txt": "",
1191 "b.txt": ""
1192 }
1193 }),
1194 )
1195 .await;
1196
1197 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1198 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1199
1200 let worktree_id = cx.read(|cx| {
1201 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1202 assert_eq!(worktrees.len(), 1);
1203 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1204 });
1205
1206 // When workspace has an active item, sort items which are closer to that item
1207 // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1208 // so that one should be sorted earlier
1209 let b_path = ProjectPath {
1210 worktree_id,
1211 path: rel_path("dir2/b.txt").into(),
1212 };
1213 workspace
1214 .update_in(cx, |workspace, window, cx| {
1215 workspace.open_path(b_path, None, true, window, cx)
1216 })
1217 .await
1218 .unwrap();
1219 let finder = open_file_picker(&workspace, cx);
1220 finder
1221 .update_in(cx, |f, window, cx| {
1222 f.delegate
1223 .spawn_search(test_path_position("a.txt"), window, cx)
1224 })
1225 .await;
1226
1227 finder.update(cx, |picker, _| {
1228 let matches = collect_search_matches(picker).search_paths_only();
1229 assert_eq!(matches[0].as_ref(), rel_path("dir2/a.txt"));
1230 assert_eq!(matches[1].as_ref(), rel_path("dir1/a.txt"));
1231 });
1232}
1233
1234#[gpui::test]
1235async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1236 let app_state = init_test(cx);
1237 app_state
1238 .fs
1239 .as_fake()
1240 .insert_tree(
1241 "/root",
1242 json!({
1243 "dir1": {},
1244 "dir2": {
1245 "dir3": {}
1246 }
1247 }),
1248 )
1249 .await;
1250
1251 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1252 let (picker, _workspace, cx) = build_find_picker(project, cx);
1253
1254 picker
1255 .update_in(cx, |f, window, cx| {
1256 f.delegate
1257 .spawn_search(test_path_position("dir"), window, cx)
1258 })
1259 .await;
1260 cx.read(|cx| {
1261 let finder = picker.read(cx);
1262 assert_eq!(finder.delegate.matches.len(), 1);
1263 assert_match_at_position(finder, 0, "dir");
1264 });
1265}
1266
1267#[gpui::test]
1268async fn test_query_history(cx: &mut gpui::TestAppContext) {
1269 let app_state = init_test(cx);
1270
1271 app_state
1272 .fs
1273 .as_fake()
1274 .insert_tree(
1275 path!("/src"),
1276 json!({
1277 "test": {
1278 "first.rs": "// First Rust file",
1279 "second.rs": "// Second Rust file",
1280 "third.rs": "// Third Rust file",
1281 }
1282 }),
1283 )
1284 .await;
1285
1286 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1287 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1288 let worktree_id = cx.read(|cx| {
1289 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1290 assert_eq!(worktrees.len(), 1);
1291 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1292 });
1293
1294 // Open and close panels, getting their history items afterwards.
1295 // Ensure history items get populated with opened items, and items are kept in a certain order.
1296 // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1297 //
1298 // TODO: without closing, the opened items do not propagate their history changes for some reason
1299 // it does work in real app though, only tests do not propagate.
1300 workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1301
1302 let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1303 assert!(
1304 initial_history.is_empty(),
1305 "Should have no history before opening any files"
1306 );
1307
1308 let history_after_first =
1309 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1310 assert_eq!(
1311 history_after_first,
1312 vec![FoundPath::new(
1313 ProjectPath {
1314 worktree_id,
1315 path: rel_path("test/first.rs").into(),
1316 },
1317 PathBuf::from(path!("/src/test/first.rs"))
1318 )],
1319 "Should show 1st opened item in the history when opening the 2nd item"
1320 );
1321
1322 let history_after_second =
1323 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1324 assert_eq!(
1325 history_after_second,
1326 vec![
1327 FoundPath::new(
1328 ProjectPath {
1329 worktree_id,
1330 path: rel_path("test/second.rs").into(),
1331 },
1332 PathBuf::from(path!("/src/test/second.rs"))
1333 ),
1334 FoundPath::new(
1335 ProjectPath {
1336 worktree_id,
1337 path: rel_path("test/first.rs").into(),
1338 },
1339 PathBuf::from(path!("/src/test/first.rs"))
1340 ),
1341 ],
1342 "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1343 2nd item should be the first in the history, as the last opened."
1344 );
1345
1346 let history_after_third =
1347 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1348 assert_eq!(
1349 history_after_third,
1350 vec![
1351 FoundPath::new(
1352 ProjectPath {
1353 worktree_id,
1354 path: rel_path("test/third.rs").into(),
1355 },
1356 PathBuf::from(path!("/src/test/third.rs"))
1357 ),
1358 FoundPath::new(
1359 ProjectPath {
1360 worktree_id,
1361 path: rel_path("test/second.rs").into(),
1362 },
1363 PathBuf::from(path!("/src/test/second.rs"))
1364 ),
1365 FoundPath::new(
1366 ProjectPath {
1367 worktree_id,
1368 path: rel_path("test/first.rs").into(),
1369 },
1370 PathBuf::from(path!("/src/test/first.rs"))
1371 ),
1372 ],
1373 "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1374 3rd item should be the first in the history, as the last opened."
1375 );
1376
1377 let history_after_second_again =
1378 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1379 assert_eq!(
1380 history_after_second_again,
1381 vec![
1382 FoundPath::new(
1383 ProjectPath {
1384 worktree_id,
1385 path: rel_path("test/second.rs").into(),
1386 },
1387 PathBuf::from(path!("/src/test/second.rs"))
1388 ),
1389 FoundPath::new(
1390 ProjectPath {
1391 worktree_id,
1392 path: rel_path("test/third.rs").into(),
1393 },
1394 PathBuf::from(path!("/src/test/third.rs"))
1395 ),
1396 FoundPath::new(
1397 ProjectPath {
1398 worktree_id,
1399 path: rel_path("test/first.rs").into(),
1400 },
1401 PathBuf::from(path!("/src/test/first.rs"))
1402 ),
1403 ],
1404 "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1405 2nd item, as the last opened, 3rd item should go next as it was opened right before."
1406 );
1407}
1408
1409#[gpui::test]
1410async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
1411 let app_state = init_test(cx);
1412
1413 app_state
1414 .fs
1415 .as_fake()
1416 .insert_tree(
1417 path!("/src"),
1418 json!({
1419 "test": {
1420 "first.rs": "// First Rust file",
1421 "second.rs": "// Second Rust file",
1422 "third.rs": "// Third Rust file",
1423 }
1424 }),
1425 )
1426 .await;
1427
1428 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1429 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1430
1431 workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
1432
1433 open_close_queried_buffer("efir", 1, "first.rs", &workspace, cx).await;
1434 let history = open_close_queried_buffer("second", 1, "second.rs", &workspace, cx).await;
1435 assert_eq!(history.len(), 1);
1436
1437 let picker = open_file_picker(&workspace, cx);
1438 cx.simulate_input("fir");
1439 picker.update_in(cx, |finder, window, cx| {
1440 let matches = &finder.delegate.matches.matches;
1441 assert_matches!(
1442 matches.as_slice(),
1443 [Match::History { .. }, Match::CreateNew { .. }]
1444 );
1445 assert_eq!(
1446 matches[0].panel_match().unwrap().0.path.as_ref(),
1447 rel_path("test/first.rs")
1448 );
1449 assert_eq!(matches[0].panel_match().unwrap().0.positions, &[5, 6, 7]);
1450
1451 let (file_label, path_label) =
1452 finder
1453 .delegate
1454 .labels_for_match(&finder.delegate.matches.matches[0], window, cx);
1455 assert_eq!(file_label.text(), "first.rs");
1456 assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
1457 assert_eq!(
1458 path_label.text(),
1459 format!("test{}", PathStyle::local().separator())
1460 );
1461 assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
1462 });
1463}
1464
1465#[gpui::test]
1466async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1467 let app_state = init_test(cx);
1468
1469 app_state
1470 .fs
1471 .as_fake()
1472 .insert_tree(
1473 path!("/src"),
1474 json!({
1475 "test": {
1476 "first.rs": "// First Rust file",
1477 "second.rs": "// Second Rust file",
1478 }
1479 }),
1480 )
1481 .await;
1482
1483 app_state
1484 .fs
1485 .as_fake()
1486 .insert_tree(
1487 path!("/external-src"),
1488 json!({
1489 "test": {
1490 "third.rs": "// Third Rust file",
1491 "fourth.rs": "// Fourth Rust file",
1492 }
1493 }),
1494 )
1495 .await;
1496
1497 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1498 cx.update(|cx| {
1499 project.update(cx, |project, cx| {
1500 project.find_or_create_worktree(path!("/external-src"), false, cx)
1501 })
1502 })
1503 .detach();
1504 cx.background_executor.run_until_parked();
1505
1506 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1507 let worktree_id = cx.read(|cx| {
1508 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1509 assert_eq!(worktrees.len(), 1,);
1510
1511 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1512 });
1513 workspace
1514 .update_in(cx, |workspace, window, cx| {
1515 workspace.open_abs_path(
1516 PathBuf::from(path!("/external-src/test/third.rs")),
1517 OpenOptions {
1518 visible: Some(OpenVisible::None),
1519 ..Default::default()
1520 },
1521 window,
1522 cx,
1523 )
1524 })
1525 .detach();
1526 cx.background_executor.run_until_parked();
1527 let external_worktree_id = cx.read(|cx| {
1528 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1529 assert_eq!(
1530 worktrees.len(),
1531 2,
1532 "External file should get opened in a new worktree"
1533 );
1534
1535 WorktreeId::from_usize(
1536 worktrees
1537 .into_iter()
1538 .find(|worktree| worktree.entity_id().as_u64() as usize != worktree_id.to_usize())
1539 .expect("New worktree should have a different id")
1540 .entity_id()
1541 .as_u64() as usize,
1542 )
1543 });
1544 cx.dispatch_action(workspace::CloseActiveItem {
1545 save_intent: None,
1546 close_pinned: false,
1547 });
1548
1549 let initial_history_items =
1550 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1551 assert_eq!(
1552 initial_history_items,
1553 vec![FoundPath::new(
1554 ProjectPath {
1555 worktree_id: external_worktree_id,
1556 path: rel_path("").into(),
1557 },
1558 PathBuf::from(path!("/external-src/test/third.rs"))
1559 )],
1560 "Should show external file with its full path in the history after it was open"
1561 );
1562
1563 let updated_history_items =
1564 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1565 assert_eq!(
1566 updated_history_items,
1567 vec![
1568 FoundPath::new(
1569 ProjectPath {
1570 worktree_id,
1571 path: rel_path("test/second.rs").into(),
1572 },
1573 PathBuf::from(path!("/src/test/second.rs"))
1574 ),
1575 FoundPath::new(
1576 ProjectPath {
1577 worktree_id: external_worktree_id,
1578 path: rel_path("").into(),
1579 },
1580 PathBuf::from(path!("/external-src/test/third.rs"))
1581 ),
1582 ],
1583 "Should keep external file with history updates",
1584 );
1585}
1586
1587#[gpui::test]
1588async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1589 let app_state = init_test(cx);
1590
1591 app_state
1592 .fs
1593 .as_fake()
1594 .insert_tree(
1595 path!("/src"),
1596 json!({
1597 "test": {
1598 "first.rs": "// First Rust file",
1599 "second.rs": "// Second Rust file",
1600 "third.rs": "// Third Rust file",
1601 }
1602 }),
1603 )
1604 .await;
1605
1606 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1607 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1608
1609 // generate some history to select from
1610 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1611 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1612 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1613 let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1614
1615 for expected_selected_index in 0..current_history.len() {
1616 cx.dispatch_action(ToggleFileFinder::default());
1617 let picker = active_file_picker(&workspace, cx);
1618 let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index());
1619 assert_eq!(
1620 selected_index, expected_selected_index,
1621 "Should select the next item in the history"
1622 );
1623 }
1624
1625 cx.dispatch_action(ToggleFileFinder::default());
1626 let selected_index = workspace.update(cx, |workspace, cx| {
1627 workspace
1628 .active_modal::<FileFinder>(cx)
1629 .unwrap()
1630 .read(cx)
1631 .picker
1632 .read(cx)
1633 .delegate
1634 .selected_index()
1635 });
1636 assert_eq!(
1637 selected_index, 0,
1638 "Should wrap around the history and start all over"
1639 );
1640}
1641
1642#[gpui::test]
1643async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1644 let app_state = init_test(cx);
1645
1646 app_state
1647 .fs
1648 .as_fake()
1649 .insert_tree(
1650 path!("/src"),
1651 json!({
1652 "test": {
1653 "first.rs": "// First Rust file",
1654 "second.rs": "// Second Rust file",
1655 "third.rs": "// Third Rust file",
1656 "fourth.rs": "// Fourth Rust file",
1657 }
1658 }),
1659 )
1660 .await;
1661
1662 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1663 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1664 let worktree_id = cx.read(|cx| {
1665 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1666 assert_eq!(worktrees.len(), 1,);
1667
1668 WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize)
1669 });
1670
1671 // generate some history to select from
1672 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1673 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1674 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1675 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1676
1677 let finder = open_file_picker(&workspace, cx);
1678 let first_query = "f";
1679 finder
1680 .update_in(cx, |finder, window, cx| {
1681 finder
1682 .delegate
1683 .update_matches(first_query.to_string(), window, cx)
1684 })
1685 .await;
1686 finder.update(cx, |picker, _| {
1687 let matches = collect_search_matches(picker);
1688 assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1689 let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1690 assert_eq!(history_match, &FoundPath::new(
1691 ProjectPath {
1692 worktree_id,
1693 path: rel_path("test/first.rs").into(),
1694 },
1695 PathBuf::from(path!("/src/test/first.rs")),
1696 ));
1697 assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1698 assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1699 });
1700
1701 let second_query = "fsdasdsa";
1702 let finder = active_file_picker(&workspace, cx);
1703 finder
1704 .update_in(cx, |finder, window, cx| {
1705 finder
1706 .delegate
1707 .update_matches(second_query.to_string(), window, cx)
1708 })
1709 .await;
1710 finder.update(cx, |picker, _| {
1711 assert!(
1712 collect_search_matches(picker)
1713 .search_paths_only()
1714 .is_empty(),
1715 "No search entries should match {second_query}"
1716 );
1717 });
1718
1719 let first_query_again = first_query;
1720
1721 let finder = active_file_picker(&workspace, cx);
1722 finder
1723 .update_in(cx, |finder, window, cx| {
1724 finder
1725 .delegate
1726 .update_matches(first_query_again.to_string(), window, cx)
1727 })
1728 .await;
1729 finder.update(cx, |picker, _| {
1730 let matches = collect_search_matches(picker);
1731 assert_eq!(matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
1732 let history_match = matches.history_found_paths.first().expect("Should have path matches for history items after querying");
1733 assert_eq!(history_match, &FoundPath::new(
1734 ProjectPath {
1735 worktree_id,
1736 path: rel_path("test/first.rs").into(),
1737 },
1738 PathBuf::from(path!("/src/test/first.rs"))
1739 ));
1740 assert_eq!(matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1741 assert_eq!(matches.search.first().unwrap().as_ref(), rel_path("test/fourth.rs"));
1742 });
1743}
1744
1745#[gpui::test]
1746async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
1747 let app_state = init_test(cx);
1748
1749 app_state
1750 .fs
1751 .as_fake()
1752 .insert_tree(
1753 path!("/root"),
1754 json!({
1755 "test": {
1756 "1_qw": "// First file that matches the query",
1757 "2_second": "// Second file",
1758 "3_third": "// Third file",
1759 "4_fourth": "// Fourth file",
1760 "5_qwqwqw": "// A file with 3 more matches than the first one",
1761 "6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
1762 "7_qwqwqw": "// One more, same amount of query matches as above",
1763 }
1764 }),
1765 )
1766 .await;
1767
1768 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1769 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1770 // generate some history to select from
1771 open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1772 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1773 open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
1774 open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
1775 open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
1776
1777 let finder = open_file_picker(&workspace, cx);
1778 let query = "qw";
1779 finder
1780 .update_in(cx, |finder, window, cx| {
1781 finder
1782 .delegate
1783 .update_matches(query.to_string(), window, cx)
1784 })
1785 .await;
1786 finder.update(cx, |finder, _| {
1787 let search_matches = collect_search_matches(finder);
1788 assert_eq!(
1789 search_matches.history,
1790 vec![
1791 rel_path("test/1_qw").into(),
1792 rel_path("test/6_qwqwqw").into()
1793 ],
1794 );
1795 assert_eq!(
1796 search_matches.search,
1797 vec![
1798 rel_path("test/5_qwqwqw").into(),
1799 rel_path("test/7_qwqwqw").into()
1800 ],
1801 );
1802 });
1803}
1804
1805#[gpui::test]
1806async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
1807 let app_state = init_test(cx);
1808
1809 app_state
1810 .fs
1811 .as_fake()
1812 .insert_tree(
1813 path!("/root"),
1814 json!({
1815 "test": {
1816 "1_qw": "",
1817 }
1818 }),
1819 )
1820 .await;
1821
1822 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
1823 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1824 // Open new buffer
1825 open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
1826
1827 let picker = open_file_picker(&workspace, cx);
1828 picker.update(cx, |finder, _| {
1829 assert_match_selection(finder, 0, "1_qw");
1830 });
1831}
1832
1833#[gpui::test]
1834async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
1835 cx: &mut TestAppContext,
1836) {
1837 let app_state = init_test(cx);
1838
1839 app_state
1840 .fs
1841 .as_fake()
1842 .insert_tree(
1843 path!("/src"),
1844 json!({
1845 "test": {
1846 "bar.rs": "// Bar file",
1847 "lib.rs": "// Lib file",
1848 "maaa.rs": "// Maaaaaaa",
1849 "main.rs": "// Main file",
1850 "moo.rs": "// Moooooo",
1851 }
1852 }),
1853 )
1854 .await;
1855
1856 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1857 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1858
1859 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1860 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1861 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1862
1863 // main.rs is on top, previously used is selected
1864 let picker = open_file_picker(&workspace, cx);
1865 picker.update(cx, |finder, _| {
1866 assert_eq!(finder.delegate.matches.len(), 3);
1867 assert_match_selection(finder, 0, "main.rs");
1868 assert_match_at_position(finder, 1, "lib.rs");
1869 assert_match_at_position(finder, 2, "bar.rs");
1870 });
1871
1872 // all files match, main.rs is still on top, but the second item is selected
1873 picker
1874 .update_in(cx, |finder, window, cx| {
1875 finder
1876 .delegate
1877 .update_matches(".rs".to_string(), window, cx)
1878 })
1879 .await;
1880 picker.update(cx, |finder, _| {
1881 assert_eq!(finder.delegate.matches.len(), 6);
1882 assert_match_at_position(finder, 0, "main.rs");
1883 assert_match_selection(finder, 1, "bar.rs");
1884 assert_match_at_position(finder, 2, "lib.rs");
1885 assert_match_at_position(finder, 3, "moo.rs");
1886 assert_match_at_position(finder, 4, "maaa.rs");
1887 assert_match_at_position(finder, 5, ".rs");
1888 });
1889
1890 // main.rs is not among matches, select top item
1891 picker
1892 .update_in(cx, |finder, window, cx| {
1893 finder.delegate.update_matches("b".to_string(), window, cx)
1894 })
1895 .await;
1896 picker.update(cx, |finder, _| {
1897 assert_eq!(finder.delegate.matches.len(), 3);
1898 assert_match_at_position(finder, 0, "bar.rs");
1899 assert_match_at_position(finder, 1, "lib.rs");
1900 assert_match_at_position(finder, 2, "b");
1901 });
1902
1903 // main.rs is back, put it on top and select next item
1904 picker
1905 .update_in(cx, |finder, window, cx| {
1906 finder.delegate.update_matches("m".to_string(), window, cx)
1907 })
1908 .await;
1909 picker.update(cx, |finder, _| {
1910 assert_eq!(finder.delegate.matches.len(), 4);
1911 assert_match_at_position(finder, 0, "main.rs");
1912 assert_match_selection(finder, 1, "moo.rs");
1913 assert_match_at_position(finder, 2, "maaa.rs");
1914 assert_match_at_position(finder, 3, "m");
1915 });
1916
1917 // get back to the initial state
1918 picker
1919 .update_in(cx, |finder, window, cx| {
1920 finder.delegate.update_matches("".to_string(), window, cx)
1921 })
1922 .await;
1923 picker.update(cx, |finder, _| {
1924 assert_eq!(finder.delegate.matches.len(), 3);
1925 assert_match_selection(finder, 0, "main.rs");
1926 assert_match_at_position(finder, 1, "lib.rs");
1927 assert_match_at_position(finder, 2, "bar.rs");
1928 });
1929}
1930
1931#[gpui::test]
1932async fn test_setting_auto_select_first_and_select_active_file(cx: &mut TestAppContext) {
1933 let app_state = init_test(cx);
1934
1935 cx.update(|cx| {
1936 let settings = *FileFinderSettings::get_global(cx);
1937
1938 FileFinderSettings::override_global(
1939 FileFinderSettings {
1940 skip_focus_for_active_in_search: false,
1941 ..settings
1942 },
1943 cx,
1944 );
1945 });
1946
1947 app_state
1948 .fs
1949 .as_fake()
1950 .insert_tree(
1951 path!("/src"),
1952 json!({
1953 "test": {
1954 "bar.rs": "// Bar file",
1955 "lib.rs": "// Lib file",
1956 "maaa.rs": "// Maaaaaaa",
1957 "main.rs": "// Main file",
1958 "moo.rs": "// Moooooo",
1959 }
1960 }),
1961 )
1962 .await;
1963
1964 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
1965 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
1966
1967 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
1968 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
1969 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
1970
1971 // main.rs is on top, previously used is selected
1972 let picker = open_file_picker(&workspace, cx);
1973 picker.update(cx, |finder, _| {
1974 assert_eq!(finder.delegate.matches.len(), 3);
1975 assert_match_selection(finder, 0, "main.rs");
1976 assert_match_at_position(finder, 1, "lib.rs");
1977 assert_match_at_position(finder, 2, "bar.rs");
1978 });
1979
1980 // all files match, main.rs is on top, and is selected
1981 picker
1982 .update_in(cx, |finder, window, cx| {
1983 finder
1984 .delegate
1985 .update_matches(".rs".to_string(), window, cx)
1986 })
1987 .await;
1988 picker.update(cx, |finder, _| {
1989 assert_eq!(finder.delegate.matches.len(), 6);
1990 assert_match_selection(finder, 0, "main.rs");
1991 assert_match_at_position(finder, 1, "bar.rs");
1992 assert_match_at_position(finder, 2, "lib.rs");
1993 assert_match_at_position(finder, 3, "moo.rs");
1994 assert_match_at_position(finder, 4, "maaa.rs");
1995 assert_match_at_position(finder, 5, ".rs");
1996 });
1997}
1998
1999#[gpui::test]
2000async fn test_non_separate_history_items(cx: &mut TestAppContext) {
2001 let app_state = init_test(cx);
2002
2003 app_state
2004 .fs
2005 .as_fake()
2006 .insert_tree(
2007 path!("/src"),
2008 json!({
2009 "test": {
2010 "bar.rs": "// Bar file",
2011 "lib.rs": "// Lib file",
2012 "maaa.rs": "// Maaaaaaa",
2013 "main.rs": "// Main file",
2014 "moo.rs": "// Moooooo",
2015 }
2016 }),
2017 )
2018 .await;
2019
2020 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2021 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2022
2023 open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
2024 open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2025 open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
2026
2027 cx.dispatch_action(ToggleFileFinder::default());
2028 let picker = active_file_picker(&workspace, cx);
2029 // main.rs is on top, previously used is selected
2030 picker.update(cx, |finder, _| {
2031 assert_eq!(finder.delegate.matches.len(), 3);
2032 assert_match_selection(finder, 0, "main.rs");
2033 assert_match_at_position(finder, 1, "lib.rs");
2034 assert_match_at_position(finder, 2, "bar.rs");
2035 });
2036
2037 // all files match, main.rs is still on top, but the second item is selected
2038 picker
2039 .update_in(cx, |finder, window, cx| {
2040 finder
2041 .delegate
2042 .update_matches(".rs".to_string(), window, cx)
2043 })
2044 .await;
2045 picker.update(cx, |finder, _| {
2046 assert_eq!(finder.delegate.matches.len(), 6);
2047 assert_match_at_position(finder, 0, "main.rs");
2048 assert_match_selection(finder, 1, "moo.rs");
2049 assert_match_at_position(finder, 2, "bar.rs");
2050 assert_match_at_position(finder, 3, "lib.rs");
2051 assert_match_at_position(finder, 4, "maaa.rs");
2052 assert_match_at_position(finder, 5, ".rs");
2053 });
2054
2055 // main.rs is not among matches, select top item
2056 picker
2057 .update_in(cx, |finder, window, cx| {
2058 finder.delegate.update_matches("b".to_string(), window, cx)
2059 })
2060 .await;
2061 picker.update(cx, |finder, _| {
2062 assert_eq!(finder.delegate.matches.len(), 3);
2063 assert_match_at_position(finder, 0, "bar.rs");
2064 assert_match_at_position(finder, 1, "lib.rs");
2065 assert_match_at_position(finder, 2, "b");
2066 });
2067
2068 // main.rs is back, put it on top and select next item
2069 picker
2070 .update_in(cx, |finder, window, cx| {
2071 finder.delegate.update_matches("m".to_string(), window, cx)
2072 })
2073 .await;
2074 picker.update(cx, |finder, _| {
2075 assert_eq!(finder.delegate.matches.len(), 4);
2076 assert_match_at_position(finder, 0, "main.rs");
2077 assert_match_selection(finder, 1, "moo.rs");
2078 assert_match_at_position(finder, 2, "maaa.rs");
2079 assert_match_at_position(finder, 3, "m");
2080 });
2081
2082 // get back to the initial state
2083 picker
2084 .update_in(cx, |finder, window, cx| {
2085 finder.delegate.update_matches("".to_string(), window, cx)
2086 })
2087 .await;
2088 picker.update(cx, |finder, _| {
2089 assert_eq!(finder.delegate.matches.len(), 3);
2090 assert_match_selection(finder, 0, "main.rs");
2091 assert_match_at_position(finder, 1, "lib.rs");
2092 assert_match_at_position(finder, 2, "bar.rs");
2093 });
2094}
2095
2096#[gpui::test]
2097async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
2098 let app_state = init_test(cx);
2099
2100 app_state
2101 .fs
2102 .as_fake()
2103 .insert_tree(
2104 path!("/test"),
2105 json!({
2106 "test": {
2107 "1.txt": "// One",
2108 "2.txt": "// Two",
2109 "3.txt": "// Three",
2110 }
2111 }),
2112 )
2113 .await;
2114
2115 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2116 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2117
2118 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2119 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2120 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2121
2122 let picker = open_file_picker(&workspace, cx);
2123 picker.update(cx, |finder, _| {
2124 assert_eq!(finder.delegate.matches.len(), 3);
2125 assert_match_selection(finder, 0, "3.txt");
2126 assert_match_at_position(finder, 1, "2.txt");
2127 assert_match_at_position(finder, 2, "1.txt");
2128 });
2129
2130 cx.dispatch_action(SelectNext);
2131 cx.dispatch_action(Confirm); // Open 2.txt
2132
2133 let picker = open_file_picker(&workspace, cx);
2134 picker.update(cx, |finder, _| {
2135 assert_eq!(finder.delegate.matches.len(), 3);
2136 assert_match_selection(finder, 0, "2.txt");
2137 assert_match_at_position(finder, 1, "3.txt");
2138 assert_match_at_position(finder, 2, "1.txt");
2139 });
2140
2141 cx.dispatch_action(SelectNext);
2142 cx.dispatch_action(SelectNext);
2143 cx.dispatch_action(Confirm); // Open 1.txt
2144
2145 let picker = open_file_picker(&workspace, cx);
2146 picker.update(cx, |finder, _| {
2147 assert_eq!(finder.delegate.matches.len(), 3);
2148 assert_match_selection(finder, 0, "1.txt");
2149 assert_match_at_position(finder, 1, "2.txt");
2150 assert_match_at_position(finder, 2, "3.txt");
2151 });
2152}
2153
2154#[gpui::test]
2155async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut TestAppContext) {
2156 let app_state = init_test(cx);
2157
2158 app_state
2159 .fs
2160 .as_fake()
2161 .insert_tree(
2162 path!("/test"),
2163 json!({
2164 "test": {
2165 "1.txt": "// One",
2166 "2.txt": "// Two",
2167 "3.txt": "// Three",
2168 }
2169 }),
2170 )
2171 .await;
2172
2173 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2174 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2175
2176 open_close_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2177 open_close_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2178 open_close_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2179
2180 let picker = open_file_picker(&workspace, cx);
2181 picker.update(cx, |finder, _| {
2182 assert_eq!(finder.delegate.matches.len(), 3);
2183 assert_match_selection(finder, 0, "3.txt");
2184 assert_match_at_position(finder, 1, "2.txt");
2185 assert_match_at_position(finder, 2, "1.txt");
2186 });
2187
2188 cx.dispatch_action(SelectNext);
2189
2190 // Add more files to the worktree to trigger update matches
2191 for i in 0..5 {
2192 let filename = if cfg!(windows) {
2193 format!("C:/test/{}.txt", 4 + i)
2194 } else {
2195 format!("/test/{}.txt", 4 + i)
2196 };
2197 app_state
2198 .fs
2199 .create_file(Path::new(&filename), Default::default())
2200 .await
2201 .expect("unable to create file");
2202 }
2203
2204 cx.executor().advance_clock(FS_WATCH_LATENCY);
2205
2206 picker.update(cx, |finder, _| {
2207 assert_eq!(finder.delegate.matches.len(), 3);
2208 assert_match_at_position(finder, 0, "3.txt");
2209 assert_match_selection(finder, 1, "2.txt");
2210 assert_match_at_position(finder, 2, "1.txt");
2211 });
2212}
2213
2214#[gpui::test]
2215async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
2216 let app_state = init_test(cx);
2217
2218 app_state
2219 .fs
2220 .as_fake()
2221 .insert_tree(
2222 path!("/src"),
2223 json!({
2224 "collab_ui": {
2225 "first.rs": "// First Rust file",
2226 "second.rs": "// Second Rust file",
2227 "third.rs": "// Third Rust file",
2228 "collab_ui.rs": "// Fourth Rust file",
2229 }
2230 }),
2231 )
2232 .await;
2233
2234 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2235 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2236 // generate some history to select from
2237 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2238 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2239 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2240 open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
2241
2242 let finder = open_file_picker(&workspace, cx);
2243 let query = "collab_ui";
2244 cx.simulate_input(query);
2245 finder.update(cx, |picker, _| {
2246 let search_entries = collect_search_matches(picker).search_paths_only();
2247 assert_eq!(
2248 search_entries,
2249 vec![
2250 rel_path("collab_ui/collab_ui.rs").into(),
2251 rel_path("collab_ui/first.rs").into(),
2252 rel_path("collab_ui/third.rs").into(),
2253 rel_path("collab_ui/second.rs").into(),
2254 ],
2255 "Despite all search results having the same directory name, the most matching one should be on top"
2256 );
2257 });
2258}
2259
2260#[gpui::test]
2261async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
2262 let app_state = init_test(cx);
2263
2264 app_state
2265 .fs
2266 .as_fake()
2267 .insert_tree(
2268 path!("/src"),
2269 json!({
2270 "test": {
2271 "first.rs": "// First Rust file",
2272 "nonexistent.rs": "// Second Rust file",
2273 "third.rs": "// Third Rust file",
2274 }
2275 }),
2276 )
2277 .await;
2278
2279 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
2280 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); // generate some history to select from
2281 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2282 open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
2283 open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
2284 open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
2285 app_state
2286 .fs
2287 .remove_file(
2288 Path::new(path!("/src/test/nonexistent.rs")),
2289 RemoveOptions::default(),
2290 )
2291 .await
2292 .unwrap();
2293 cx.run_until_parked();
2294
2295 let picker = open_file_picker(&workspace, cx);
2296 cx.simulate_input("rs");
2297
2298 picker.update(cx, |picker, _| {
2299 assert_eq!(
2300 collect_search_matches(picker).history,
2301 vec![
2302 rel_path("test/first.rs").into(),
2303 rel_path("test/third.rs").into()
2304 ],
2305 "Should have all opened files in the history, except the ones that do not exist on disk"
2306 );
2307 });
2308}
2309
2310#[gpui::test]
2311async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) {
2312 let app_state = init_test(cx);
2313
2314 app_state
2315 .fs
2316 .as_fake()
2317 .insert_tree(
2318 "/src",
2319 json!({
2320 "lib.rs": "// Lib file",
2321 "main.rs": "// Bar file",
2322 "read.me": "// Readme file",
2323 }),
2324 )
2325 .await;
2326
2327 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2328 let (workspace, cx) =
2329 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2330
2331 // Initial state
2332 let picker = open_file_picker(&workspace, cx);
2333 cx.simulate_input("rs");
2334 picker.update(cx, |finder, _| {
2335 assert_eq!(finder.delegate.matches.len(), 3);
2336 assert_match_at_position(finder, 0, "lib.rs");
2337 assert_match_at_position(finder, 1, "main.rs");
2338 assert_match_at_position(finder, 2, "rs");
2339 });
2340 // Delete main.rs
2341 app_state
2342 .fs
2343 .remove_file("/src/main.rs".as_ref(), Default::default())
2344 .await
2345 .expect("unable to remove file");
2346 cx.executor().advance_clock(FS_WATCH_LATENCY);
2347
2348 // main.rs is in not among search results anymore
2349 picker.update(cx, |finder, _| {
2350 assert_eq!(finder.delegate.matches.len(), 2);
2351 assert_match_at_position(finder, 0, "lib.rs");
2352 assert_match_at_position(finder, 1, "rs");
2353 });
2354
2355 // Create util.rs
2356 app_state
2357 .fs
2358 .create_file("/src/util.rs".as_ref(), Default::default())
2359 .await
2360 .expect("unable to create file");
2361 cx.executor().advance_clock(FS_WATCH_LATENCY);
2362
2363 // util.rs is among search results
2364 picker.update(cx, |finder, _| {
2365 assert_eq!(finder.delegate.matches.len(), 3);
2366 assert_match_at_position(finder, 0, "lib.rs");
2367 assert_match_at_position(finder, 1, "util.rs");
2368 assert_match_at_position(finder, 2, "rs");
2369 });
2370}
2371
2372#[gpui::test]
2373async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui::TestAppContext) {
2374 let app_state = init_test(cx);
2375
2376 app_state
2377 .fs
2378 .as_fake()
2379 .insert_tree(
2380 "/src",
2381 json!({
2382 "lib.rs": "// Lib file",
2383 "main.rs": "// Bar file",
2384 "read.me": "// Readme file",
2385 }),
2386 )
2387 .await;
2388 app_state
2389 .fs
2390 .as_fake()
2391 .insert_tree(
2392 "/test",
2393 json!({
2394 "new.rs": "// New file",
2395 }),
2396 )
2397 .await;
2398
2399 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2400 let (workspace, cx) =
2401 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2402
2403 cx.update(|_, cx| {
2404 open_paths(
2405 &[PathBuf::from(path!("/test/new.rs"))],
2406 app_state,
2407 workspace::OpenOptions::default(),
2408 cx,
2409 )
2410 })
2411 .await
2412 .unwrap();
2413 assert_eq!(cx.update(|_, cx| cx.windows().len()), 1);
2414
2415 let initial_history = open_close_queried_buffer("new", 1, "new.rs", &workspace, cx).await;
2416 assert_eq!(
2417 initial_history.first().unwrap().absolute,
2418 PathBuf::from(path!("/test/new.rs")),
2419 "Should show 1st opened item in the history when opening the 2nd item"
2420 );
2421
2422 let history_after_first = open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
2423 assert_eq!(
2424 history_after_first.first().unwrap().absolute,
2425 PathBuf::from(path!("/test/new.rs")),
2426 "Should show 1st opened item in the history when opening the 2nd item"
2427 );
2428}
2429
2430#[gpui::test]
2431async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
2432 cx: &mut gpui::TestAppContext,
2433) {
2434 let app_state = init_test(cx);
2435
2436 app_state
2437 .fs
2438 .as_fake()
2439 .insert_tree(
2440 "/test",
2441 json!({
2442 "project_1": {
2443 "bar.rs": "// Bar file",
2444 "lib.rs": "// Lib file",
2445 },
2446 "project_2": {
2447 "Cargo.toml": "// Cargo file",
2448 "main.rs": "// Main file",
2449 }
2450 }),
2451 )
2452 .await;
2453
2454 let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await;
2455 let (workspace, cx) =
2456 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2457 let worktree_1_id = project.update(cx, |project, cx| {
2458 let worktree = project.worktrees(cx).last().expect("worktree not found");
2459 worktree.read(cx).id()
2460 });
2461
2462 // Initial state
2463 let picker = open_file_picker(&workspace, cx);
2464 cx.simulate_input("rs");
2465 picker.update(cx, |finder, _| {
2466 assert_eq!(finder.delegate.matches.len(), 3);
2467 assert_match_at_position(finder, 0, "bar.rs");
2468 assert_match_at_position(finder, 1, "lib.rs");
2469 assert_match_at_position(finder, 2, "rs");
2470 });
2471
2472 // Add new worktree
2473 project
2474 .update(cx, |project, cx| {
2475 project
2476 .find_or_create_worktree("/test/project_2", true, cx)
2477 .into_future()
2478 })
2479 .await
2480 .expect("unable to create workdir");
2481 cx.executor().advance_clock(FS_WATCH_LATENCY);
2482
2483 // main.rs is among search results
2484 picker.update(cx, |finder, _| {
2485 assert_eq!(finder.delegate.matches.len(), 4);
2486 assert_match_at_position(finder, 0, "bar.rs");
2487 assert_match_at_position(finder, 1, "lib.rs");
2488 assert_match_at_position(finder, 2, "main.rs");
2489 assert_match_at_position(finder, 3, "rs");
2490 });
2491
2492 // Remove the first worktree
2493 project.update(cx, |project, cx| {
2494 project.remove_worktree(worktree_1_id, cx);
2495 });
2496 cx.executor().advance_clock(FS_WATCH_LATENCY);
2497
2498 // Files from the first worktree are not in the search results anymore
2499 picker.update(cx, |finder, _| {
2500 assert_eq!(finder.delegate.matches.len(), 2);
2501 assert_match_at_position(finder, 0, "main.rs");
2502 assert_match_at_position(finder, 1, "rs");
2503 });
2504}
2505
2506#[gpui::test]
2507async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files(
2508 cx: &mut TestAppContext,
2509) {
2510 let app_state = init_test(cx);
2511 app_state
2512 .fs
2513 .as_fake()
2514 .insert_tree(
2515 path!("/repo1"),
2516 json!({
2517 "package.json": r#"{"name": "repo1"}"#,
2518 "src": {
2519 "index.js": "// Repo 1 index",
2520 }
2521 }),
2522 )
2523 .await;
2524
2525 app_state
2526 .fs
2527 .as_fake()
2528 .insert_tree(
2529 path!("/repo2"),
2530 json!({
2531 "package.json": r#"{"name": "repo2"}"#,
2532 "src": {
2533 "index.js": "// Repo 2 index",
2534 }
2535 }),
2536 )
2537 .await;
2538
2539 let project = Project::test(
2540 app_state.fs.clone(),
2541 [path!("/repo1").as_ref(), path!("/repo2").as_ref()],
2542 cx,
2543 )
2544 .await;
2545
2546 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2547 let (worktree_id1, worktree_id2) = cx.read(|cx| {
2548 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2549 (worktrees[0].read(cx).id(), worktrees[1].read(cx).id())
2550 });
2551
2552 workspace
2553 .update_in(cx, |workspace, window, cx| {
2554 workspace.open_path(
2555 ProjectPath {
2556 worktree_id: worktree_id1,
2557 path: rel_path("package.json").into(),
2558 },
2559 None,
2560 true,
2561 window,
2562 cx,
2563 )
2564 })
2565 .await
2566 .unwrap();
2567
2568 cx.dispatch_action(workspace::CloseActiveItem {
2569 save_intent: None,
2570 close_pinned: false,
2571 });
2572 workspace
2573 .update_in(cx, |workspace, window, cx| {
2574 workspace.open_path(
2575 ProjectPath {
2576 worktree_id: worktree_id2,
2577 path: rel_path("package.json").into(),
2578 },
2579 None,
2580 true,
2581 window,
2582 cx,
2583 )
2584 })
2585 .await
2586 .unwrap();
2587
2588 cx.dispatch_action(workspace::CloseActiveItem {
2589 save_intent: None,
2590 close_pinned: false,
2591 });
2592
2593 let picker = open_file_picker(&workspace, cx);
2594 cx.simulate_input("package.json");
2595
2596 picker.update(cx, |finder, _| {
2597 let matches = &finder.delegate.matches.matches;
2598
2599 assert_eq!(
2600 matches.len(),
2601 2,
2602 "Expected 1 history match + 1 search matches, but got {} matches: {:?}",
2603 matches.len(),
2604 matches
2605 );
2606
2607 assert_matches!(matches[0], Match::History { .. });
2608
2609 let search_matches = collect_search_matches(finder);
2610 assert_eq!(
2611 search_matches.history.len(),
2612 2,
2613 "Should have exactly 2 history match"
2614 );
2615 assert_eq!(
2616 search_matches.search.len(),
2617 0,
2618 "Should have exactly 0 search match (because we already opened the 2 package.json)"
2619 );
2620
2621 if let Match::History { path, panel_match } = &matches[0] {
2622 assert_eq!(path.project.worktree_id, worktree_id2);
2623 assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
2624 let panel_match = panel_match.as_ref().unwrap();
2625 assert_eq!(panel_match.0.path_prefix, rel_path("repo2").into());
2626 assert_eq!(panel_match.0.path, rel_path("package.json").into());
2627 assert_eq!(
2628 panel_match.0.positions,
2629 vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
2630 );
2631 }
2632
2633 if let Match::History { path, panel_match } = &matches[1] {
2634 assert_eq!(path.project.worktree_id, worktree_id1);
2635 assert_eq!(path.project.path.as_ref(), rel_path("package.json"));
2636 let panel_match = panel_match.as_ref().unwrap();
2637 assert_eq!(panel_match.0.path_prefix, rel_path("repo1").into());
2638 assert_eq!(panel_match.0.path, rel_path("package.json").into());
2639 assert_eq!(
2640 panel_match.0.positions,
2641 vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
2642 );
2643 }
2644 });
2645}
2646
2647#[gpui::test]
2648async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
2649 let app_state = init_test(cx);
2650
2651 app_state.fs.as_fake().insert_tree("/src", json!({})).await;
2652
2653 app_state
2654 .fs
2655 .create_dir("/src/even".as_ref())
2656 .await
2657 .expect("unable to create dir");
2658
2659 let initial_files_num = 5;
2660 for i in 0..initial_files_num {
2661 let filename = format!("/src/even/file_{}.txt", 10 + i);
2662 app_state
2663 .fs
2664 .create_file(Path::new(&filename), Default::default())
2665 .await
2666 .expect("unable to create file");
2667 }
2668
2669 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2670 let (workspace, cx) =
2671 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2672
2673 // Initial state
2674 let picker = open_file_picker(&workspace, cx);
2675 cx.simulate_input("file");
2676 let selected_index = 3;
2677 // Checking only the filename, not the whole path
2678 let selected_file = format!("file_{}.txt", 10 + selected_index);
2679 // Select even/file_13.txt
2680 for _ in 0..selected_index {
2681 cx.dispatch_action(SelectNext);
2682 }
2683
2684 picker.update(cx, |finder, _| {
2685 assert_match_selection(finder, selected_index, &selected_file)
2686 });
2687
2688 // Add more matches to the search results
2689 let files_to_add = 10;
2690 for i in 0..files_to_add {
2691 let filename = format!("/src/file_{}.txt", 20 + i);
2692 app_state
2693 .fs
2694 .create_file(Path::new(&filename), Default::default())
2695 .await
2696 .expect("unable to create file");
2697 }
2698 cx.executor().advance_clock(FS_WATCH_LATENCY);
2699
2700 // file_13.txt is still selected
2701 picker.update(cx, |finder, _| {
2702 let expected_selected_index = selected_index + files_to_add;
2703 assert_match_selection(finder, expected_selected_index, &selected_file);
2704 });
2705}
2706
2707#[gpui::test]
2708async fn test_first_match_selected_if_previous_one_is_not_in_the_match_list(
2709 cx: &mut gpui::TestAppContext,
2710) {
2711 let app_state = init_test(cx);
2712
2713 app_state
2714 .fs
2715 .as_fake()
2716 .insert_tree(
2717 "/src",
2718 json!({
2719 "file_1.txt": "// file_1",
2720 "file_2.txt": "// file_2",
2721 "file_3.txt": "// file_3",
2722 }),
2723 )
2724 .await;
2725
2726 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
2727 let (workspace, cx) =
2728 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2729
2730 // Initial state
2731 let picker = open_file_picker(&workspace, cx);
2732 cx.simulate_input("file");
2733 // Select even/file_2.txt
2734 cx.dispatch_action(SelectNext);
2735
2736 // Remove the selected entry
2737 app_state
2738 .fs
2739 .remove_file("/src/file_2.txt".as_ref(), Default::default())
2740 .await
2741 .expect("unable to remove file");
2742 cx.executor().advance_clock(FS_WATCH_LATENCY);
2743
2744 // file_1.txt is now selected
2745 picker.update(cx, |finder, _| {
2746 assert_match_selection(finder, 0, "file_1.txt");
2747 });
2748}
2749
2750#[gpui::test]
2751async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2752 let app_state = init_test(cx);
2753
2754 app_state
2755 .fs
2756 .as_fake()
2757 .insert_tree(
2758 path!("/test"),
2759 json!({
2760 "1.txt": "// One",
2761 }),
2762 )
2763 .await;
2764
2765 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2766 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2767
2768 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2769
2770 cx.simulate_modifiers_change(Modifiers::secondary_key());
2771 open_file_picker(&workspace, cx);
2772
2773 cx.simulate_modifiers_change(Modifiers::none());
2774 active_file_picker(&workspace, cx);
2775}
2776
2777#[gpui::test]
2778async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext) {
2779 let app_state = init_test(cx);
2780
2781 app_state
2782 .fs
2783 .as_fake()
2784 .insert_tree(
2785 path!("/test"),
2786 json!({
2787 "1.txt": "// One",
2788 "2.txt": "// Two",
2789 }),
2790 )
2791 .await;
2792
2793 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2794 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2795
2796 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2797 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2798
2799 cx.simulate_modifiers_change(Modifiers::secondary_key());
2800 let picker = open_file_picker(&workspace, cx);
2801 picker.update(cx, |finder, _| {
2802 assert_eq!(finder.delegate.matches.len(), 2);
2803 assert_match_selection(finder, 0, "2.txt");
2804 assert_match_at_position(finder, 1, "1.txt");
2805 });
2806
2807 cx.dispatch_action(SelectNext);
2808 cx.simulate_modifiers_change(Modifiers::none());
2809 cx.read(|cx| {
2810 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2811 assert_eq!(active_editor.read(cx).title(cx), "1.txt");
2812 });
2813}
2814
2815#[gpui::test]
2816async fn test_switches_between_release_norelease_modes_on_forward_nav(
2817 cx: &mut gpui::TestAppContext,
2818) {
2819 let app_state = init_test(cx);
2820
2821 app_state
2822 .fs
2823 .as_fake()
2824 .insert_tree(
2825 path!("/test"),
2826 json!({
2827 "1.txt": "// One",
2828 "2.txt": "// Two",
2829 }),
2830 )
2831 .await;
2832
2833 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2834 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2835
2836 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2837 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2838
2839 // Open with a shortcut
2840 cx.simulate_modifiers_change(Modifiers::secondary_key());
2841 let picker = open_file_picker(&workspace, cx);
2842 picker.update(cx, |finder, _| {
2843 assert_eq!(finder.delegate.matches.len(), 2);
2844 assert_match_selection(finder, 0, "2.txt");
2845 assert_match_at_position(finder, 1, "1.txt");
2846 });
2847
2848 // Switch to navigating with other shortcuts
2849 // Don't open file on modifiers release
2850 cx.simulate_modifiers_change(Modifiers::control());
2851 cx.dispatch_action(SelectNext);
2852 cx.simulate_modifiers_change(Modifiers::none());
2853 picker.update(cx, |finder, _| {
2854 assert_eq!(finder.delegate.matches.len(), 2);
2855 assert_match_at_position(finder, 0, "2.txt");
2856 assert_match_selection(finder, 1, "1.txt");
2857 });
2858
2859 // Back to navigation with initial shortcut
2860 // Open file on modifiers release
2861 cx.simulate_modifiers_change(Modifiers::secondary_key());
2862 cx.dispatch_action(ToggleFileFinder::default());
2863 cx.simulate_modifiers_change(Modifiers::none());
2864 cx.read(|cx| {
2865 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2866 assert_eq!(active_editor.read(cx).title(cx), "2.txt");
2867 });
2868}
2869
2870#[gpui::test]
2871async fn test_switches_between_release_norelease_modes_on_backward_nav(
2872 cx: &mut gpui::TestAppContext,
2873) {
2874 let app_state = init_test(cx);
2875
2876 app_state
2877 .fs
2878 .as_fake()
2879 .insert_tree(
2880 path!("/test"),
2881 json!({
2882 "1.txt": "// One",
2883 "2.txt": "// Two",
2884 "3.txt": "// Three"
2885 }),
2886 )
2887 .await;
2888
2889 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2890 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2891
2892 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2893 open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
2894 open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
2895
2896 // Open with a shortcut
2897 cx.simulate_modifiers_change(Modifiers::secondary_key());
2898 let picker = open_file_picker(&workspace, cx);
2899 picker.update(cx, |finder, _| {
2900 assert_eq!(finder.delegate.matches.len(), 3);
2901 assert_match_selection(finder, 0, "3.txt");
2902 assert_match_at_position(finder, 1, "2.txt");
2903 assert_match_at_position(finder, 2, "1.txt");
2904 });
2905
2906 // Switch to navigating with other shortcuts
2907 // Don't open file on modifiers release
2908 cx.simulate_modifiers_change(Modifiers::control());
2909 cx.dispatch_action(menu::SelectPrevious);
2910 cx.simulate_modifiers_change(Modifiers::none());
2911 picker.update(cx, |finder, _| {
2912 assert_eq!(finder.delegate.matches.len(), 3);
2913 assert_match_at_position(finder, 0, "3.txt");
2914 assert_match_at_position(finder, 1, "2.txt");
2915 assert_match_selection(finder, 2, "1.txt");
2916 });
2917
2918 // Back to navigation with initial shortcut
2919 // Open file on modifiers release
2920 cx.simulate_modifiers_change(Modifiers::secondary_key());
2921 cx.dispatch_action(SelectPrevious); // <-- File Finder's SelectPrevious, not menu's
2922 cx.simulate_modifiers_change(Modifiers::none());
2923 cx.read(|cx| {
2924 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
2925 assert_eq!(active_editor.read(cx).title(cx), "3.txt");
2926 });
2927}
2928
2929#[gpui::test]
2930async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::TestAppContext) {
2931 let app_state = init_test(cx);
2932
2933 app_state
2934 .fs
2935 .as_fake()
2936 .insert_tree(
2937 path!("/test"),
2938 json!({
2939 "1.txt": "// One",
2940 }),
2941 )
2942 .await;
2943
2944 let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
2945 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2946
2947 open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
2948
2949 cx.simulate_modifiers_change(Modifiers::secondary_key());
2950 open_file_picker(&workspace, cx);
2951
2952 cx.simulate_modifiers_change(Modifiers::command_shift());
2953 active_file_picker(&workspace, cx);
2954}
2955
2956#[gpui::test]
2957async fn test_repeat_toggle_action(cx: &mut gpui::TestAppContext) {
2958 let app_state = init_test(cx);
2959 app_state
2960 .fs
2961 .as_fake()
2962 .insert_tree(
2963 "/test",
2964 json!({
2965 "00.txt": "",
2966 "01.txt": "",
2967 "02.txt": "",
2968 "03.txt": "",
2969 "04.txt": "",
2970 "05.txt": "",
2971 }),
2972 )
2973 .await;
2974
2975 let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
2976 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
2977
2978 cx.dispatch_action(ToggleFileFinder::default());
2979 let picker = active_file_picker(&workspace, cx);
2980
2981 picker.update_in(cx, |picker, window, cx| {
2982 picker.update_matches(".txt".to_string(), window, cx)
2983 });
2984
2985 cx.run_until_parked();
2986
2987 picker.update(cx, |picker, _| {
2988 assert_eq!(picker.delegate.matches.len(), 7);
2989 assert_eq!(picker.delegate.selected_index, 0);
2990 });
2991
2992 // When toggling repeatedly, the picker scrolls to reveal the selected item.
2993 cx.dispatch_action(ToggleFileFinder::default());
2994 cx.dispatch_action(ToggleFileFinder::default());
2995 cx.dispatch_action(ToggleFileFinder::default());
2996
2997 cx.run_until_parked();
2998
2999 picker.update(cx, |picker, _| {
3000 assert_eq!(picker.delegate.matches.len(), 7);
3001 assert_eq!(picker.delegate.selected_index, 3);
3002 });
3003}
3004
3005async fn open_close_queried_buffer(
3006 input: &str,
3007 expected_matches: usize,
3008 expected_editor_title: &str,
3009 workspace: &Entity<Workspace>,
3010 cx: &mut gpui::VisualTestContext,
3011) -> Vec<FoundPath> {
3012 let history_items = open_queried_buffer(
3013 input,
3014 expected_matches,
3015 expected_editor_title,
3016 workspace,
3017 cx,
3018 )
3019 .await;
3020
3021 cx.dispatch_action(workspace::CloseActiveItem {
3022 save_intent: None,
3023 close_pinned: false,
3024 });
3025
3026 history_items
3027}
3028
3029async fn open_queried_buffer(
3030 input: &str,
3031 expected_matches: usize,
3032 expected_editor_title: &str,
3033 workspace: &Entity<Workspace>,
3034 cx: &mut gpui::VisualTestContext,
3035) -> Vec<FoundPath> {
3036 let picker = open_file_picker(workspace, cx);
3037 cx.simulate_input(input);
3038
3039 let history_items = picker.update(cx, |finder, _| {
3040 assert_eq!(
3041 finder.delegate.matches.len(),
3042 expected_matches + 1, // +1 from CreateNew option
3043 "Unexpected number of matches found for query `{input}`, matches: {:?}",
3044 finder.delegate.matches
3045 );
3046 finder.delegate.history_items.clone()
3047 });
3048
3049 cx.dispatch_action(Confirm);
3050
3051 cx.read(|cx| {
3052 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3053 let active_editor_title = active_editor.read(cx).title(cx);
3054 assert_eq!(
3055 expected_editor_title, active_editor_title,
3056 "Unexpected editor title for query `{input}`"
3057 );
3058 });
3059
3060 history_items
3061}
3062
3063fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
3064 cx.update(|cx| {
3065 let state = AppState::test(cx);
3066 theme::init(theme::LoadThemes::JustBase, cx);
3067 language::init(cx);
3068 super::init(cx);
3069 editor::init(cx);
3070 workspace::init_settings(cx);
3071 Project::init_settings(cx);
3072 state
3073 })
3074}
3075
3076fn test_path_position(test_str: &str) -> FileSearchQuery {
3077 let path_position = PathWithPosition::parse_str(test_str);
3078
3079 FileSearchQuery {
3080 raw_query: test_str.to_owned(),
3081 file_query_end: if path_position.path.to_str().unwrap() == test_str {
3082 None
3083 } else {
3084 Some(path_position.path.to_str().unwrap().len())
3085 },
3086 path_position,
3087 }
3088}
3089
3090fn build_find_picker(
3091 project: Entity<Project>,
3092 cx: &mut TestAppContext,
3093) -> (
3094 Entity<Picker<FileFinderDelegate>>,
3095 Entity<Workspace>,
3096 &mut VisualTestContext,
3097) {
3098 let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
3099 let picker = open_file_picker(&workspace, cx);
3100 (picker, workspace, cx)
3101}
3102
3103#[track_caller]
3104fn open_file_picker(
3105 workspace: &Entity<Workspace>,
3106 cx: &mut VisualTestContext,
3107) -> Entity<Picker<FileFinderDelegate>> {
3108 cx.dispatch_action(ToggleFileFinder {
3109 separate_history: true,
3110 });
3111 active_file_picker(workspace, cx)
3112}
3113
3114#[track_caller]
3115fn active_file_picker(
3116 workspace: &Entity<Workspace>,
3117 cx: &mut VisualTestContext,
3118) -> Entity<Picker<FileFinderDelegate>> {
3119 workspace.update(cx, |workspace, cx| {
3120 workspace
3121 .active_modal::<FileFinder>(cx)
3122 .expect("file finder is not open")
3123 .read(cx)
3124 .picker
3125 .clone()
3126 })
3127}
3128
3129#[derive(Debug, Default)]
3130struct SearchEntries {
3131 history: Vec<Arc<RelPath>>,
3132 history_found_paths: Vec<FoundPath>,
3133 search: Vec<Arc<RelPath>>,
3134 search_matches: Vec<PathMatch>,
3135}
3136
3137impl SearchEntries {
3138 #[track_caller]
3139 fn search_paths_only(self) -> Vec<Arc<RelPath>> {
3140 assert!(
3141 self.history.is_empty(),
3142 "Should have no history matches, but got: {:?}",
3143 self.history
3144 );
3145 self.search
3146 }
3147
3148 #[track_caller]
3149 fn search_matches_only(self) -> Vec<PathMatch> {
3150 assert!(
3151 self.history.is_empty(),
3152 "Should have no history matches, but got: {:?}",
3153 self.history
3154 );
3155 self.search_matches
3156 }
3157}
3158
3159fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
3160 let mut search_entries = SearchEntries::default();
3161 for m in &picker.delegate.matches.matches {
3162 match &m {
3163 Match::History {
3164 path: history_path,
3165 panel_match: path_match,
3166 } => {
3167 if let Some(path_match) = path_match.as_ref() {
3168 search_entries
3169 .history
3170 .push(path_match.0.path_prefix.join(&path_match.0.path));
3171 } else {
3172 // This occurs when the query is empty and we show history matches
3173 // that are outside the project.
3174 panic!("currently not exercised in tests");
3175 }
3176 search_entries
3177 .history_found_paths
3178 .push(history_path.clone());
3179 }
3180 Match::Search(path_match) => {
3181 search_entries
3182 .search
3183 .push(path_match.0.path_prefix.join(&path_match.0.path));
3184 search_entries.search_matches.push(path_match.0.clone());
3185 }
3186 Match::CreateNew(_) => {}
3187 }
3188 }
3189 search_entries
3190}
3191
3192#[track_caller]
3193fn assert_match_selection(
3194 finder: &Picker<FileFinderDelegate>,
3195 expected_selection_index: usize,
3196 expected_file_name: &str,
3197) {
3198 assert_eq!(
3199 finder.delegate.selected_index(),
3200 expected_selection_index,
3201 "Match is not selected"
3202 );
3203 assert_match_at_position(finder, expected_selection_index, expected_file_name);
3204}
3205
3206#[track_caller]
3207fn assert_match_at_position(
3208 finder: &Picker<FileFinderDelegate>,
3209 match_index: usize,
3210 expected_file_name: &str,
3211) {
3212 let match_item = finder
3213 .delegate
3214 .matches
3215 .get(match_index)
3216 .unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
3217 let match_file_name = match &match_item {
3218 Match::History { path, .. } => path.absolute.file_name().and_then(|s| s.to_str()),
3219 Match::Search(path_match) => path_match.0.path.file_name(),
3220 Match::CreateNew(project_path) => project_path.path.file_name(),
3221 }
3222 .unwrap();
3223 assert_eq!(match_file_name, expected_file_name);
3224}
3225
3226#[gpui::test]
3227async fn test_filename_precedence(cx: &mut TestAppContext) {
3228 let app_state = init_test(cx);
3229
3230 app_state
3231 .fs
3232 .as_fake()
3233 .insert_tree(
3234 path!("/src"),
3235 json!({
3236 "layout": {
3237 "app.css": "",
3238 "app.d.ts": "",
3239 "app.html": "",
3240 "+page.svelte": "",
3241 },
3242 "routes": {
3243 "+layout.svelte": "",
3244 }
3245 }),
3246 )
3247 .await;
3248
3249 let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
3250 let (picker, _, cx) = build_find_picker(project, cx);
3251
3252 cx.simulate_input("layout");
3253
3254 picker.update(cx, |finder, _| {
3255 let search_matches = collect_search_matches(finder).search_paths_only();
3256
3257 assert_eq!(
3258 search_matches,
3259 vec![
3260 rel_path("routes/+layout.svelte").into(),
3261 rel_path("layout/app.css").into(),
3262 rel_path("layout/app.d.ts").into(),
3263 rel_path("layout/app.html").into(),
3264 rel_path("layout/+page.svelte").into(),
3265 ],
3266 "File with 'layout' in filename should be prioritized over files in 'layout' directory"
3267 );
3268 });
3269}
3270
3271#[gpui::test]
3272async fn test_paths_with_starting_slash(cx: &mut TestAppContext) {
3273 let app_state = init_test(cx);
3274 app_state
3275 .fs
3276 .as_fake()
3277 .insert_tree(
3278 path!("/root"),
3279 json!({
3280 "a": {
3281 "file1.txt": "",
3282 "b": {
3283 "file2.txt": "",
3284 },
3285 }
3286 }),
3287 )
3288 .await;
3289
3290 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3291
3292 let (picker, workspace, cx) = build_find_picker(project, cx);
3293
3294 let matching_abs_path = "/file1.txt".to_string();
3295 picker
3296 .update_in(cx, |picker, window, cx| {
3297 picker
3298 .delegate
3299 .update_matches(matching_abs_path, window, cx)
3300 })
3301 .await;
3302 picker.update(cx, |picker, _| {
3303 assert_eq!(
3304 collect_search_matches(picker).search_paths_only(),
3305 vec![rel_path("a/file1.txt").into()],
3306 "Relative path starting with slash should match"
3307 )
3308 });
3309 cx.dispatch_action(SelectNext);
3310 cx.dispatch_action(Confirm);
3311 cx.read(|cx| {
3312 let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
3313 assert_eq!(active_editor.read(cx).title(cx), "file1.txt");
3314 });
3315}