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