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