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