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