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