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