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