1use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
2use fuzzy::PathMatch;
3use gpui::{
4 actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
5};
6use picker::{Picker, PickerDelegate};
7use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
8use std::{
9 path::{Path, PathBuf},
10 sync::{
11 atomic::{self, AtomicBool},
12 Arc,
13 },
14};
15use text::Point;
16use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
17use workspace::Workspace;
18
19pub type FileFinder = Picker<FileFinderDelegate>;
20
21pub struct FileFinderDelegate {
22 workspace: WeakViewHandle<Workspace>,
23 project: ModelHandle<Project>,
24 search_count: usize,
25 latest_search_id: usize,
26 latest_search_did_cancel: bool,
27 latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
28 currently_opened_path: Option<FoundPath>,
29 matches: Matches,
30 selected_index: Option<usize>,
31 cancel_flag: Arc<AtomicBool>,
32 history_items: Vec<FoundPath>,
33}
34
35#[derive(Debug)]
36enum Matches {
37 History(Vec<FoundPath>),
38 Search(Vec<PathMatch>),
39}
40
41#[derive(Debug)]
42enum Match<'a> {
43 History(&'a FoundPath),
44 Search(&'a PathMatch),
45}
46
47impl Matches {
48 fn len(&self) -> usize {
49 match self {
50 Self::History(items) => items.len(),
51 Self::Search(items) => items.len(),
52 }
53 }
54
55 fn get(&self, index: usize) -> Option<Match<'_>> {
56 match self {
57 Self::History(items) => items.get(index).map(Match::History),
58 Self::Search(items) => items.get(index).map(Match::Search),
59 }
60 }
61}
62
63impl Default for Matches {
64 fn default() -> Self {
65 Self::History(Vec::new())
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70struct FoundPath {
71 project: ProjectPath,
72 absolute: Option<PathBuf>,
73}
74
75impl FoundPath {
76 fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
77 Self { project, absolute }
78 }
79}
80
81actions!(file_finder, [Toggle]);
82
83pub fn init(cx: &mut AppContext) {
84 cx.add_action(toggle_file_finder);
85 FileFinder::init(cx);
86}
87
88const MAX_RECENT_SELECTIONS: usize = 20;
89
90fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
91 workspace.toggle_modal(cx, |workspace, cx| {
92 let project = workspace.project().read(cx);
93
94 let currently_opened_path = workspace
95 .active_item(cx)
96 .and_then(|item| item.project_path(cx))
97 .map(|project_path| {
98 let abs_path = project
99 .worktree_for_id(project_path.worktree_id, cx)
100 .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
101 FoundPath::new(project_path, abs_path)
102 });
103
104 // if exists, bubble the currently opened path to the top
105 let history_items = currently_opened_path
106 .clone()
107 .into_iter()
108 .chain(
109 workspace
110 .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
111 .into_iter()
112 .filter(|(history_path, _)| {
113 Some(history_path)
114 != currently_opened_path
115 .as_ref()
116 .map(|found_path| &found_path.project)
117 })
118 .filter(|(_, history_abs_path)| {
119 history_abs_path.as_ref()
120 != currently_opened_path
121 .as_ref()
122 .and_then(|found_path| found_path.absolute.as_ref())
123 })
124 .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
125 )
126 .collect();
127
128 let project = workspace.project().clone();
129 let workspace = cx.handle().downgrade();
130 let finder = cx.add_view(|cx| {
131 Picker::new(
132 FileFinderDelegate::new(
133 workspace,
134 project,
135 currently_opened_path,
136 history_items,
137 cx,
138 ),
139 cx,
140 )
141 });
142 finder
143 });
144}
145
146pub enum Event {
147 Selected(ProjectPath),
148 Dismissed,
149}
150
151#[derive(Debug, Clone)]
152struct FileSearchQuery {
153 raw_query: String,
154 file_query_end: Option<usize>,
155}
156
157impl FileSearchQuery {
158 fn path_query(&self) -> &str {
159 match self.file_query_end {
160 Some(file_path_end) => &self.raw_query[..file_path_end],
161 None => &self.raw_query,
162 }
163 }
164}
165
166impl FileFinderDelegate {
167 fn new(
168 workspace: WeakViewHandle<Workspace>,
169 project: ModelHandle<Project>,
170 currently_opened_path: Option<FoundPath>,
171 history_items: Vec<FoundPath>,
172 cx: &mut ViewContext<FileFinder>,
173 ) -> Self {
174 cx.observe(&project, |picker, _, cx| {
175 picker.update_matches(picker.query(cx), cx);
176 })
177 .detach();
178 Self {
179 workspace,
180 project,
181 search_count: 0,
182 latest_search_id: 0,
183 latest_search_did_cancel: false,
184 latest_search_query: None,
185 currently_opened_path,
186 matches: Matches::default(),
187 selected_index: None,
188 cancel_flag: Arc::new(AtomicBool::new(false)),
189 history_items,
190 }
191 }
192
193 fn spawn_search(
194 &mut self,
195 query: PathLikeWithPosition<FileSearchQuery>,
196 cx: &mut ViewContext<FileFinder>,
197 ) -> Task<()> {
198 let relative_to = self
199 .currently_opened_path
200 .as_ref()
201 .map(|found_path| Arc::clone(&found_path.project.path));
202 let worktrees = self
203 .project
204 .read(cx)
205 .visible_worktrees(cx)
206 .collect::<Vec<_>>();
207 let include_root_name = worktrees.len() > 1;
208 let candidate_sets = worktrees
209 .into_iter()
210 .map(|worktree| {
211 let worktree = worktree.read(cx);
212 PathMatchCandidateSet {
213 snapshot: worktree.snapshot(),
214 include_ignored: worktree
215 .root_entry()
216 .map_or(false, |entry| entry.is_ignored),
217 include_root_name,
218 }
219 })
220 .collect::<Vec<_>>();
221
222 let search_id = util::post_inc(&mut self.search_count);
223 self.cancel_flag.store(true, atomic::Ordering::Relaxed);
224 self.cancel_flag = Arc::new(AtomicBool::new(false));
225 let cancel_flag = self.cancel_flag.clone();
226 cx.spawn(|picker, mut cx| async move {
227 let matches = fuzzy::match_path_sets(
228 candidate_sets.as_slice(),
229 query.path_like.path_query(),
230 relative_to,
231 false,
232 100,
233 &cancel_flag,
234 cx.background(),
235 )
236 .await;
237 let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
238 picker
239 .update(&mut cx, |picker, cx| {
240 picker
241 .delegate_mut()
242 .set_search_matches(search_id, did_cancel, query, matches, cx)
243 })
244 .log_err();
245 })
246 }
247
248 fn set_search_matches(
249 &mut self,
250 search_id: usize,
251 did_cancel: bool,
252 query: PathLikeWithPosition<FileSearchQuery>,
253 matches: Vec<PathMatch>,
254 cx: &mut ViewContext<FileFinder>,
255 ) {
256 if search_id >= self.latest_search_id {
257 self.latest_search_id = search_id;
258 if self.latest_search_did_cancel
259 && Some(query.path_like.path_query())
260 == self
261 .latest_search_query
262 .as_ref()
263 .map(|query| query.path_like.path_query())
264 {
265 match &mut self.matches {
266 Matches::History(_) => self.matches = Matches::Search(matches),
267 Matches::Search(search_matches) => {
268 util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
269 b.cmp(a)
270 })
271 }
272 }
273 } else {
274 self.matches = Matches::Search(matches);
275 }
276 self.latest_search_query = Some(query);
277 self.latest_search_did_cancel = did_cancel;
278 cx.notify();
279 }
280 }
281
282 fn labels_for_match(
283 &self,
284 path_match: Match,
285 cx: &AppContext,
286 ix: usize,
287 ) -> (String, Vec<usize>, String, Vec<usize>) {
288 let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
289 Match::History(found_path) => {
290 let worktree_id = found_path.project.worktree_id;
291 let project_relative_path = &found_path.project.path;
292 let has_worktree = self
293 .project
294 .read(cx)
295 .worktree_for_id(worktree_id, cx)
296 .is_some();
297
298 if !has_worktree {
299 if let Some(absolute_path) = &found_path.absolute {
300 return (
301 absolute_path
302 .file_name()
303 .map_or_else(
304 || project_relative_path.to_string_lossy(),
305 |file_name| file_name.to_string_lossy(),
306 )
307 .to_string(),
308 Vec::new(),
309 absolute_path.to_string_lossy().to_string(),
310 Vec::new(),
311 );
312 }
313 }
314
315 let mut path = Arc::clone(project_relative_path);
316 if project_relative_path.as_ref() == Path::new("") {
317 if let Some(absolute_path) = &found_path.absolute {
318 path = Arc::from(absolute_path.as_path());
319 }
320 }
321 self.labels_for_path_match(&PathMatch {
322 score: ix as f64,
323 positions: Vec::new(),
324 worktree_id: worktree_id.to_usize(),
325 path,
326 path_prefix: "".into(),
327 distance_to_relative_ancestor: usize::MAX,
328 })
329 }
330 Match::Search(path_match) => self.labels_for_path_match(path_match),
331 };
332
333 if file_name_positions.is_empty() {
334 if let Some(user_home_path) = std::env::var("HOME").ok() {
335 let user_home_path = user_home_path.trim();
336 if !user_home_path.is_empty() {
337 if (&full_path).starts_with(user_home_path) {
338 return (
339 file_name,
340 file_name_positions,
341 full_path.replace(user_home_path, "~"),
342 full_path_positions,
343 );
344 }
345 }
346 }
347 }
348
349 (
350 file_name,
351 file_name_positions,
352 full_path,
353 full_path_positions,
354 )
355 }
356
357 fn labels_for_path_match(
358 &self,
359 path_match: &PathMatch,
360 ) -> (String, Vec<usize>, String, Vec<usize>) {
361 let path = &path_match.path;
362 let path_string = path.to_string_lossy();
363 let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
364 let path_positions = path_match.positions.clone();
365
366 let file_name = path.file_name().map_or_else(
367 || path_match.path_prefix.to_string(),
368 |file_name| file_name.to_string_lossy().to_string(),
369 );
370 let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
371 - file_name.chars().count();
372 let file_name_positions = path_positions
373 .iter()
374 .filter_map(|pos| {
375 if pos >= &file_name_start {
376 Some(pos - file_name_start)
377 } else {
378 None
379 }
380 })
381 .collect();
382
383 (file_name, file_name_positions, full_path, path_positions)
384 }
385}
386
387impl PickerDelegate for FileFinderDelegate {
388 fn placeholder_text(&self) -> Arc<str> {
389 "Search project files...".into()
390 }
391
392 fn match_count(&self) -> usize {
393 self.matches.len()
394 }
395
396 fn selected_index(&self) -> usize {
397 self.selected_index.unwrap_or(0)
398 }
399
400 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
401 self.selected_index = Some(ix);
402 cx.notify();
403 }
404
405 fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
406 if raw_query.is_empty() {
407 let project = self.project.read(cx);
408 self.latest_search_id = post_inc(&mut self.search_count);
409 self.matches = Matches::History(
410 self.history_items
411 .iter()
412 .filter(|history_item| {
413 project
414 .worktree_for_id(history_item.project.worktree_id, cx)
415 .is_some()
416 || (project.is_local()
417 && history_item
418 .absolute
419 .as_ref()
420 .filter(|abs_path| abs_path.exists())
421 .is_some())
422 })
423 .cloned()
424 .collect(),
425 );
426 cx.notify();
427 Task::ready(())
428 } else {
429 let raw_query = &raw_query;
430 let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
431 Ok::<_, std::convert::Infallible>(FileSearchQuery {
432 raw_query: raw_query.to_owned(),
433 file_query_end: if path_like_str == raw_query {
434 None
435 } else {
436 Some(path_like_str.len())
437 },
438 })
439 })
440 .expect("infallible");
441 self.spawn_search(query, cx)
442 }
443 }
444
445 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<FileFinder>) {
446 if let Some(m) = self.matches.get(self.selected_index()) {
447 if let Some(workspace) = self.workspace.upgrade(cx) {
448 let open_task = workspace.update(cx, move |workspace, cx| {
449 let split_or_open = |workspace: &mut Workspace, project_path, cx| {
450 if secondary {
451 workspace.split_path(project_path, cx)
452 } else {
453 workspace.open_path(project_path, None, true, cx)
454 }
455 };
456 match m {
457 Match::History(history_match) => {
458 let worktree_id = history_match.project.worktree_id;
459 if workspace
460 .project()
461 .read(cx)
462 .worktree_for_id(worktree_id, cx)
463 .is_some()
464 {
465 split_or_open(
466 workspace,
467 ProjectPath {
468 worktree_id,
469 path: Arc::clone(&history_match.project.path),
470 },
471 cx,
472 )
473 } else {
474 match history_match.absolute.as_ref() {
475 Some(abs_path) => {
476 if secondary {
477 workspace.split_abs_path(
478 abs_path.to_path_buf(),
479 false,
480 cx,
481 )
482 } else {
483 workspace.open_abs_path(
484 abs_path.to_path_buf(),
485 false,
486 cx,
487 )
488 }
489 }
490 None => split_or_open(
491 workspace,
492 ProjectPath {
493 worktree_id,
494 path: Arc::clone(&history_match.project.path),
495 },
496 cx,
497 ),
498 }
499 }
500 }
501 Match::Search(m) => split_or_open(
502 workspace,
503 ProjectPath {
504 worktree_id: WorktreeId::from_usize(m.worktree_id),
505 path: m.path.clone(),
506 },
507 cx,
508 ),
509 }
510 });
511
512 let row = self
513 .latest_search_query
514 .as_ref()
515 .and_then(|query| query.row)
516 .map(|row| row.saturating_sub(1));
517 let col = self
518 .latest_search_query
519 .as_ref()
520 .and_then(|query| query.column)
521 .unwrap_or(0)
522 .saturating_sub(1);
523 cx.spawn(|_, mut cx| async move {
524 let item = open_task.await.log_err()?;
525 if let Some(row) = row {
526 if let Some(active_editor) = item.downcast::<Editor>() {
527 active_editor
528 .downgrade()
529 .update(&mut cx, |editor, cx| {
530 let snapshot = editor.snapshot(cx).display_snapshot;
531 let point = snapshot
532 .buffer_snapshot
533 .clip_point(Point::new(row, col), Bias::Left);
534 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
535 s.select_ranges([point..point])
536 });
537 })
538 .log_err();
539 }
540 }
541 workspace
542 .downgrade()
543 .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
544 .log_err();
545
546 Some(())
547 })
548 .detach();
549 }
550 }
551 }
552
553 fn dismissed(&mut self, _: &mut ViewContext<FileFinder>) {}
554
555 fn render_match(
556 &self,
557 ix: usize,
558 mouse_state: &mut MouseState,
559 selected: bool,
560 cx: &AppContext,
561 ) -> AnyElement<Picker<Self>> {
562 let path_match = self
563 .matches
564 .get(ix)
565 .expect("Invalid matches state: no element for index {ix}");
566 let theme = theme::current(cx);
567 let style = theme.picker.item.in_state(selected).style_for(mouse_state);
568 let (file_name, file_name_positions, full_path, full_path_positions) =
569 self.labels_for_match(path_match, cx, ix);
570 Flex::column()
571 .with_child(
572 Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
573 )
574 .with_child(
575 Label::new(full_path, style.label.clone()).with_highlights(full_path_positions),
576 )
577 .flex(1., false)
578 .contained()
579 .with_style(style.container)
580 .into_any_named("match")
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
587
588 use super::*;
589 use editor::Editor;
590 use gpui::{TestAppContext, ViewHandle};
591 use menu::{Confirm, SelectNext};
592 use serde_json::json;
593 use workspace::{AppState, Workspace};
594
595 #[ctor::ctor]
596 fn init_logger() {
597 if std::env::var("RUST_LOG").is_ok() {
598 env_logger::init();
599 }
600 }
601
602 #[gpui::test]
603 async fn test_matching_paths(cx: &mut TestAppContext) {
604 let app_state = init_test(cx);
605 app_state
606 .fs
607 .as_fake()
608 .insert_tree(
609 "/root",
610 json!({
611 "a": {
612 "banana": "",
613 "bandana": "",
614 }
615 }),
616 )
617 .await;
618
619 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
620 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
621 cx.dispatch_action(window_id, Toggle);
622
623 let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
624 finder
625 .update(cx, |finder, cx| {
626 finder.delegate_mut().update_matches("bna".to_string(), cx)
627 })
628 .await;
629 finder.read_with(cx, |finder, _| {
630 assert_eq!(finder.delegate().matches.len(), 2);
631 });
632
633 let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
634 cx.dispatch_action(window_id, SelectNext);
635 cx.dispatch_action(window_id, Confirm);
636 active_pane
637 .condition(cx, |pane, _| pane.active_item().is_some())
638 .await;
639 cx.read(|cx| {
640 let active_item = active_pane.read(cx).active_item().unwrap();
641 assert_eq!(
642 active_item
643 .as_any()
644 .downcast_ref::<Editor>()
645 .unwrap()
646 .read(cx)
647 .title(cx),
648 "bandana"
649 );
650 });
651 }
652
653 #[gpui::test]
654 async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
655 let app_state = init_test(cx);
656
657 let first_file_name = "first.rs";
658 let first_file_contents = "// First Rust file";
659 app_state
660 .fs
661 .as_fake()
662 .insert_tree(
663 "/src",
664 json!({
665 "test": {
666 first_file_name: first_file_contents,
667 "second.rs": "// Second Rust file",
668 }
669 }),
670 )
671 .await;
672
673 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
674 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
675 cx.dispatch_action(window_id, Toggle);
676 let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
677
678 let file_query = &first_file_name[..3];
679 let file_row = 1;
680 let file_column = 3;
681 assert!(file_column <= first_file_contents.len());
682 let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
683 finder
684 .update(cx, |finder, cx| {
685 finder
686 .delegate_mut()
687 .update_matches(query_inside_file.to_string(), cx)
688 })
689 .await;
690 finder.read_with(cx, |finder, _| {
691 let finder = finder.delegate();
692 assert_eq!(finder.matches.len(), 1);
693 let latest_search_query = finder
694 .latest_search_query
695 .as_ref()
696 .expect("Finder should have a query after the update_matches call");
697 assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
698 assert_eq!(
699 latest_search_query.path_like.file_query_end,
700 Some(file_query.len())
701 );
702 assert_eq!(latest_search_query.row, Some(file_row));
703 assert_eq!(latest_search_query.column, Some(file_column as u32));
704 });
705
706 let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
707 cx.dispatch_action(window_id, SelectNext);
708 cx.dispatch_action(window_id, Confirm);
709 active_pane
710 .condition(cx, |pane, _| pane.active_item().is_some())
711 .await;
712 let editor = cx.update(|cx| {
713 let active_item = active_pane.read(cx).active_item().unwrap();
714 active_item.downcast::<Editor>().unwrap()
715 });
716 cx.foreground().advance_clock(Duration::from_secs(2));
717 cx.foreground().start_waiting();
718 cx.foreground().finish_waiting();
719 editor.update(cx, |editor, cx| {
720 let all_selections = editor.selections.all_adjusted(cx);
721 assert_eq!(
722 all_selections.len(),
723 1,
724 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
725 );
726 let caret_selection = all_selections.into_iter().next().unwrap();
727 assert_eq!(caret_selection.start, caret_selection.end,
728 "Caret selection should have its start and end at the same position");
729 assert_eq!(file_row, caret_selection.start.row + 1,
730 "Query inside file should get caret with the same focus row");
731 assert_eq!(file_column, caret_selection.start.column as usize + 1,
732 "Query inside file should get caret with the same focus column");
733 });
734 }
735
736 #[gpui::test]
737 async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
738 let app_state = init_test(cx);
739
740 let first_file_name = "first.rs";
741 let first_file_contents = "// First Rust file";
742 app_state
743 .fs
744 .as_fake()
745 .insert_tree(
746 "/src",
747 json!({
748 "test": {
749 first_file_name: first_file_contents,
750 "second.rs": "// Second Rust file",
751 }
752 }),
753 )
754 .await;
755
756 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
757 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
758 cx.dispatch_action(window_id, Toggle);
759 let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
760
761 let file_query = &first_file_name[..3];
762 let file_row = 200;
763 let file_column = 300;
764 assert!(file_column > first_file_contents.len());
765 let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
766 finder
767 .update(cx, |finder, cx| {
768 finder
769 .delegate_mut()
770 .update_matches(query_outside_file.to_string(), cx)
771 })
772 .await;
773 finder.read_with(cx, |finder, _| {
774 let finder = finder.delegate();
775 assert_eq!(finder.matches.len(), 1);
776 let latest_search_query = finder
777 .latest_search_query
778 .as_ref()
779 .expect("Finder should have a query after the update_matches call");
780 assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
781 assert_eq!(
782 latest_search_query.path_like.file_query_end,
783 Some(file_query.len())
784 );
785 assert_eq!(latest_search_query.row, Some(file_row));
786 assert_eq!(latest_search_query.column, Some(file_column as u32));
787 });
788
789 let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
790 cx.dispatch_action(window_id, SelectNext);
791 cx.dispatch_action(window_id, Confirm);
792 active_pane
793 .condition(cx, |pane, _| pane.active_item().is_some())
794 .await;
795 let editor = cx.update(|cx| {
796 let active_item = active_pane.read(cx).active_item().unwrap();
797 active_item.downcast::<Editor>().unwrap()
798 });
799 cx.foreground().advance_clock(Duration::from_secs(2));
800 cx.foreground().start_waiting();
801 cx.foreground().finish_waiting();
802 editor.update(cx, |editor, cx| {
803 let all_selections = editor.selections.all_adjusted(cx);
804 assert_eq!(
805 all_selections.len(),
806 1,
807 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
808 );
809 let caret_selection = all_selections.into_iter().next().unwrap();
810 assert_eq!(caret_selection.start, caret_selection.end,
811 "Caret selection should have its start and end at the same position");
812 assert_eq!(0, caret_selection.start.row,
813 "Excessive rows (as in query outside file borders) should get trimmed to last file row");
814 assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
815 "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
816 });
817 }
818
819 #[gpui::test]
820 async fn test_matching_cancellation(cx: &mut TestAppContext) {
821 let app_state = init_test(cx);
822 app_state
823 .fs
824 .as_fake()
825 .insert_tree(
826 "/dir",
827 json!({
828 "hello": "",
829 "goodbye": "",
830 "halogen-light": "",
831 "happiness": "",
832 "height": "",
833 "hi": "",
834 "hiccup": "",
835 }),
836 )
837 .await;
838
839 let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
840 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
841 let (_, finder) = cx.add_window(|cx| {
842 Picker::new(
843 FileFinderDelegate::new(
844 workspace.downgrade(),
845 workspace.read(cx).project().clone(),
846 None,
847 Vec::new(),
848 cx,
849 ),
850 cx,
851 )
852 });
853
854 let query = test_path_like("hi");
855 finder
856 .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
857 .await;
858 finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5));
859
860 finder.update(cx, |finder, cx| {
861 let delegate = finder.delegate_mut();
862 let matches = match &delegate.matches {
863 Matches::Search(path_matches) => path_matches,
864 _ => panic!("Search matches expected"),
865 }
866 .clone();
867
868 // Simulate a search being cancelled after the time limit,
869 // returning only a subset of the matches that would have been found.
870 drop(delegate.spawn_search(query.clone(), cx));
871 delegate.set_search_matches(
872 delegate.latest_search_id,
873 true, // did-cancel
874 query.clone(),
875 vec![matches[1].clone(), matches[3].clone()],
876 cx,
877 );
878
879 // Simulate another cancellation.
880 drop(delegate.spawn_search(query.clone(), cx));
881 delegate.set_search_matches(
882 delegate.latest_search_id,
883 true, // did-cancel
884 query.clone(),
885 vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
886 cx,
887 );
888
889 match &delegate.matches {
890 Matches::Search(new_matches) => {
891 assert_eq!(new_matches.as_slice(), &matches[0..4])
892 }
893 _ => panic!("Search matches expected"),
894 };
895 });
896 }
897
898 #[gpui::test]
899 async fn test_ignored_files(cx: &mut TestAppContext) {
900 let app_state = init_test(cx);
901 app_state
902 .fs
903 .as_fake()
904 .insert_tree(
905 "/ancestor",
906 json!({
907 ".gitignore": "ignored-root",
908 "ignored-root": {
909 "happiness": "",
910 "height": "",
911 "hi": "",
912 "hiccup": "",
913 },
914 "tracked-root": {
915 ".gitignore": "height",
916 "happiness": "",
917 "height": "",
918 "hi": "",
919 "hiccup": "",
920 },
921 }),
922 )
923 .await;
924
925 let project = Project::test(
926 app_state.fs.clone(),
927 [
928 "/ancestor/tracked-root".as_ref(),
929 "/ancestor/ignored-root".as_ref(),
930 ],
931 cx,
932 )
933 .await;
934 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
935 let (_, finder) = cx.add_window(|cx| {
936 Picker::new(
937 FileFinderDelegate::new(
938 workspace.downgrade(),
939 workspace.read(cx).project().clone(),
940 None,
941 Vec::new(),
942 cx,
943 ),
944 cx,
945 )
946 });
947 finder
948 .update(cx, |f, cx| {
949 f.delegate_mut().spawn_search(test_path_like("hi"), cx)
950 })
951 .await;
952 finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
953 }
954
955 #[gpui::test]
956 async fn test_single_file_worktrees(cx: &mut TestAppContext) {
957 let app_state = init_test(cx);
958 app_state
959 .fs
960 .as_fake()
961 .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
962 .await;
963
964 let project = Project::test(
965 app_state.fs.clone(),
966 ["/root/the-parent-dir/the-file".as_ref()],
967 cx,
968 )
969 .await;
970 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
971 let (_, finder) = cx.add_window(|cx| {
972 Picker::new(
973 FileFinderDelegate::new(
974 workspace.downgrade(),
975 workspace.read(cx).project().clone(),
976 None,
977 Vec::new(),
978 cx,
979 ),
980 cx,
981 )
982 });
983
984 // Even though there is only one worktree, that worktree's filename
985 // is included in the matching, because the worktree is a single file.
986 finder
987 .update(cx, |f, cx| {
988 f.delegate_mut().spawn_search(test_path_like("thf"), cx)
989 })
990 .await;
991 cx.read(|cx| {
992 let finder = finder.read(cx);
993 let delegate = finder.delegate();
994 let matches = match &delegate.matches {
995 Matches::Search(path_matches) => path_matches,
996 _ => panic!("Search matches expected"),
997 };
998 assert_eq!(matches.len(), 1);
999
1000 let (file_name, file_name_positions, full_path, full_path_positions) =
1001 delegate.labels_for_path_match(&matches[0]);
1002 assert_eq!(file_name, "the-file");
1003 assert_eq!(file_name_positions, &[0, 1, 4]);
1004 assert_eq!(full_path, "the-file");
1005 assert_eq!(full_path_positions, &[0, 1, 4]);
1006 });
1007
1008 // Since the worktree root is a file, searching for its name followed by a slash does
1009 // not match anything.
1010 finder
1011 .update(cx, |f, cx| {
1012 f.delegate_mut().spawn_search(test_path_like("thf/"), cx)
1013 })
1014 .await;
1015 finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
1016 }
1017
1018 #[gpui::test]
1019 async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
1020 let app_state = init_test(cx);
1021 app_state
1022 .fs
1023 .as_fake()
1024 .insert_tree(
1025 "/root",
1026 json!({
1027 "dir1": { "a.txt": "" },
1028 "dir2": { "a.txt": "" }
1029 }),
1030 )
1031 .await;
1032
1033 let project = Project::test(
1034 app_state.fs.clone(),
1035 ["/root/dir1".as_ref(), "/root/dir2".as_ref()],
1036 cx,
1037 )
1038 .await;
1039 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1040
1041 let (_, finder) = cx.add_window(|cx| {
1042 Picker::new(
1043 FileFinderDelegate::new(
1044 workspace.downgrade(),
1045 workspace.read(cx).project().clone(),
1046 None,
1047 Vec::new(),
1048 cx,
1049 ),
1050 cx,
1051 )
1052 });
1053
1054 // Run a search that matches two files with the same relative path.
1055 finder
1056 .update(cx, |f, cx| {
1057 f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
1058 })
1059 .await;
1060
1061 // Can switch between different matches with the same relative path.
1062 finder.update(cx, |finder, cx| {
1063 let delegate = finder.delegate_mut();
1064 assert_eq!(delegate.matches.len(), 2);
1065 assert_eq!(delegate.selected_index(), 0);
1066 delegate.set_selected_index(1, cx);
1067 assert_eq!(delegate.selected_index(), 1);
1068 delegate.set_selected_index(0, cx);
1069 assert_eq!(delegate.selected_index(), 0);
1070 });
1071 }
1072
1073 #[gpui::test]
1074 async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1075 let app_state = init_test(cx);
1076 app_state
1077 .fs
1078 .as_fake()
1079 .insert_tree(
1080 "/root",
1081 json!({
1082 "dir1": { "a.txt": "" },
1083 "dir2": {
1084 "a.txt": "",
1085 "b.txt": ""
1086 }
1087 }),
1088 )
1089 .await;
1090
1091 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1092 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1093 let worktree_id = cx.read(|cx| {
1094 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1095 assert_eq!(worktrees.len(), 1);
1096 WorktreeId::from_usize(worktrees[0].id())
1097 });
1098
1099 // When workspace has an active item, sort items which are closer to that item
1100 // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1101 // so that one should be sorted earlier
1102 let b_path = Some(dummy_found_path(ProjectPath {
1103 worktree_id,
1104 path: Arc::from(Path::new("/root/dir2/b.txt")),
1105 }));
1106 let (_, finder) = cx.add_window(|cx| {
1107 Picker::new(
1108 FileFinderDelegate::new(
1109 workspace.downgrade(),
1110 workspace.read(cx).project().clone(),
1111 b_path,
1112 Vec::new(),
1113 cx,
1114 ),
1115 cx,
1116 )
1117 });
1118
1119 finder
1120 .update(cx, |f, cx| {
1121 f.delegate_mut().spawn_search(test_path_like("a.txt"), cx)
1122 })
1123 .await;
1124
1125 finder.read_with(cx, |f, _| {
1126 let delegate = f.delegate();
1127 let matches = match &delegate.matches {
1128 Matches::Search(path_matches) => path_matches,
1129 _ => panic!("Search matches expected"),
1130 };
1131 assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
1132 assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
1133 });
1134 }
1135
1136 #[gpui::test]
1137 async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1138 let app_state = init_test(cx);
1139 app_state
1140 .fs
1141 .as_fake()
1142 .insert_tree(
1143 "/root",
1144 json!({
1145 "dir1": {},
1146 "dir2": {
1147 "dir3": {}
1148 }
1149 }),
1150 )
1151 .await;
1152
1153 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1154 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1155 let (_, finder) = cx.add_window(|cx| {
1156 Picker::new(
1157 FileFinderDelegate::new(
1158 workspace.downgrade(),
1159 workspace.read(cx).project().clone(),
1160 None,
1161 Vec::new(),
1162 cx,
1163 ),
1164 cx,
1165 )
1166 });
1167 finder
1168 .update(cx, |f, cx| {
1169 f.delegate_mut().spawn_search(test_path_like("dir"), cx)
1170 })
1171 .await;
1172 cx.read(|cx| {
1173 let finder = finder.read(cx);
1174 assert_eq!(finder.delegate().matches.len(), 0);
1175 });
1176 }
1177
1178 #[gpui::test]
1179 async fn test_query_history(
1180 deterministic: Arc<gpui::executor::Deterministic>,
1181 cx: &mut gpui::TestAppContext,
1182 ) {
1183 let app_state = init_test(cx);
1184
1185 app_state
1186 .fs
1187 .as_fake()
1188 .insert_tree(
1189 "/src",
1190 json!({
1191 "test": {
1192 "first.rs": "// First Rust file",
1193 "second.rs": "// Second Rust file",
1194 "third.rs": "// Third Rust file",
1195 }
1196 }),
1197 )
1198 .await;
1199
1200 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1201 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1202 let worktree_id = cx.read(|cx| {
1203 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1204 assert_eq!(worktrees.len(), 1);
1205 WorktreeId::from_usize(worktrees[0].id())
1206 });
1207
1208 // Open and close panels, getting their history items afterwards.
1209 // Ensure history items get populated with opened items, and items are kept in a certain order.
1210 // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1211 //
1212 // TODO: without closing, the opened items do not propagate their history changes for some reason
1213 // it does work in real app though, only tests do not propagate.
1214
1215 let initial_history = open_close_queried_buffer(
1216 "fir",
1217 1,
1218 "first.rs",
1219 window_id,
1220 &workspace,
1221 &deterministic,
1222 cx,
1223 )
1224 .await;
1225 assert!(
1226 initial_history.is_empty(),
1227 "Should have no history before opening any files"
1228 );
1229
1230 let history_after_first = open_close_queried_buffer(
1231 "sec",
1232 1,
1233 "second.rs",
1234 window_id,
1235 &workspace,
1236 &deterministic,
1237 cx,
1238 )
1239 .await;
1240 assert_eq!(
1241 history_after_first,
1242 vec![FoundPath::new(
1243 ProjectPath {
1244 worktree_id,
1245 path: Arc::from(Path::new("test/first.rs")),
1246 },
1247 Some(PathBuf::from("/src/test/first.rs"))
1248 )],
1249 "Should show 1st opened item in the history when opening the 2nd item"
1250 );
1251
1252 let history_after_second = open_close_queried_buffer(
1253 "thi",
1254 1,
1255 "third.rs",
1256 window_id,
1257 &workspace,
1258 &deterministic,
1259 cx,
1260 )
1261 .await;
1262 assert_eq!(
1263 history_after_second,
1264 vec![
1265 FoundPath::new(
1266 ProjectPath {
1267 worktree_id,
1268 path: Arc::from(Path::new("test/second.rs")),
1269 },
1270 Some(PathBuf::from("/src/test/second.rs"))
1271 ),
1272 FoundPath::new(
1273 ProjectPath {
1274 worktree_id,
1275 path: Arc::from(Path::new("test/first.rs")),
1276 },
1277 Some(PathBuf::from("/src/test/first.rs"))
1278 ),
1279 ],
1280 "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
12812nd item should be the first in the history, as the last opened."
1282 );
1283
1284 let history_after_third = open_close_queried_buffer(
1285 "sec",
1286 1,
1287 "second.rs",
1288 window_id,
1289 &workspace,
1290 &deterministic,
1291 cx,
1292 )
1293 .await;
1294 assert_eq!(
1295 history_after_third,
1296 vec![
1297 FoundPath::new(
1298 ProjectPath {
1299 worktree_id,
1300 path: Arc::from(Path::new("test/third.rs")),
1301 },
1302 Some(PathBuf::from("/src/test/third.rs"))
1303 ),
1304 FoundPath::new(
1305 ProjectPath {
1306 worktree_id,
1307 path: Arc::from(Path::new("test/second.rs")),
1308 },
1309 Some(PathBuf::from("/src/test/second.rs"))
1310 ),
1311 FoundPath::new(
1312 ProjectPath {
1313 worktree_id,
1314 path: Arc::from(Path::new("test/first.rs")),
1315 },
1316 Some(PathBuf::from("/src/test/first.rs"))
1317 ),
1318 ],
1319 "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
13203rd item should be the first in the history, as the last opened."
1321 );
1322
1323 let history_after_second_again = open_close_queried_buffer(
1324 "thi",
1325 1,
1326 "third.rs",
1327 window_id,
1328 &workspace,
1329 &deterministic,
1330 cx,
1331 )
1332 .await;
1333 assert_eq!(
1334 history_after_second_again,
1335 vec![
1336 FoundPath::new(
1337 ProjectPath {
1338 worktree_id,
1339 path: Arc::from(Path::new("test/second.rs")),
1340 },
1341 Some(PathBuf::from("/src/test/second.rs"))
1342 ),
1343 FoundPath::new(
1344 ProjectPath {
1345 worktree_id,
1346 path: Arc::from(Path::new("test/third.rs")),
1347 },
1348 Some(PathBuf::from("/src/test/third.rs"))
1349 ),
1350 FoundPath::new(
1351 ProjectPath {
1352 worktree_id,
1353 path: Arc::from(Path::new("test/first.rs")),
1354 },
1355 Some(PathBuf::from("/src/test/first.rs"))
1356 ),
1357 ],
1358 "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
13592nd item, as the last opened, 3rd item should go next as it was opened right before."
1360 );
1361 }
1362
1363 #[gpui::test]
1364 async fn test_external_files_history(
1365 deterministic: Arc<gpui::executor::Deterministic>,
1366 cx: &mut gpui::TestAppContext,
1367 ) {
1368 let app_state = init_test(cx);
1369
1370 app_state
1371 .fs
1372 .as_fake()
1373 .insert_tree(
1374 "/src",
1375 json!({
1376 "test": {
1377 "first.rs": "// First Rust file",
1378 "second.rs": "// Second Rust file",
1379 }
1380 }),
1381 )
1382 .await;
1383
1384 app_state
1385 .fs
1386 .as_fake()
1387 .insert_tree(
1388 "/external-src",
1389 json!({
1390 "test": {
1391 "third.rs": "// Third Rust file",
1392 "fourth.rs": "// Fourth Rust file",
1393 }
1394 }),
1395 )
1396 .await;
1397
1398 let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1399 cx.update(|cx| {
1400 project.update(cx, |project, cx| {
1401 project.find_or_create_local_worktree("/external-src", false, cx)
1402 })
1403 })
1404 .detach();
1405 deterministic.run_until_parked();
1406
1407 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1408 let worktree_id = cx.read(|cx| {
1409 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1410 assert_eq!(worktrees.len(), 1,);
1411
1412 WorktreeId::from_usize(worktrees[0].id())
1413 });
1414 workspace
1415 .update(cx, |workspace, cx| {
1416 workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
1417 })
1418 .detach();
1419 deterministic.run_until_parked();
1420 let external_worktree_id = cx.read(|cx| {
1421 let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1422 assert_eq!(
1423 worktrees.len(),
1424 2,
1425 "External file should get opened in a new worktree"
1426 );
1427
1428 WorktreeId::from_usize(
1429 worktrees
1430 .into_iter()
1431 .find(|worktree| worktree.id() != worktree_id.to_usize())
1432 .expect("New worktree should have a different id")
1433 .id(),
1434 )
1435 });
1436 close_active_item(&workspace, &deterministic, cx).await;
1437
1438 let initial_history_items = open_close_queried_buffer(
1439 "sec",
1440 1,
1441 "second.rs",
1442 window_id,
1443 &workspace,
1444 &deterministic,
1445 cx,
1446 )
1447 .await;
1448 assert_eq!(
1449 initial_history_items,
1450 vec![FoundPath::new(
1451 ProjectPath {
1452 worktree_id: external_worktree_id,
1453 path: Arc::from(Path::new("")),
1454 },
1455 Some(PathBuf::from("/external-src/test/third.rs"))
1456 )],
1457 "Should show external file with its full path in the history after it was open"
1458 );
1459
1460 let updated_history_items = open_close_queried_buffer(
1461 "fir",
1462 1,
1463 "first.rs",
1464 window_id,
1465 &workspace,
1466 &deterministic,
1467 cx,
1468 )
1469 .await;
1470 assert_eq!(
1471 updated_history_items,
1472 vec![
1473 FoundPath::new(
1474 ProjectPath {
1475 worktree_id,
1476 path: Arc::from(Path::new("test/second.rs")),
1477 },
1478 Some(PathBuf::from("/src/test/second.rs"))
1479 ),
1480 FoundPath::new(
1481 ProjectPath {
1482 worktree_id: external_worktree_id,
1483 path: Arc::from(Path::new("")),
1484 },
1485 Some(PathBuf::from("/external-src/test/third.rs"))
1486 ),
1487 ],
1488 "Should keep external file with history updates",
1489 );
1490 }
1491
1492 async fn open_close_queried_buffer(
1493 input: &str,
1494 expected_matches: usize,
1495 expected_editor_title: &str,
1496 window_id: usize,
1497 workspace: &ViewHandle<Workspace>,
1498 deterministic: &gpui::executor::Deterministic,
1499 cx: &mut gpui::TestAppContext,
1500 ) -> Vec<FoundPath> {
1501 cx.dispatch_action(window_id, Toggle);
1502 let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
1503 finder
1504 .update(cx, |finder, cx| {
1505 finder.delegate_mut().update_matches(input.to_string(), cx)
1506 })
1507 .await;
1508 let history_items = finder.read_with(cx, |finder, _| {
1509 assert_eq!(
1510 finder.delegate().matches.len(),
1511 expected_matches,
1512 "Unexpected number of matches found for query {input}"
1513 );
1514 finder.delegate().history_items.clone()
1515 });
1516
1517 let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1518 cx.dispatch_action(window_id, SelectNext);
1519 cx.dispatch_action(window_id, Confirm);
1520 deterministic.run_until_parked();
1521 active_pane
1522 .condition(cx, |pane, _| pane.active_item().is_some())
1523 .await;
1524 cx.read(|cx| {
1525 let active_item = active_pane.read(cx).active_item().unwrap();
1526 let active_editor_title = active_item
1527 .as_any()
1528 .downcast_ref::<Editor>()
1529 .unwrap()
1530 .read(cx)
1531 .title(cx);
1532 assert_eq!(
1533 expected_editor_title, active_editor_title,
1534 "Unexpected editor title for query {input}"
1535 );
1536 });
1537
1538 close_active_item(workspace, deterministic, cx).await;
1539
1540 history_items
1541 }
1542
1543 async fn close_active_item(
1544 workspace: &ViewHandle<Workspace>,
1545 deterministic: &gpui::executor::Deterministic,
1546 cx: &mut TestAppContext,
1547 ) {
1548 let mut original_items = HashMap::new();
1549 cx.read(|cx| {
1550 for pane in workspace.read(cx).panes() {
1551 let pane_id = pane.id();
1552 let pane = pane.read(cx);
1553 let insertion_result = original_items.insert(pane_id, pane.items().count());
1554 assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
1555 }
1556 });
1557
1558 let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1559 active_pane
1560 .update(cx, |pane, cx| {
1561 pane.close_active_item(&workspace::CloseActiveItem, cx)
1562 .unwrap()
1563 })
1564 .await
1565 .unwrap();
1566 deterministic.run_until_parked();
1567 cx.read(|cx| {
1568 for pane in workspace.read(cx).panes() {
1569 let pane_id = pane.id();
1570 let pane = pane.read(cx);
1571 match original_items.remove(&pane_id) {
1572 Some(original_items) => {
1573 assert_eq!(
1574 pane.items().count(),
1575 original_items.saturating_sub(1),
1576 "Pane id {pane_id} should have item closed"
1577 );
1578 }
1579 None => panic!("Pane id {pane_id} not found in original items"),
1580 }
1581 }
1582 });
1583 assert!(
1584 original_items.len() <= 1,
1585 "At most one panel should got closed"
1586 );
1587 }
1588
1589 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1590 cx.foreground().forbid_parking();
1591 cx.update(|cx| {
1592 let state = AppState::test(cx);
1593 theme::init((), cx);
1594 language::init(cx);
1595 super::init(cx);
1596 editor::init(cx);
1597 workspace::init_settings(cx);
1598 Project::init_settings(cx);
1599 state
1600 })
1601 }
1602
1603 fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1604 PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1605 Ok::<_, std::convert::Infallible>(FileSearchQuery {
1606 raw_query: test_str.to_owned(),
1607 file_query_end: if path_like_str == test_str {
1608 None
1609 } else {
1610 Some(path_like_str.len())
1611 },
1612 })
1613 })
1614 .unwrap()
1615 }
1616
1617 fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
1618 FoundPath {
1619 project: project_path,
1620 absolute: None,
1621 }
1622 }
1623}