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