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