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