1#[cfg(test)]
2mod file_finder_tests;
3
4mod new_path_prompt;
5
6use collections::{HashMap, HashSet};
7use editor::{scroll::Autoscroll, Bias, Editor};
8use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
9use gpui::{
10 actions, rems, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
11 Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, View,
12 ViewContext, VisualContext, WeakView,
13};
14use itertools::Itertools;
15use new_path_prompt::NewPathPrompt;
16use picker::{Picker, PickerDelegate};
17use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
18use settings::Settings;
19use std::{
20 cmp,
21 path::{Path, PathBuf},
22 sync::{
23 atomic::{self, AtomicBool},
24 Arc,
25 },
26};
27use text::Point;
28use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
29use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
30use workspace::{item::PreviewTabsSettings, ModalView, Workspace};
31
32actions!(file_finder, [Toggle, SelectPrev]);
33
34impl ModalView for FileFinder {}
35
36pub struct FileFinder {
37 picker: View<Picker<FileFinderDelegate>>,
38 init_modifiers: Option<Modifiers>,
39}
40
41pub fn init(cx: &mut AppContext) {
42 cx.observe_new_views(FileFinder::register).detach();
43 cx.observe_new_views(NewPathPrompt::register).detach();
44}
45
46impl FileFinder {
47 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
48 workspace.register_action(|workspace, _: &Toggle, cx| {
49 let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
50 Self::open(workspace, cx);
51 return;
52 };
53
54 file_finder.update(cx, |file_finder, cx| {
55 file_finder.init_modifiers = Some(cx.modifiers());
56 file_finder.picker.update(cx, |picker, cx| {
57 picker.cycle_selection(cx);
58 });
59 });
60 });
61 }
62
63 fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
64 let project = workspace.project().read(cx);
65
66 let currently_opened_path = workspace
67 .active_item(cx)
68 .and_then(|item| item.project_path(cx))
69 .map(|project_path| {
70 let abs_path = project
71 .worktree_for_id(project_path.worktree_id, cx)
72 .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
73 FoundPath::new(project_path, abs_path)
74 });
75
76 let history_items = workspace
77 .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
78 .into_iter()
79 .filter(|(_, history_abs_path)| match history_abs_path {
80 Some(abs_path) => history_file_exists(abs_path),
81 None => true,
82 })
83 .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path))
84 .collect::<Vec<_>>();
85
86 let project = workspace.project().clone();
87 let weak_workspace = cx.view().downgrade();
88 workspace.toggle_modal(cx, |cx| {
89 let delegate = FileFinderDelegate::new(
90 cx.view().downgrade(),
91 weak_workspace,
92 project,
93 currently_opened_path,
94 history_items,
95 cx,
96 );
97
98 FileFinder::new(delegate, cx)
99 });
100 }
101
102 fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
103 Self {
104 picker: cx.new_view(|cx| Picker::uniform_list(delegate, cx)),
105 init_modifiers: cx.modifiers().modified().then_some(cx.modifiers()),
106 }
107 }
108
109 fn handle_modifiers_changed(
110 &mut self,
111 event: &ModifiersChangedEvent,
112 cx: &mut ViewContext<Self>,
113 ) {
114 let Some(init_modifiers) = self.init_modifiers.take() else {
115 return;
116 };
117 if self.picker.read(cx).delegate.has_changed_selected_index {
118 if !event.modified() || !init_modifiers.is_subset_of(&event) {
119 self.init_modifiers = None;
120 cx.dispatch_action(menu::Confirm.boxed_clone());
121 }
122 }
123 }
124
125 fn handle_select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
126 self.init_modifiers = Some(cx.modifiers());
127 cx.dispatch_action(Box::new(menu::SelectPrev));
128 }
129}
130
131impl EventEmitter<DismissEvent> for FileFinder {}
132
133impl FocusableView for FileFinder {
134 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
135 self.picker.focus_handle(cx)
136 }
137}
138
139impl Render for FileFinder {
140 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
141 v_flex()
142 .key_context("FileFinder")
143 .w(rems(34.))
144 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
145 .on_action(cx.listener(Self::handle_select_prev))
146 .child(self.picker.clone())
147 }
148}
149
150pub struct FileFinderDelegate {
151 file_finder: WeakView<FileFinder>,
152 workspace: WeakView<Workspace>,
153 project: Model<Project>,
154 search_count: usize,
155 latest_search_id: usize,
156 latest_search_did_cancel: bool,
157 latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
158 currently_opened_path: Option<FoundPath>,
159 matches: Matches,
160 selected_index: usize,
161 has_changed_selected_index: bool,
162 cancel_flag: Arc<AtomicBool>,
163 history_items: Vec<FoundPath>,
164}
165
166/// Use a custom ordering for file finder: the regular one
167/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:
168/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`
169///
170/// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:
171/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`
172/// as the files are shown in the project panel lists.
173#[derive(Debug, Clone, PartialEq, Eq)]
174struct ProjectPanelOrdMatch(PathMatch);
175
176impl Ord for ProjectPanelOrdMatch {
177 fn cmp(&self, other: &Self) -> cmp::Ordering {
178 self.0
179 .score
180 .partial_cmp(&other.0.score)
181 .unwrap_or(cmp::Ordering::Equal)
182 .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))
183 .then_with(|| {
184 other
185 .0
186 .distance_to_relative_ancestor
187 .cmp(&self.0.distance_to_relative_ancestor)
188 })
189 .then_with(|| self.0.path.cmp(&other.0.path).reverse())
190 }
191}
192
193impl PartialOrd for ProjectPanelOrdMatch {
194 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
195 Some(self.cmp(other))
196 }
197}
198
199#[derive(Debug, Default)]
200struct Matches {
201 history: Vec<(FoundPath, Option<ProjectPanelOrdMatch>)>,
202 search: Vec<ProjectPanelOrdMatch>,
203}
204
205#[derive(Debug)]
206enum Match<'a> {
207 History(&'a FoundPath, Option<&'a ProjectPanelOrdMatch>),
208 Search(&'a ProjectPanelOrdMatch),
209}
210
211impl Matches {
212 fn len(&self) -> usize {
213 self.history.len() + self.search.len()
214 }
215
216 fn get(&self, index: usize) -> Option<Match<'_>> {
217 if index < self.history.len() {
218 self.history
219 .get(index)
220 .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
221 } else {
222 self.search
223 .get(index - self.history.len())
224 .map(Match::Search)
225 }
226 }
227
228 fn push_new_matches(
229 &mut self,
230 history_items: &Vec<FoundPath>,
231 currently_opened: Option<&FoundPath>,
232 query: &PathLikeWithPosition<FileSearchQuery>,
233 new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
234 extend_old_matches: bool,
235 ) {
236 let matching_history_paths =
237 matching_history_item_paths(history_items, currently_opened, query);
238 let new_search_matches = new_search_matches
239 .filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
240
241 self.set_new_history(
242 currently_opened,
243 Some(&matching_history_paths),
244 history_items,
245 );
246 if extend_old_matches {
247 self.search
248 .retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
249 } else {
250 self.search.clear();
251 }
252 util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
253 }
254
255 fn set_new_history<'a>(
256 &mut self,
257 currently_opened: Option<&'a FoundPath>,
258 query_matches: Option<&'a HashMap<Arc<Path>, ProjectPanelOrdMatch>>,
259 history_items: impl IntoIterator<Item = &'a FoundPath> + 'a,
260 ) {
261 let mut processed_paths = HashSet::default();
262 self.history = history_items
263 .into_iter()
264 .chain(currently_opened)
265 .filter(|&path| processed_paths.insert(path))
266 .filter_map(|history_item| match &query_matches {
267 Some(query_matches) => Some((
268 history_item.clone(),
269 Some(query_matches.get(&history_item.project.path)?.clone()),
270 )),
271 None => Some((history_item.clone(), None)),
272 })
273 .enumerate()
274 .sorted_by(
275 |(index_a, (path_a, match_a)), (index_b, (path_b, match_b))| match (
276 Some(path_a) == currently_opened,
277 Some(path_b) == currently_opened,
278 ) {
279 // bubble currently opened files to the top
280 (true, false) => cmp::Ordering::Less,
281 (false, true) => cmp::Ordering::Greater,
282 // arrange the files by their score (best score on top) and by their occurrence in the history
283 // (history items visited later are on the top)
284 _ => match_b.cmp(match_a).then(index_a.cmp(index_b)),
285 },
286 )
287 .map(|(_, paths)| paths)
288 .collect();
289 }
290}
291
292fn matching_history_item_paths(
293 history_items: &Vec<FoundPath>,
294 currently_opened: Option<&FoundPath>,
295 query: &PathLikeWithPosition<FileSearchQuery>,
296) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
297 let history_items_by_worktrees = history_items
298 .iter()
299 .chain(currently_opened)
300 .filter_map(|found_path| {
301 let candidate = PathMatchCandidate {
302 path: &found_path.project.path,
303 // Only match history items names, otherwise their paths may match too many queries, producing false positives.
304 // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
305 // it would be shown first always, despite the latter being a better match.
306 char_bag: CharBag::from_iter(
307 found_path
308 .project
309 .path
310 .file_name()?
311 .to_string_lossy()
312 .to_lowercase()
313 .chars(),
314 ),
315 };
316 Some((found_path.project.worktree_id, candidate))
317 })
318 .fold(
319 HashMap::default(),
320 |mut candidates, (worktree_id, new_candidate)| {
321 candidates
322 .entry(worktree_id)
323 .or_insert_with(Vec::new)
324 .push(new_candidate);
325 candidates
326 },
327 );
328 let mut matching_history_paths = HashMap::default();
329 for (worktree, candidates) in history_items_by_worktrees {
330 let max_results = candidates.len() + 1;
331 matching_history_paths.extend(
332 fuzzy::match_fixed_path_set(
333 candidates,
334 worktree.to_usize(),
335 query.path_like.path_query(),
336 false,
337 max_results,
338 )
339 .into_iter()
340 .map(|path_match| {
341 (
342 Arc::clone(&path_match.path),
343 ProjectPanelOrdMatch(path_match),
344 )
345 }),
346 );
347 }
348 matching_history_paths
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
352struct FoundPath {
353 project: ProjectPath,
354 absolute: Option<PathBuf>,
355}
356
357impl FoundPath {
358 fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
359 Self { project, absolute }
360 }
361}
362
363const MAX_RECENT_SELECTIONS: usize = 20;
364
365#[cfg(not(test))]
366fn history_file_exists(abs_path: &PathBuf) -> bool {
367 abs_path.exists()
368}
369
370#[cfg(test)]
371fn history_file_exists(abs_path: &PathBuf) -> bool {
372 !abs_path.ends_with("nonexistent.rs")
373}
374
375pub enum Event {
376 Selected(ProjectPath),
377 Dismissed,
378}
379
380#[derive(Debug, Clone)]
381struct FileSearchQuery {
382 raw_query: String,
383 file_query_end: Option<usize>,
384}
385
386impl FileSearchQuery {
387 fn path_query(&self) -> &str {
388 match self.file_query_end {
389 Some(file_path_end) => &self.raw_query[..file_path_end],
390 None => &self.raw_query,
391 }
392 }
393}
394
395impl FileFinderDelegate {
396 fn new(
397 file_finder: WeakView<FileFinder>,
398 workspace: WeakView<Workspace>,
399 project: Model<Project>,
400 currently_opened_path: Option<FoundPath>,
401 history_items: Vec<FoundPath>,
402 cx: &mut ViewContext<FileFinder>,
403 ) -> Self {
404 Self::subscribe_to_updates(&project, cx);
405 Self {
406 file_finder,
407 workspace,
408 project,
409 search_count: 0,
410 latest_search_id: 0,
411 latest_search_did_cancel: false,
412 latest_search_query: None,
413 currently_opened_path,
414 matches: Matches::default(),
415 has_changed_selected_index: false,
416 selected_index: 0,
417 cancel_flag: Arc::new(AtomicBool::new(false)),
418 history_items,
419 }
420 }
421
422 fn subscribe_to_updates(project: &Model<Project>, cx: &mut ViewContext<FileFinder>) {
423 cx.subscribe(project, |file_finder, _, event, cx| {
424 match event {
425 project::Event::WorktreeUpdatedEntries(_, _)
426 | project::Event::WorktreeAdded
427 | project::Event::WorktreeRemoved(_) => file_finder
428 .picker
429 .update(cx, |picker, cx| picker.refresh(cx)),
430 _ => {}
431 };
432 })
433 .detach();
434 }
435
436 fn spawn_search(
437 &mut self,
438 query: PathLikeWithPosition<FileSearchQuery>,
439 cx: &mut ViewContext<Picker<Self>>,
440 ) -> Task<()> {
441 let relative_to = self
442 .currently_opened_path
443 .as_ref()
444 .map(|found_path| Arc::clone(&found_path.project.path));
445 let worktrees = self
446 .project
447 .read(cx)
448 .visible_worktrees(cx)
449 .collect::<Vec<_>>();
450 let include_root_name = worktrees.len() > 1;
451 let candidate_sets = worktrees
452 .into_iter()
453 .map(|worktree| {
454 let worktree = worktree.read(cx);
455 PathMatchCandidateSet {
456 snapshot: worktree.snapshot(),
457 include_ignored: worktree
458 .root_entry()
459 .map_or(false, |entry| entry.is_ignored),
460 include_root_name,
461 directories_only: false,
462 }
463 })
464 .collect::<Vec<_>>();
465
466 let search_id = util::post_inc(&mut self.search_count);
467 self.cancel_flag.store(true, atomic::Ordering::Relaxed);
468 self.cancel_flag = Arc::new(AtomicBool::new(false));
469 let cancel_flag = self.cancel_flag.clone();
470 cx.spawn(|picker, mut cx| async move {
471 let matches = fuzzy::match_path_sets(
472 candidate_sets.as_slice(),
473 query.path_like.path_query(),
474 relative_to,
475 false,
476 100,
477 &cancel_flag,
478 cx.background_executor().clone(),
479 )
480 .await
481 .into_iter()
482 .map(ProjectPanelOrdMatch);
483 let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
484 picker
485 .update(&mut cx, |picker, cx| {
486 picker
487 .delegate
488 .set_search_matches(search_id, did_cancel, query, matches, cx)
489 })
490 .log_err();
491 })
492 }
493
494 fn set_search_matches(
495 &mut self,
496 search_id: usize,
497 did_cancel: bool,
498 query: PathLikeWithPosition<FileSearchQuery>,
499 matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
500 cx: &mut ViewContext<Picker<Self>>,
501 ) {
502 if search_id >= self.latest_search_id {
503 self.latest_search_id = search_id;
504 let extend_old_matches = self.latest_search_did_cancel
505 && Some(query.path_like.path_query())
506 == self
507 .latest_search_query
508 .as_ref()
509 .map(|query| query.path_like.path_query());
510 self.matches.push_new_matches(
511 &self.history_items,
512 self.currently_opened_path.as_ref(),
513 &query,
514 matches.into_iter(),
515 extend_old_matches,
516 );
517 self.latest_search_query = Some(query);
518 self.latest_search_did_cancel = did_cancel;
519 self.selected_index = self.calculate_selected_index();
520 cx.notify();
521 }
522 }
523
524 fn labels_for_match(
525 &self,
526 path_match: Match,
527 cx: &AppContext,
528 ix: usize,
529 ) -> (String, Vec<usize>, String, Vec<usize>) {
530 let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
531 Match::History(found_path, found_path_match) => {
532 let worktree_id = found_path.project.worktree_id;
533 let project_relative_path = &found_path.project.path;
534 let has_worktree = self
535 .project
536 .read(cx)
537 .worktree_for_id(worktree_id, cx)
538 .is_some();
539
540 if !has_worktree {
541 if let Some(absolute_path) = &found_path.absolute {
542 return (
543 absolute_path
544 .file_name()
545 .map_or_else(
546 || project_relative_path.to_string_lossy(),
547 |file_name| file_name.to_string_lossy(),
548 )
549 .to_string(),
550 Vec::new(),
551 absolute_path.to_string_lossy().to_string(),
552 Vec::new(),
553 );
554 }
555 }
556
557 let mut path = Arc::clone(project_relative_path);
558 if project_relative_path.as_ref() == Path::new("") {
559 if let Some(absolute_path) = &found_path.absolute {
560 path = Arc::from(absolute_path.as_path());
561 }
562 }
563
564 let mut path_match = PathMatch {
565 score: ix as f64,
566 positions: Vec::new(),
567 worktree_id: worktree_id.to_usize(),
568 path,
569 path_prefix: "".into(),
570 distance_to_relative_ancestor: usize::MAX,
571 };
572 if let Some(found_path_match) = found_path_match {
573 path_match
574 .positions
575 .extend(found_path_match.0.positions.iter())
576 }
577
578 self.labels_for_path_match(&path_match)
579 }
580 Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
581 };
582
583 if file_name_positions.is_empty() {
584 if let Some(user_home_path) = std::env::var("HOME").ok() {
585 let user_home_path = user_home_path.trim();
586 if !user_home_path.is_empty() {
587 if (&full_path).starts_with(user_home_path) {
588 return (
589 file_name,
590 file_name_positions,
591 full_path.replace(user_home_path, "~"),
592 full_path_positions,
593 );
594 }
595 }
596 }
597 }
598
599 (
600 file_name,
601 file_name_positions,
602 full_path,
603 full_path_positions,
604 )
605 }
606
607 fn labels_for_path_match(
608 &self,
609 path_match: &PathMatch,
610 ) -> (String, Vec<usize>, String, Vec<usize>) {
611 let path = &path_match.path;
612 let path_string = path.to_string_lossy();
613 let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
614 let mut path_positions = path_match.positions.clone();
615
616 let file_name = path.file_name().map_or_else(
617 || path_match.path_prefix.to_string(),
618 |file_name| file_name.to_string_lossy().to_string(),
619 );
620 let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
621 let file_name_positions = path_positions
622 .iter()
623 .filter_map(|pos| {
624 if pos >= &file_name_start {
625 Some(pos - file_name_start)
626 } else {
627 None
628 }
629 })
630 .collect();
631
632 let full_path = full_path.trim_end_matches(&file_name).to_string();
633 path_positions.retain(|idx| *idx < full_path.len());
634
635 (file_name, file_name_positions, full_path, path_positions)
636 }
637
638 fn lookup_absolute_path(
639 &self,
640 query: PathLikeWithPosition<FileSearchQuery>,
641 cx: &mut ViewContext<'_, Picker<Self>>,
642 ) -> Task<()> {
643 cx.spawn(|picker, mut cx| async move {
644 let Some((project, fs)) = picker
645 .update(&mut cx, |picker, cx| {
646 let fs = Arc::clone(&picker.delegate.project.read(cx).fs());
647 (picker.delegate.project.clone(), fs)
648 })
649 .log_err()
650 else {
651 return;
652 };
653
654 let query_path = Path::new(query.path_like.path_query());
655 let mut path_matches = Vec::new();
656 match fs.metadata(query_path).await.log_err() {
657 Some(Some(_metadata)) => {
658 let update_result = project
659 .update(&mut cx, |project, cx| {
660 if let Some((worktree, relative_path)) =
661 project.find_local_worktree(query_path, cx)
662 {
663 path_matches.push(ProjectPanelOrdMatch(PathMatch {
664 score: 1.0,
665 positions: Vec::new(),
666 worktree_id: worktree.read(cx).id().to_usize(),
667 path: Arc::from(relative_path),
668 path_prefix: "".into(),
669 distance_to_relative_ancestor: usize::MAX,
670 }));
671 }
672 })
673 .log_err();
674 if update_result.is_none() {
675 return;
676 }
677 }
678 Some(None) => {}
679 None => return,
680 }
681
682 picker
683 .update(&mut cx, |picker, cx| {
684 let picker_delegate = &mut picker.delegate;
685 let search_id = util::post_inc(&mut picker_delegate.search_count);
686 picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
687
688 anyhow::Ok(())
689 })
690 .log_err();
691 })
692 }
693
694 /// Skips first history match (that is displayed topmost) if it's currently opened.
695 fn calculate_selected_index(&self) -> usize {
696 if let Some(Match::History(path, _)) = self.matches.get(0) {
697 if Some(path) == self.currently_opened_path.as_ref() {
698 let elements_after_first = self.matches.len() - 1;
699 if elements_after_first > 0 {
700 return 1;
701 }
702 }
703 }
704 0
705 }
706}
707
708impl PickerDelegate for FileFinderDelegate {
709 type ListItem = ListItem;
710
711 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
712 "Search project files...".into()
713 }
714
715 fn match_count(&self) -> usize {
716 self.matches.len()
717 }
718
719 fn selected_index(&self) -> usize {
720 self.selected_index
721 }
722
723 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
724 self.has_changed_selected_index = true;
725 self.selected_index = ix;
726 cx.notify();
727 }
728
729 fn separators_after_indices(&self) -> Vec<usize> {
730 let history_items = self.matches.history.len();
731 if history_items == 0 || self.matches.search.is_empty() {
732 Vec::new()
733 } else {
734 vec![history_items - 1]
735 }
736 }
737
738 fn update_matches(
739 &mut self,
740 raw_query: String,
741 cx: &mut ViewContext<Picker<Self>>,
742 ) -> Task<()> {
743 let raw_query = raw_query.replace(' ', "");
744 let raw_query = raw_query.trim();
745 if raw_query.is_empty() {
746 let project = self.project.read(cx);
747 self.latest_search_id = post_inc(&mut self.search_count);
748 self.matches = Matches {
749 history: Vec::new(),
750 search: Vec::new(),
751 };
752 self.matches.set_new_history(
753 self.currently_opened_path.as_ref(),
754 None,
755 self.history_items.iter().filter(|history_item| {
756 project
757 .worktree_for_id(history_item.project.worktree_id, cx)
758 .is_some()
759 || (project.is_local() && history_item.absolute.is_some())
760 }),
761 );
762
763 self.selected_index = 0;
764 cx.notify();
765 Task::ready(())
766 } else {
767 let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
768 Ok::<_, std::convert::Infallible>(FileSearchQuery {
769 raw_query: raw_query.to_owned(),
770 file_query_end: if path_like_str == raw_query {
771 None
772 } else {
773 Some(path_like_str.len())
774 },
775 })
776 })
777 .expect("infallible");
778
779 if Path::new(query.path_like.path_query()).is_absolute() {
780 self.lookup_absolute_path(query, cx)
781 } else {
782 self.spawn_search(query, cx)
783 }
784 }
785 }
786
787 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
788 if let Some(m) = self.matches.get(self.selected_index()) {
789 if let Some(workspace) = self.workspace.upgrade() {
790 let open_task = workspace.update(cx, move |workspace, cx| {
791 let split_or_open =
792 |workspace: &mut Workspace,
793 project_path,
794 cx: &mut ViewContext<Workspace>| {
795 let allow_preview =
796 PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
797 if secondary {
798 workspace.split_path_preview(project_path, allow_preview, cx)
799 } else {
800 workspace.open_path_preview(
801 project_path,
802 None,
803 true,
804 allow_preview,
805 cx,
806 )
807 }
808 };
809 match m {
810 Match::History(history_match, _) => {
811 let worktree_id = history_match.project.worktree_id;
812 if workspace
813 .project()
814 .read(cx)
815 .worktree_for_id(worktree_id, cx)
816 .is_some()
817 {
818 split_or_open(
819 workspace,
820 ProjectPath {
821 worktree_id,
822 path: Arc::clone(&history_match.project.path),
823 },
824 cx,
825 )
826 } else {
827 match history_match.absolute.as_ref() {
828 Some(abs_path) => {
829 if secondary {
830 workspace.split_abs_path(
831 abs_path.to_path_buf(),
832 false,
833 cx,
834 )
835 } else {
836 workspace.open_abs_path(
837 abs_path.to_path_buf(),
838 false,
839 cx,
840 )
841 }
842 }
843 None => split_or_open(
844 workspace,
845 ProjectPath {
846 worktree_id,
847 path: Arc::clone(&history_match.project.path),
848 },
849 cx,
850 ),
851 }
852 }
853 }
854 Match::Search(m) => split_or_open(
855 workspace,
856 ProjectPath {
857 worktree_id: WorktreeId::from_usize(m.0.worktree_id),
858 path: m.0.path.clone(),
859 },
860 cx,
861 ),
862 }
863 });
864
865 let row = self
866 .latest_search_query
867 .as_ref()
868 .and_then(|query| query.row)
869 .map(|row| row.saturating_sub(1));
870 let col = self
871 .latest_search_query
872 .as_ref()
873 .and_then(|query| query.column)
874 .unwrap_or(0)
875 .saturating_sub(1);
876 let finder = self.file_finder.clone();
877
878 cx.spawn(|_, mut cx| async move {
879 let item = open_task.await.log_err()?;
880 if let Some(row) = row {
881 if let Some(active_editor) = item.downcast::<Editor>() {
882 active_editor
883 .downgrade()
884 .update(&mut cx, |editor, cx| {
885 let snapshot = editor.snapshot(cx).display_snapshot;
886 let point = snapshot
887 .buffer_snapshot
888 .clip_point(Point::new(row, col), Bias::Left);
889 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
890 s.select_ranges([point..point])
891 });
892 })
893 .log_err();
894 }
895 }
896 finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
897
898 Some(())
899 })
900 .detach();
901 }
902 }
903 }
904
905 fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
906 self.file_finder
907 .update(cx, |_, cx| cx.emit(DismissEvent))
908 .log_err();
909 }
910
911 fn render_match(
912 &self,
913 ix: usize,
914 selected: bool,
915 cx: &mut ViewContext<Picker<Self>>,
916 ) -> Option<Self::ListItem> {
917 let path_match = self
918 .matches
919 .get(ix)
920 .expect("Invalid matches state: no element for index {ix}");
921
922 let (file_name, file_name_positions, full_path, full_path_positions) =
923 self.labels_for_match(path_match, cx, ix);
924
925 Some(
926 ListItem::new(ix)
927 .spacing(ListItemSpacing::Sparse)
928 .inset(true)
929 .selected(selected)
930 .child(
931 h_flex()
932 .gap_2()
933 .py_px()
934 .child(HighlightedLabel::new(file_name, file_name_positions))
935 .child(
936 HighlightedLabel::new(full_path, full_path_positions)
937 .size(LabelSize::Small)
938 .color(Color::Muted),
939 ),
940 ),
941 )
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948
949 #[test]
950 fn test_custom_project_search_ordering_in_file_finder() {
951 let mut file_finder_sorted_output = vec![
952 ProjectPanelOrdMatch(PathMatch {
953 score: 0.5,
954 positions: Vec::new(),
955 worktree_id: 0,
956 path: Arc::from(Path::new("b0.5")),
957 path_prefix: Arc::from(""),
958 distance_to_relative_ancestor: 0,
959 }),
960 ProjectPanelOrdMatch(PathMatch {
961 score: 1.0,
962 positions: Vec::new(),
963 worktree_id: 0,
964 path: Arc::from(Path::new("c1.0")),
965 path_prefix: Arc::from(""),
966 distance_to_relative_ancestor: 0,
967 }),
968 ProjectPanelOrdMatch(PathMatch {
969 score: 1.0,
970 positions: Vec::new(),
971 worktree_id: 0,
972 path: Arc::from(Path::new("a1.0")),
973 path_prefix: Arc::from(""),
974 distance_to_relative_ancestor: 0,
975 }),
976 ProjectPanelOrdMatch(PathMatch {
977 score: 0.5,
978 positions: Vec::new(),
979 worktree_id: 0,
980 path: Arc::from(Path::new("a0.5")),
981 path_prefix: Arc::from(""),
982 distance_to_relative_ancestor: 0,
983 }),
984 ProjectPanelOrdMatch(PathMatch {
985 score: 1.0,
986 positions: Vec::new(),
987 worktree_id: 0,
988 path: Arc::from(Path::new("b1.0")),
989 path_prefix: Arc::from(""),
990 distance_to_relative_ancestor: 0,
991 }),
992 ];
993 file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
994
995 assert_eq!(
996 file_finder_sorted_output,
997 vec![
998 ProjectPanelOrdMatch(PathMatch {
999 score: 1.0,
1000 positions: Vec::new(),
1001 worktree_id: 0,
1002 path: Arc::from(Path::new("a1.0")),
1003 path_prefix: Arc::from(""),
1004 distance_to_relative_ancestor: 0,
1005 }),
1006 ProjectPanelOrdMatch(PathMatch {
1007 score: 1.0,
1008 positions: Vec::new(),
1009 worktree_id: 0,
1010 path: Arc::from(Path::new("b1.0")),
1011 path_prefix: Arc::from(""),
1012 distance_to_relative_ancestor: 0,
1013 }),
1014 ProjectPanelOrdMatch(PathMatch {
1015 score: 1.0,
1016 positions: Vec::new(),
1017 worktree_id: 0,
1018 path: Arc::from(Path::new("c1.0")),
1019 path_prefix: Arc::from(""),
1020 distance_to_relative_ancestor: 0,
1021 }),
1022 ProjectPanelOrdMatch(PathMatch {
1023 score: 0.5,
1024 positions: Vec::new(),
1025 worktree_id: 0,
1026 path: Arc::from(Path::new("a0.5")),
1027 path_prefix: Arc::from(""),
1028 distance_to_relative_ancestor: 0,
1029 }),
1030 ProjectPanelOrdMatch(PathMatch {
1031 score: 0.5,
1032 positions: Vec::new(),
1033 worktree_id: 0,
1034 path: Arc::from(Path::new("b0.5")),
1035 path_prefix: Arc::from(""),
1036 distance_to_relative_ancestor: 0,
1037 }),
1038 ]
1039 );
1040 }
1041}