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 is_dir: false, // You can't open directories as project items
322 path: &found_path.project.path,
323 // Only match history items names, otherwise their paths may match too many queries, producing false positives.
324 // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
325 // it would be shown first always, despite the latter being a better match.
326 char_bag: CharBag::from_iter(
327 found_path
328 .project
329 .path
330 .file_name()?
331 .to_string_lossy()
332 .to_lowercase()
333 .chars(),
334 ),
335 };
336 Some((found_path.project.worktree_id, candidate))
337 })
338 .fold(
339 HashMap::default(),
340 |mut candidates, (worktree_id, new_candidate)| {
341 candidates
342 .entry(worktree_id)
343 .or_insert_with(Vec::new)
344 .push(new_candidate);
345 candidates
346 },
347 );
348 let mut matching_history_paths = HashMap::default();
349 for (worktree, candidates) in history_items_by_worktrees {
350 let max_results = candidates.len() + 1;
351 matching_history_paths.extend(
352 fuzzy::match_fixed_path_set(
353 candidates,
354 worktree.to_usize(),
355 query.path_query(),
356 false,
357 max_results,
358 )
359 .into_iter()
360 .map(|path_match| {
361 (
362 Arc::clone(&path_match.path),
363 Some(ProjectPanelOrdMatch(path_match)),
364 )
365 }),
366 );
367 }
368 matching_history_paths
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
372struct FoundPath {
373 project: ProjectPath,
374 absolute: Option<PathBuf>,
375}
376
377impl FoundPath {
378 fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
379 Self { project, absolute }
380 }
381}
382
383const MAX_RECENT_SELECTIONS: usize = 20;
384
385#[cfg(not(test))]
386fn history_file_exists(abs_path: &PathBuf) -> bool {
387 abs_path.exists()
388}
389
390#[cfg(test)]
391fn history_file_exists(abs_path: &PathBuf) -> bool {
392 !abs_path.ends_with("nonexistent.rs")
393}
394
395pub enum Event {
396 Selected(ProjectPath),
397 Dismissed,
398}
399
400#[derive(Debug, Clone)]
401struct FileSearchQuery {
402 raw_query: String,
403 file_query_end: Option<usize>,
404 path_position: PathWithPosition,
405}
406
407impl FileSearchQuery {
408 fn path_query(&self) -> &str {
409 match self.file_query_end {
410 Some(file_path_end) => &self.raw_query[..file_path_end],
411 None => &self.raw_query,
412 }
413 }
414}
415
416impl FileFinderDelegate {
417 fn new(
418 file_finder: WeakView<FileFinder>,
419 workspace: WeakView<Workspace>,
420 project: Model<Project>,
421 currently_opened_path: Option<FoundPath>,
422 history_items: Vec<FoundPath>,
423 separate_history: bool,
424 cx: &mut ViewContext<FileFinder>,
425 ) -> Self {
426 Self::subscribe_to_updates(&project, cx);
427 Self {
428 file_finder,
429 workspace,
430 project,
431 search_count: 0,
432 latest_search_id: 0,
433 latest_search_did_cancel: false,
434 latest_search_query: None,
435 currently_opened_path,
436 matches: Matches::default(),
437 has_changed_selected_index: false,
438 selected_index: 0,
439 cancel_flag: Arc::new(AtomicBool::new(false)),
440 history_items,
441 separate_history,
442 }
443 }
444
445 fn subscribe_to_updates(project: &Model<Project>, cx: &mut ViewContext<FileFinder>) {
446 cx.subscribe(project, |file_finder, _, event, cx| {
447 match event {
448 project::Event::WorktreeUpdatedEntries(_, _)
449 | project::Event::WorktreeAdded
450 | project::Event::WorktreeRemoved(_) => file_finder
451 .picker
452 .update(cx, |picker, cx| picker.refresh(cx)),
453 _ => {}
454 };
455 })
456 .detach();
457 }
458
459 fn spawn_search(
460 &mut self,
461 query: FileSearchQuery,
462 cx: &mut ViewContext<Picker<Self>>,
463 ) -> Task<()> {
464 let relative_to = self
465 .currently_opened_path
466 .as_ref()
467 .map(|found_path| Arc::clone(&found_path.project.path));
468 let worktrees = self
469 .project
470 .read(cx)
471 .visible_worktrees(cx)
472 .collect::<Vec<_>>();
473 let include_root_name = worktrees.len() > 1;
474 let candidate_sets = worktrees
475 .into_iter()
476 .map(|worktree| {
477 let worktree = worktree.read(cx);
478 PathMatchCandidateSet {
479 snapshot: worktree.snapshot(),
480 include_ignored: worktree
481 .root_entry()
482 .map_or(false, |entry| entry.is_ignored),
483 include_root_name,
484 candidates: project::Candidates::Files,
485 }
486 })
487 .collect::<Vec<_>>();
488
489 let search_id = util::post_inc(&mut self.search_count);
490 self.cancel_flag.store(true, atomic::Ordering::Relaxed);
491 self.cancel_flag = Arc::new(AtomicBool::new(false));
492 let cancel_flag = self.cancel_flag.clone();
493 cx.spawn(|picker, mut cx| async move {
494 let matches = fuzzy::match_path_sets(
495 candidate_sets.as_slice(),
496 query.path_query(),
497 relative_to,
498 false,
499 100,
500 &cancel_flag,
501 cx.background_executor().clone(),
502 )
503 .await
504 .into_iter()
505 .map(ProjectPanelOrdMatch);
506 let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
507 picker
508 .update(&mut cx, |picker, cx| {
509 picker
510 .delegate
511 .set_search_matches(search_id, did_cancel, query, matches, cx)
512 })
513 .log_err();
514 })
515 }
516
517 fn set_search_matches(
518 &mut self,
519 search_id: usize,
520 did_cancel: bool,
521 query: FileSearchQuery,
522 matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
523 cx: &mut ViewContext<Picker<Self>>,
524 ) {
525 if search_id >= self.latest_search_id {
526 self.latest_search_id = search_id;
527 let extend_old_matches = self.latest_search_did_cancel
528 && Some(query.path_query())
529 == self
530 .latest_search_query
531 .as_ref()
532 .map(|query| query.path_query());
533 self.matches.push_new_matches(
534 &self.history_items,
535 self.currently_opened_path.as_ref(),
536 Some(&query),
537 matches.into_iter(),
538 extend_old_matches,
539 );
540 self.latest_search_query = Some(query);
541 self.latest_search_did_cancel = did_cancel;
542 self.selected_index = self.calculate_selected_index();
543 cx.notify();
544 }
545 }
546
547 fn labels_for_match(
548 &self,
549 path_match: &Match,
550 cx: &AppContext,
551 ix: usize,
552 ) -> (String, Vec<usize>, String, Vec<usize>) {
553 let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
554 Match::History(found_path, found_path_match) => {
555 let worktree_id = found_path.project.worktree_id;
556 let project_relative_path = &found_path.project.path;
557 let has_worktree = self
558 .project
559 .read(cx)
560 .worktree_for_id(worktree_id, cx)
561 .is_some();
562
563 if !has_worktree {
564 if let Some(absolute_path) = &found_path.absolute {
565 return (
566 absolute_path
567 .file_name()
568 .map_or_else(
569 || project_relative_path.to_string_lossy(),
570 |file_name| file_name.to_string_lossy(),
571 )
572 .to_string(),
573 Vec::new(),
574 absolute_path.to_string_lossy().to_string(),
575 Vec::new(),
576 );
577 }
578 }
579
580 let mut path = Arc::clone(project_relative_path);
581 if project_relative_path.as_ref() == Path::new("") {
582 if let Some(absolute_path) = &found_path.absolute {
583 path = Arc::from(absolute_path.as_path());
584 }
585 }
586
587 let mut path_match = PathMatch {
588 score: ix as f64,
589 positions: Vec::new(),
590 worktree_id: worktree_id.to_usize(),
591 path,
592 is_dir: false, // File finder doesn't support directories
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: 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_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_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 is_dir: false, // File finder doesn't support directories
694 distance_to_relative_ancestor: usize::MAX,
695 }));
696 }
697 })
698 .log_err();
699 if update_result.is_none() {
700 return;
701 }
702 }
703 Some(None) => {}
704 None => return,
705 }
706
707 picker
708 .update(&mut cx, |picker, cx| {
709 let picker_delegate = &mut picker.delegate;
710 let search_id = util::post_inc(&mut picker_delegate.search_count);
711 picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
712
713 anyhow::Ok(())
714 })
715 .log_err();
716 })
717 }
718
719 /// Skips first history match (that is displayed topmost) if it's currently opened.
720 fn calculate_selected_index(&self) -> usize {
721 if let Some(Match::History(path, _)) = self.matches.get(0) {
722 if Some(path) == self.currently_opened_path.as_ref() {
723 let elements_after_first = self.matches.len() - 1;
724 if elements_after_first > 0 {
725 return 1;
726 }
727 }
728 }
729 0
730 }
731}
732
733impl PickerDelegate for FileFinderDelegate {
734 type ListItem = ListItem;
735
736 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
737 "Search project files...".into()
738 }
739
740 fn match_count(&self) -> usize {
741 self.matches.len()
742 }
743
744 fn selected_index(&self) -> usize {
745 self.selected_index
746 }
747
748 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
749 self.has_changed_selected_index = true;
750 self.selected_index = ix;
751 cx.notify();
752 }
753
754 fn separators_after_indices(&self) -> Vec<usize> {
755 if self.separate_history {
756 let first_non_history_index = self
757 .matches
758 .matches
759 .iter()
760 .enumerate()
761 .find(|(_, m)| !matches!(m, Match::History(_, _)))
762 .map(|(i, _)| i);
763 if let Some(first_non_history_index) = first_non_history_index {
764 if first_non_history_index > 0 {
765 return vec![first_non_history_index - 1];
766 }
767 }
768 }
769 Vec::new()
770 }
771
772 fn update_matches(
773 &mut self,
774 raw_query: String,
775 cx: &mut ViewContext<Picker<Self>>,
776 ) -> Task<()> {
777 let raw_query = raw_query.replace(' ', "");
778 let raw_query = raw_query.trim();
779 if raw_query.is_empty() {
780 let project = self.project.read(cx);
781 self.latest_search_id = post_inc(&mut self.search_count);
782 self.matches = Matches {
783 separate_history: self.separate_history,
784 ..Matches::default()
785 };
786 self.matches.push_new_matches(
787 self.history_items.iter().filter(|history_item| {
788 project
789 .worktree_for_id(history_item.project.worktree_id, cx)
790 .is_some()
791 || (project.is_local() && history_item.absolute.is_some())
792 }),
793 self.currently_opened_path.as_ref(),
794 None,
795 None.into_iter(),
796 false,
797 );
798
799 self.selected_index = 0;
800 cx.notify();
801 Task::ready(())
802 } else {
803 let path_position = PathWithPosition::parse_str(&raw_query);
804
805 let query = FileSearchQuery {
806 raw_query: raw_query.trim().to_owned(),
807 file_query_end: if path_position.path.to_str().unwrap_or(raw_query) == raw_query {
808 None
809 } else {
810 // Safe to unwrap as we won't get here when the unwrap in if fails
811 Some(path_position.path.to_str().unwrap().len())
812 },
813 path_position,
814 };
815
816 if Path::new(query.path_query()).is_absolute() {
817 self.lookup_absolute_path(query, cx)
818 } else {
819 self.spawn_search(query, cx)
820 }
821 }
822 }
823
824 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
825 if let Some(m) = self.matches.get(self.selected_index()) {
826 if let Some(workspace) = self.workspace.upgrade() {
827 let open_task = workspace.update(cx, move |workspace, cx| {
828 let split_or_open =
829 |workspace: &mut Workspace,
830 project_path,
831 cx: &mut ViewContext<Workspace>| {
832 let allow_preview =
833 PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
834 if secondary {
835 workspace.split_path_preview(project_path, allow_preview, cx)
836 } else {
837 workspace.open_path_preview(
838 project_path,
839 None,
840 true,
841 allow_preview,
842 cx,
843 )
844 }
845 };
846 match m {
847 Match::History(history_match, _) => {
848 let worktree_id = history_match.project.worktree_id;
849 if workspace
850 .project()
851 .read(cx)
852 .worktree_for_id(worktree_id, cx)
853 .is_some()
854 {
855 split_or_open(
856 workspace,
857 ProjectPath {
858 worktree_id,
859 path: Arc::clone(&history_match.project.path),
860 },
861 cx,
862 )
863 } else {
864 match history_match.absolute.as_ref() {
865 Some(abs_path) => {
866 if secondary {
867 workspace.split_abs_path(
868 abs_path.to_path_buf(),
869 false,
870 cx,
871 )
872 } else {
873 workspace.open_abs_path(
874 abs_path.to_path_buf(),
875 false,
876 cx,
877 )
878 }
879 }
880 None => split_or_open(
881 workspace,
882 ProjectPath {
883 worktree_id,
884 path: Arc::clone(&history_match.project.path),
885 },
886 cx,
887 ),
888 }
889 }
890 }
891 Match::Search(m) => split_or_open(
892 workspace,
893 ProjectPath {
894 worktree_id: WorktreeId::from_usize(m.0.worktree_id),
895 path: m.0.path.clone(),
896 },
897 cx,
898 ),
899 }
900 });
901
902 let row = self
903 .latest_search_query
904 .as_ref()
905 .and_then(|query| query.path_position.row)
906 .map(|row| row.saturating_sub(1));
907 let col = self
908 .latest_search_query
909 .as_ref()
910 .and_then(|query| query.path_position.column)
911 .unwrap_or(0)
912 .saturating_sub(1);
913 let finder = self.file_finder.clone();
914
915 cx.spawn(|_, mut cx| async move {
916 let item = open_task.await.log_err()?;
917 if let Some(row) = row {
918 if let Some(active_editor) = item.downcast::<Editor>() {
919 active_editor
920 .downgrade()
921 .update(&mut cx, |editor, cx| {
922 let snapshot = editor.snapshot(cx).display_snapshot;
923 let point = snapshot
924 .buffer_snapshot
925 .clip_point(Point::new(row, col), Bias::Left);
926 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
927 s.select_ranges([point..point])
928 });
929 })
930 .log_err();
931 }
932 }
933 finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
934
935 Some(())
936 })
937 .detach();
938 }
939 }
940 }
941
942 fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
943 self.file_finder
944 .update(cx, |_, cx| cx.emit(DismissEvent))
945 .log_err();
946 }
947
948 fn render_match(
949 &self,
950 ix: usize,
951 selected: bool,
952 cx: &mut ViewContext<Picker<Self>>,
953 ) -> Option<Self::ListItem> {
954 let path_match = self
955 .matches
956 .get(ix)
957 .expect("Invalid matches state: no element for index {ix}");
958
959 let icon = match &path_match {
960 Match::History(_, _) => Icon::new(IconName::HistoryRerun)
961 .color(Color::Muted)
962 .size(IconSize::Small)
963 .into_any_element(),
964 Match::Search(_) => v_flex()
965 .flex_none()
966 .size(IconSize::Small.rems())
967 .into_any_element(),
968 };
969 let (file_name, file_name_positions, full_path, full_path_positions) =
970 self.labels_for_match(path_match, cx, ix);
971
972 Some(
973 ListItem::new(ix)
974 .spacing(ListItemSpacing::Sparse)
975 .end_slot::<AnyElement>(Some(icon))
976 .inset(true)
977 .selected(selected)
978 .child(
979 h_flex()
980 .gap_2()
981 .py_px()
982 .child(HighlightedLabel::new(file_name, file_name_positions))
983 .child(
984 HighlightedLabel::new(full_path, full_path_positions)
985 .size(LabelSize::Small)
986 .color(Color::Muted),
987 ),
988 ),
989 )
990 }
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996
997 #[test]
998 fn test_custom_project_search_ordering_in_file_finder() {
999 let mut file_finder_sorted_output = vec![
1000 ProjectPanelOrdMatch(PathMatch {
1001 score: 0.5,
1002 positions: Vec::new(),
1003 worktree_id: 0,
1004 path: Arc::from(Path::new("b0.5")),
1005 path_prefix: Arc::default(),
1006 distance_to_relative_ancestor: 0,
1007 is_dir: false,
1008 }),
1009 ProjectPanelOrdMatch(PathMatch {
1010 score: 1.0,
1011 positions: Vec::new(),
1012 worktree_id: 0,
1013 path: Arc::from(Path::new("c1.0")),
1014 path_prefix: Arc::default(),
1015 distance_to_relative_ancestor: 0,
1016 is_dir: false,
1017 }),
1018 ProjectPanelOrdMatch(PathMatch {
1019 score: 1.0,
1020 positions: Vec::new(),
1021 worktree_id: 0,
1022 path: Arc::from(Path::new("a1.0")),
1023 path_prefix: Arc::default(),
1024 distance_to_relative_ancestor: 0,
1025 is_dir: false,
1026 }),
1027 ProjectPanelOrdMatch(PathMatch {
1028 score: 0.5,
1029 positions: Vec::new(),
1030 worktree_id: 0,
1031 path: Arc::from(Path::new("a0.5")),
1032 path_prefix: Arc::default(),
1033 distance_to_relative_ancestor: 0,
1034 is_dir: false,
1035 }),
1036 ProjectPanelOrdMatch(PathMatch {
1037 score: 1.0,
1038 positions: Vec::new(),
1039 worktree_id: 0,
1040 path: Arc::from(Path::new("b1.0")),
1041 path_prefix: Arc::default(),
1042 distance_to_relative_ancestor: 0,
1043 is_dir: false,
1044 }),
1045 ];
1046 file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
1047
1048 assert_eq!(
1049 file_finder_sorted_output,
1050 vec![
1051 ProjectPanelOrdMatch(PathMatch {
1052 score: 1.0,
1053 positions: Vec::new(),
1054 worktree_id: 0,
1055 path: Arc::from(Path::new("a1.0")),
1056 path_prefix: Arc::default(),
1057 distance_to_relative_ancestor: 0,
1058 is_dir: false,
1059 }),
1060 ProjectPanelOrdMatch(PathMatch {
1061 score: 1.0,
1062 positions: Vec::new(),
1063 worktree_id: 0,
1064 path: Arc::from(Path::new("b1.0")),
1065 path_prefix: Arc::default(),
1066 distance_to_relative_ancestor: 0,
1067 is_dir: false,
1068 }),
1069 ProjectPanelOrdMatch(PathMatch {
1070 score: 1.0,
1071 positions: Vec::new(),
1072 worktree_id: 0,
1073 path: Arc::from(Path::new("c1.0")),
1074 path_prefix: Arc::default(),
1075 distance_to_relative_ancestor: 0,
1076 is_dir: false,
1077 }),
1078 ProjectPanelOrdMatch(PathMatch {
1079 score: 0.5,
1080 positions: Vec::new(),
1081 worktree_id: 0,
1082 path: Arc::from(Path::new("a0.5")),
1083 path_prefix: Arc::default(),
1084 distance_to_relative_ancestor: 0,
1085 is_dir: false,
1086 }),
1087 ProjectPanelOrdMatch(PathMatch {
1088 score: 0.5,
1089 positions: Vec::new(),
1090 worktree_id: 0,
1091 path: Arc::from(Path::new("b0.5")),
1092 path_prefix: Arc::default(),
1093 distance_to_relative_ancestor: 0,
1094 is_dir: false,
1095 }),
1096 ]
1097 );
1098 }
1099}