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::PathLikeWithPosition, 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<PathLikeWithPosition<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<&PathLikeWithPosition<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<&PathLikeWithPosition<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_like.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}
404
405impl FileSearchQuery {
406 fn path_query(&self) -> &str {
407 match self.file_query_end {
408 Some(file_path_end) => &self.raw_query[..file_path_end],
409 None => &self.raw_query,
410 }
411 }
412}
413
414impl FileFinderDelegate {
415 fn new(
416 file_finder: WeakView<FileFinder>,
417 workspace: WeakView<Workspace>,
418 project: Model<Project>,
419 currently_opened_path: Option<FoundPath>,
420 history_items: Vec<FoundPath>,
421 separate_history: bool,
422 cx: &mut ViewContext<FileFinder>,
423 ) -> Self {
424 Self::subscribe_to_updates(&project, cx);
425 Self {
426 file_finder,
427 workspace,
428 project,
429 search_count: 0,
430 latest_search_id: 0,
431 latest_search_did_cancel: false,
432 latest_search_query: None,
433 currently_opened_path,
434 matches: Matches::default(),
435 has_changed_selected_index: false,
436 selected_index: 0,
437 cancel_flag: Arc::new(AtomicBool::new(false)),
438 history_items,
439 separate_history,
440 }
441 }
442
443 fn subscribe_to_updates(project: &Model<Project>, cx: &mut ViewContext<FileFinder>) {
444 cx.subscribe(project, |file_finder, _, event, cx| {
445 match event {
446 project::Event::WorktreeUpdatedEntries(_, _)
447 | project::Event::WorktreeAdded
448 | project::Event::WorktreeRemoved(_) => file_finder
449 .picker
450 .update(cx, |picker, cx| picker.refresh(cx)),
451 _ => {}
452 };
453 })
454 .detach();
455 }
456
457 fn spawn_search(
458 &mut self,
459 query: PathLikeWithPosition<FileSearchQuery>,
460 cx: &mut ViewContext<Picker<Self>>,
461 ) -> Task<()> {
462 let relative_to = self
463 .currently_opened_path
464 .as_ref()
465 .map(|found_path| Arc::clone(&found_path.project.path));
466 let worktrees = self
467 .project
468 .read(cx)
469 .visible_worktrees(cx)
470 .collect::<Vec<_>>();
471 let include_root_name = worktrees.len() > 1;
472 let candidate_sets = worktrees
473 .into_iter()
474 .map(|worktree| {
475 let worktree = worktree.read(cx);
476 PathMatchCandidateSet {
477 snapshot: worktree.snapshot(),
478 include_ignored: worktree
479 .root_entry()
480 .map_or(false, |entry| entry.is_ignored),
481 include_root_name,
482 candidates: project::Candidates::Files,
483 }
484 })
485 .collect::<Vec<_>>();
486
487 let search_id = util::post_inc(&mut self.search_count);
488 self.cancel_flag.store(true, atomic::Ordering::Relaxed);
489 self.cancel_flag = Arc::new(AtomicBool::new(false));
490 let cancel_flag = self.cancel_flag.clone();
491 cx.spawn(|picker, mut cx| async move {
492 let matches = fuzzy::match_path_sets(
493 candidate_sets.as_slice(),
494 query.path_like.path_query(),
495 relative_to,
496 false,
497 100,
498 &cancel_flag,
499 cx.background_executor().clone(),
500 )
501 .await
502 .into_iter()
503 .map(ProjectPanelOrdMatch);
504 let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
505 picker
506 .update(&mut cx, |picker, cx| {
507 picker
508 .delegate
509 .set_search_matches(search_id, did_cancel, query, matches, cx)
510 })
511 .log_err();
512 })
513 }
514
515 fn set_search_matches(
516 &mut self,
517 search_id: usize,
518 did_cancel: bool,
519 query: PathLikeWithPosition<FileSearchQuery>,
520 matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
521 cx: &mut ViewContext<Picker<Self>>,
522 ) {
523 if search_id >= self.latest_search_id {
524 self.latest_search_id = search_id;
525 let extend_old_matches = self.latest_search_did_cancel
526 && Some(query.path_like.path_query())
527 == self
528 .latest_search_query
529 .as_ref()
530 .map(|query| query.path_like.path_query());
531 self.matches.push_new_matches(
532 &self.history_items,
533 self.currently_opened_path.as_ref(),
534 Some(&query),
535 matches.into_iter(),
536 extend_old_matches,
537 );
538 self.latest_search_query = Some(query);
539 self.latest_search_did_cancel = did_cancel;
540 self.selected_index = self.calculate_selected_index();
541 cx.notify();
542 }
543 }
544
545 fn labels_for_match(
546 &self,
547 path_match: &Match,
548 cx: &AppContext,
549 ix: usize,
550 ) -> (String, Vec<usize>, String, Vec<usize>) {
551 let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
552 Match::History(found_path, found_path_match) => {
553 let worktree_id = found_path.project.worktree_id;
554 let project_relative_path = &found_path.project.path;
555 let has_worktree = self
556 .project
557 .read(cx)
558 .worktree_for_id(worktree_id, cx)
559 .is_some();
560
561 if !has_worktree {
562 if let Some(absolute_path) = &found_path.absolute {
563 return (
564 absolute_path
565 .file_name()
566 .map_or_else(
567 || project_relative_path.to_string_lossy(),
568 |file_name| file_name.to_string_lossy(),
569 )
570 .to_string(),
571 Vec::new(),
572 absolute_path.to_string_lossy().to_string(),
573 Vec::new(),
574 );
575 }
576 }
577
578 let mut path = Arc::clone(project_relative_path);
579 if project_relative_path.as_ref() == Path::new("") {
580 if let Some(absolute_path) = &found_path.absolute {
581 path = Arc::from(absolute_path.as_path());
582 }
583 }
584
585 let mut path_match = PathMatch {
586 score: ix as f64,
587 positions: Vec::new(),
588 worktree_id: worktree_id.to_usize(),
589 path,
590 path_prefix: "".into(),
591 distance_to_relative_ancestor: usize::MAX,
592 };
593 if let Some(found_path_match) = found_path_match {
594 path_match
595 .positions
596 .extend(found_path_match.0.positions.iter())
597 }
598
599 self.labels_for_path_match(&path_match)
600 }
601 Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
602 };
603
604 if file_name_positions.is_empty() {
605 if let Some(user_home_path) = std::env::var("HOME").ok() {
606 let user_home_path = user_home_path.trim();
607 if !user_home_path.is_empty() {
608 if (&full_path).starts_with(user_home_path) {
609 return (
610 file_name,
611 file_name_positions,
612 full_path.replace(user_home_path, "~"),
613 full_path_positions,
614 );
615 }
616 }
617 }
618 }
619
620 (
621 file_name,
622 file_name_positions,
623 full_path,
624 full_path_positions,
625 )
626 }
627
628 fn labels_for_path_match(
629 &self,
630 path_match: &PathMatch,
631 ) -> (String, Vec<usize>, String, Vec<usize>) {
632 let path = &path_match.path;
633 let path_string = path.to_string_lossy();
634 let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
635 let mut path_positions = path_match.positions.clone();
636
637 let file_name = path.file_name().map_or_else(
638 || path_match.path_prefix.to_string(),
639 |file_name| file_name.to_string_lossy().to_string(),
640 );
641 let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
642 let file_name_positions = path_positions
643 .iter()
644 .filter_map(|pos| {
645 if pos >= &file_name_start {
646 Some(pos - file_name_start)
647 } else {
648 None
649 }
650 })
651 .collect();
652
653 let full_path = full_path.trim_end_matches(&file_name).to_string();
654 path_positions.retain(|idx| *idx < full_path.len());
655
656 (file_name, file_name_positions, full_path, path_positions)
657 }
658
659 fn lookup_absolute_path(
660 &self,
661 query: PathLikeWithPosition<FileSearchQuery>,
662 cx: &mut ViewContext<'_, Picker<Self>>,
663 ) -> Task<()> {
664 cx.spawn(|picker, mut cx| async move {
665 let Some((project, fs)) = picker
666 .update(&mut cx, |picker, cx| {
667 let fs = Arc::clone(&picker.delegate.project.read(cx).fs());
668 (picker.delegate.project.clone(), fs)
669 })
670 .log_err()
671 else {
672 return;
673 };
674
675 let query_path = Path::new(query.path_like.path_query());
676 let mut path_matches = Vec::new();
677 match fs.metadata(query_path).await.log_err() {
678 Some(Some(_metadata)) => {
679 let update_result = project
680 .update(&mut cx, |project, cx| {
681 if let Some((worktree, relative_path)) =
682 project.find_worktree(query_path, cx)
683 {
684 path_matches.push(ProjectPanelOrdMatch(PathMatch {
685 score: 1.0,
686 positions: Vec::new(),
687 worktree_id: worktree.read(cx).id().to_usize(),
688 path: Arc::from(relative_path),
689 path_prefix: "".into(),
690 distance_to_relative_ancestor: usize::MAX,
691 }));
692 }
693 })
694 .log_err();
695 if update_result.is_none() {
696 return;
697 }
698 }
699 Some(None) => {}
700 None => return,
701 }
702
703 picker
704 .update(&mut cx, |picker, cx| {
705 let picker_delegate = &mut picker.delegate;
706 let search_id = util::post_inc(&mut picker_delegate.search_count);
707 picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
708
709 anyhow::Ok(())
710 })
711 .log_err();
712 })
713 }
714
715 /// Skips first history match (that is displayed topmost) if it's currently opened.
716 fn calculate_selected_index(&self) -> usize {
717 if let Some(Match::History(path, _)) = self.matches.get(0) {
718 if Some(path) == self.currently_opened_path.as_ref() {
719 let elements_after_first = self.matches.len() - 1;
720 if elements_after_first > 0 {
721 return 1;
722 }
723 }
724 }
725 0
726 }
727}
728
729impl PickerDelegate for FileFinderDelegate {
730 type ListItem = ListItem;
731
732 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
733 "Search project files...".into()
734 }
735
736 fn match_count(&self) -> usize {
737 self.matches.len()
738 }
739
740 fn selected_index(&self) -> usize {
741 self.selected_index
742 }
743
744 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
745 self.has_changed_selected_index = true;
746 self.selected_index = ix;
747 cx.notify();
748 }
749
750 fn separators_after_indices(&self) -> Vec<usize> {
751 if self.separate_history {
752 let first_non_history_index = self
753 .matches
754 .matches
755 .iter()
756 .enumerate()
757 .find(|(_, m)| !matches!(m, Match::History(_, _)))
758 .map(|(i, _)| i);
759 if let Some(first_non_history_index) = first_non_history_index {
760 if first_non_history_index > 0 {
761 return vec![first_non_history_index - 1];
762 }
763 }
764 }
765 Vec::new()
766 }
767
768 fn update_matches(
769 &mut self,
770 raw_query: String,
771 cx: &mut ViewContext<Picker<Self>>,
772 ) -> Task<()> {
773 let raw_query = raw_query.replace(' ', "");
774 let raw_query = raw_query.trim();
775 if raw_query.is_empty() {
776 let project = self.project.read(cx);
777 self.latest_search_id = post_inc(&mut self.search_count);
778 self.matches = Matches {
779 separate_history: self.separate_history,
780 ..Matches::default()
781 };
782 self.matches.push_new_matches(
783 self.history_items.iter().filter(|history_item| {
784 project
785 .worktree_for_id(history_item.project.worktree_id, cx)
786 .is_some()
787 || (project.is_local() && history_item.absolute.is_some())
788 }),
789 self.currently_opened_path.as_ref(),
790 None,
791 None.into_iter(),
792 false,
793 );
794
795 self.selected_index = 0;
796 cx.notify();
797 Task::ready(())
798 } else {
799 let query =
800 PathLikeWithPosition::parse_str(&raw_query, |normalized_query, path_like_str| {
801 Ok::<_, std::convert::Infallible>(FileSearchQuery {
802 raw_query: normalized_query.to_owned(),
803 file_query_end: if path_like_str == raw_query {
804 None
805 } else {
806 Some(path_like_str.len())
807 },
808 })
809 })
810 .expect("infallible");
811
812 if Path::new(query.path_like.path_query()).is_absolute() {
813 self.lookup_absolute_path(query, cx)
814 } else {
815 self.spawn_search(query, cx)
816 }
817 }
818 }
819
820 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
821 if let Some(m) = self.matches.get(self.selected_index()) {
822 if let Some(workspace) = self.workspace.upgrade() {
823 let open_task = workspace.update(cx, move |workspace, cx| {
824 let split_or_open =
825 |workspace: &mut Workspace,
826 project_path,
827 cx: &mut ViewContext<Workspace>| {
828 let allow_preview =
829 PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
830 if secondary {
831 workspace.split_path_preview(project_path, allow_preview, cx)
832 } else {
833 workspace.open_path_preview(
834 project_path,
835 None,
836 true,
837 allow_preview,
838 cx,
839 )
840 }
841 };
842 match m {
843 Match::History(history_match, _) => {
844 let worktree_id = history_match.project.worktree_id;
845 if workspace
846 .project()
847 .read(cx)
848 .worktree_for_id(worktree_id, cx)
849 .is_some()
850 {
851 split_or_open(
852 workspace,
853 ProjectPath {
854 worktree_id,
855 path: Arc::clone(&history_match.project.path),
856 },
857 cx,
858 )
859 } else {
860 match history_match.absolute.as_ref() {
861 Some(abs_path) => {
862 if secondary {
863 workspace.split_abs_path(
864 abs_path.to_path_buf(),
865 false,
866 cx,
867 )
868 } else {
869 workspace.open_abs_path(
870 abs_path.to_path_buf(),
871 false,
872 cx,
873 )
874 }
875 }
876 None => split_or_open(
877 workspace,
878 ProjectPath {
879 worktree_id,
880 path: Arc::clone(&history_match.project.path),
881 },
882 cx,
883 ),
884 }
885 }
886 }
887 Match::Search(m) => split_or_open(
888 workspace,
889 ProjectPath {
890 worktree_id: WorktreeId::from_usize(m.0.worktree_id),
891 path: m.0.path.clone(),
892 },
893 cx,
894 ),
895 }
896 });
897
898 let row = self
899 .latest_search_query
900 .as_ref()
901 .and_then(|query| query.row)
902 .map(|row| row.saturating_sub(1));
903 let col = self
904 .latest_search_query
905 .as_ref()
906 .and_then(|query| query.column)
907 .unwrap_or(0)
908 .saturating_sub(1);
909 let finder = self.file_finder.clone();
910
911 cx.spawn(|_, mut cx| async move {
912 let item = open_task.await.log_err()?;
913 if let Some(row) = row {
914 if let Some(active_editor) = item.downcast::<Editor>() {
915 active_editor
916 .downgrade()
917 .update(&mut cx, |editor, cx| {
918 let snapshot = editor.snapshot(cx).display_snapshot;
919 let point = snapshot
920 .buffer_snapshot
921 .clip_point(Point::new(row, col), Bias::Left);
922 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
923 s.select_ranges([point..point])
924 });
925 })
926 .log_err();
927 }
928 }
929 finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
930
931 Some(())
932 })
933 .detach();
934 }
935 }
936 }
937
938 fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
939 self.file_finder
940 .update(cx, |_, cx| cx.emit(DismissEvent))
941 .log_err();
942 }
943
944 fn render_match(
945 &self,
946 ix: usize,
947 selected: bool,
948 cx: &mut ViewContext<Picker<Self>>,
949 ) -> Option<Self::ListItem> {
950 let path_match = self
951 .matches
952 .get(ix)
953 .expect("Invalid matches state: no element for index {ix}");
954
955 let icon = match &path_match {
956 Match::History(_, _) => Icon::new(IconName::HistoryRerun)
957 .color(Color::Muted)
958 .size(IconSize::Small)
959 .into_any_element(),
960 Match::Search(_) => v_flex()
961 .flex_none()
962 .size(IconSize::Small.rems())
963 .into_any_element(),
964 };
965 let (file_name, file_name_positions, full_path, full_path_positions) =
966 self.labels_for_match(path_match, cx, ix);
967
968 Some(
969 ListItem::new(ix)
970 .spacing(ListItemSpacing::Sparse)
971 .end_slot::<AnyElement>(Some(icon))
972 .inset(true)
973 .selected(selected)
974 .child(
975 h_flex()
976 .gap_2()
977 .py_px()
978 .child(HighlightedLabel::new(file_name, file_name_positions))
979 .child(
980 HighlightedLabel::new(full_path, full_path_positions)
981 .size(LabelSize::Small)
982 .color(Color::Muted),
983 ),
984 ),
985 )
986 }
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992
993 #[test]
994 fn test_custom_project_search_ordering_in_file_finder() {
995 let mut file_finder_sorted_output = vec![
996 ProjectPanelOrdMatch(PathMatch {
997 score: 0.5,
998 positions: Vec::new(),
999 worktree_id: 0,
1000 path: Arc::from(Path::new("b0.5")),
1001 path_prefix: Arc::from(""),
1002 distance_to_relative_ancestor: 0,
1003 }),
1004 ProjectPanelOrdMatch(PathMatch {
1005 score: 1.0,
1006 positions: Vec::new(),
1007 worktree_id: 0,
1008 path: Arc::from(Path::new("c1.0")),
1009 path_prefix: Arc::from(""),
1010 distance_to_relative_ancestor: 0,
1011 }),
1012 ProjectPanelOrdMatch(PathMatch {
1013 score: 1.0,
1014 positions: Vec::new(),
1015 worktree_id: 0,
1016 path: Arc::from(Path::new("a1.0")),
1017 path_prefix: Arc::from(""),
1018 distance_to_relative_ancestor: 0,
1019 }),
1020 ProjectPanelOrdMatch(PathMatch {
1021 score: 0.5,
1022 positions: Vec::new(),
1023 worktree_id: 0,
1024 path: Arc::from(Path::new("a0.5")),
1025 path_prefix: Arc::from(""),
1026 distance_to_relative_ancestor: 0,
1027 }),
1028 ProjectPanelOrdMatch(PathMatch {
1029 score: 1.0,
1030 positions: Vec::new(),
1031 worktree_id: 0,
1032 path: Arc::from(Path::new("b1.0")),
1033 path_prefix: Arc::from(""),
1034 distance_to_relative_ancestor: 0,
1035 }),
1036 ];
1037 file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
1038
1039 assert_eq!(
1040 file_finder_sorted_output,
1041 vec![
1042 ProjectPanelOrdMatch(PathMatch {
1043 score: 1.0,
1044 positions: Vec::new(),
1045 worktree_id: 0,
1046 path: Arc::from(Path::new("a1.0")),
1047 path_prefix: Arc::from(""),
1048 distance_to_relative_ancestor: 0,
1049 }),
1050 ProjectPanelOrdMatch(PathMatch {
1051 score: 1.0,
1052 positions: Vec::new(),
1053 worktree_id: 0,
1054 path: Arc::from(Path::new("b1.0")),
1055 path_prefix: Arc::from(""),
1056 distance_to_relative_ancestor: 0,
1057 }),
1058 ProjectPanelOrdMatch(PathMatch {
1059 score: 1.0,
1060 positions: Vec::new(),
1061 worktree_id: 0,
1062 path: Arc::from(Path::new("c1.0")),
1063 path_prefix: Arc::from(""),
1064 distance_to_relative_ancestor: 0,
1065 }),
1066 ProjectPanelOrdMatch(PathMatch {
1067 score: 0.5,
1068 positions: Vec::new(),
1069 worktree_id: 0,
1070 path: Arc::from(Path::new("a0.5")),
1071 path_prefix: Arc::from(""),
1072 distance_to_relative_ancestor: 0,
1073 }),
1074 ProjectPanelOrdMatch(PathMatch {
1075 score: 0.5,
1076 positions: Vec::new(),
1077 worktree_id: 0,
1078 path: Arc::from(Path::new("b0.5")),
1079 path_prefix: Arc::from(""),
1080 distance_to_relative_ancestor: 0,
1081 }),
1082 ]
1083 );
1084 }
1085}