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)) => {
289 match_b.cmp(match_a)
290 }
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 .then(history_score_a.cmp(history_score_b))
300 })
301 .take(100)
302 .map(|(_, m)| m)
303 .collect();
304 }
305}
306
307fn matching_history_item_paths<'a>(
308 history_items: impl IntoIterator<Item = &'a FoundPath>,
309 currently_opened: Option<&'a FoundPath>,
310 query: Option<&PathLikeWithPosition<FileSearchQuery>>,
311) -> HashMap<Arc<Path>, Option<ProjectPanelOrdMatch>> {
312 let Some(query) = query else {
313 return history_items
314 .into_iter()
315 .chain(currently_opened)
316 .map(|found_path| (Arc::clone(&found_path.project.path), None))
317 .collect();
318 };
319
320 let history_items_by_worktrees = history_items
321 .into_iter()
322 .chain(currently_opened)
323 .filter_map(|found_path| {
324 let candidate = PathMatchCandidate {
325 path: &found_path.project.path,
326 // Only match history items names, otherwise their paths may match too many queries, producing false positives.
327 // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
328 // it would be shown first always, despite the latter being a better match.
329 char_bag: CharBag::from_iter(
330 found_path
331 .project
332 .path
333 .file_name()?
334 .to_string_lossy()
335 .to_lowercase()
336 .chars(),
337 ),
338 };
339 Some((found_path.project.worktree_id, candidate))
340 })
341 .fold(
342 HashMap::default(),
343 |mut candidates, (worktree_id, new_candidate)| {
344 candidates
345 .entry(worktree_id)
346 .or_insert_with(Vec::new)
347 .push(new_candidate);
348 candidates
349 },
350 );
351 let mut matching_history_paths = HashMap::default();
352 for (worktree, candidates) in history_items_by_worktrees {
353 let max_results = candidates.len() + 1;
354 matching_history_paths.extend(
355 fuzzy::match_fixed_path_set(
356 candidates,
357 worktree.to_usize(),
358 query.path_like.path_query(),
359 false,
360 max_results,
361 )
362 .into_iter()
363 .map(|path_match| {
364 (
365 Arc::clone(&path_match.path),
366 Some(ProjectPanelOrdMatch(path_match)),
367 )
368 }),
369 );
370 }
371 matching_history_paths
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
375struct FoundPath {
376 project: ProjectPath,
377 absolute: Option<PathBuf>,
378}
379
380impl FoundPath {
381 fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
382 Self { project, absolute }
383 }
384}
385
386const MAX_RECENT_SELECTIONS: usize = 20;
387
388#[cfg(not(test))]
389fn history_file_exists(abs_path: &PathBuf) -> bool {
390 abs_path.exists()
391}
392
393#[cfg(test)]
394fn history_file_exists(abs_path: &PathBuf) -> bool {
395 !abs_path.ends_with("nonexistent.rs")
396}
397
398pub enum Event {
399 Selected(ProjectPath),
400 Dismissed,
401}
402
403#[derive(Debug, Clone)]
404struct FileSearchQuery {
405 raw_query: String,
406 file_query_end: Option<usize>,
407}
408
409impl FileSearchQuery {
410 fn path_query(&self) -> &str {
411 match self.file_query_end {
412 Some(file_path_end) => &self.raw_query[..file_path_end],
413 None => &self.raw_query,
414 }
415 }
416}
417
418impl FileFinderDelegate {
419 fn new(
420 file_finder: WeakView<FileFinder>,
421 workspace: WeakView<Workspace>,
422 project: Model<Project>,
423 currently_opened_path: Option<FoundPath>,
424 history_items: Vec<FoundPath>,
425 separate_history: bool,
426 cx: &mut ViewContext<FileFinder>,
427 ) -> Self {
428 Self::subscribe_to_updates(&project, cx);
429 Self {
430 file_finder,
431 workspace,
432 project,
433 search_count: 0,
434 latest_search_id: 0,
435 latest_search_did_cancel: false,
436 latest_search_query: None,
437 currently_opened_path,
438 matches: Matches::default(),
439 has_changed_selected_index: false,
440 selected_index: 0,
441 cancel_flag: Arc::new(AtomicBool::new(false)),
442 history_items,
443 separate_history,
444 }
445 }
446
447 fn subscribe_to_updates(project: &Model<Project>, cx: &mut ViewContext<FileFinder>) {
448 cx.subscribe(project, |file_finder, _, event, cx| {
449 match event {
450 project::Event::WorktreeUpdatedEntries(_, _)
451 | project::Event::WorktreeAdded
452 | project::Event::WorktreeRemoved(_) => file_finder
453 .picker
454 .update(cx, |picker, cx| picker.refresh(cx)),
455 _ => {}
456 };
457 })
458 .detach();
459 }
460
461 fn spawn_search(
462 &mut self,
463 query: PathLikeWithPosition<FileSearchQuery>,
464 cx: &mut ViewContext<Picker<Self>>,
465 ) -> Task<()> {
466 let relative_to = self
467 .currently_opened_path
468 .as_ref()
469 .map(|found_path| Arc::clone(&found_path.project.path));
470 let worktrees = self
471 .project
472 .read(cx)
473 .visible_worktrees(cx)
474 .collect::<Vec<_>>();
475 let include_root_name = worktrees.len() > 1;
476 let candidate_sets = worktrees
477 .into_iter()
478 .map(|worktree| {
479 let worktree = worktree.read(cx);
480 PathMatchCandidateSet {
481 snapshot: worktree.snapshot(),
482 include_ignored: worktree
483 .root_entry()
484 .map_or(false, |entry| entry.is_ignored),
485 include_root_name,
486 directories_only: false,
487 }
488 })
489 .collect::<Vec<_>>();
490
491 let search_id = util::post_inc(&mut self.search_count);
492 self.cancel_flag.store(true, atomic::Ordering::Relaxed);
493 self.cancel_flag = Arc::new(AtomicBool::new(false));
494 let cancel_flag = self.cancel_flag.clone();
495 cx.spawn(|picker, mut cx| async move {
496 let matches = fuzzy::match_path_sets(
497 candidate_sets.as_slice(),
498 query.path_like.path_query(),
499 relative_to,
500 false,
501 100,
502 &cancel_flag,
503 cx.background_executor().clone(),
504 )
505 .await
506 .into_iter()
507 .map(ProjectPanelOrdMatch);
508 let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
509 picker
510 .update(&mut cx, |picker, cx| {
511 picker
512 .delegate
513 .set_search_matches(search_id, did_cancel, query, matches, cx)
514 })
515 .log_err();
516 })
517 }
518
519 fn set_search_matches(
520 &mut self,
521 search_id: usize,
522 did_cancel: bool,
523 query: PathLikeWithPosition<FileSearchQuery>,
524 matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
525 cx: &mut ViewContext<Picker<Self>>,
526 ) {
527 if search_id >= self.latest_search_id {
528 self.latest_search_id = search_id;
529 let extend_old_matches = self.latest_search_did_cancel
530 && Some(query.path_like.path_query())
531 == self
532 .latest_search_query
533 .as_ref()
534 .map(|query| query.path_like.path_query());
535 self.matches.push_new_matches(
536 &self.history_items,
537 self.currently_opened_path.as_ref(),
538 Some(&query),
539 matches.into_iter(),
540 extend_old_matches,
541 );
542 self.latest_search_query = Some(query);
543 self.latest_search_did_cancel = did_cancel;
544 self.selected_index = self.calculate_selected_index();
545 cx.notify();
546 }
547 }
548
549 fn labels_for_match(
550 &self,
551 path_match: &Match,
552 cx: &AppContext,
553 ix: usize,
554 ) -> (String, Vec<usize>, String, Vec<usize>) {
555 let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
556 Match::History(found_path, found_path_match) => {
557 let worktree_id = found_path.project.worktree_id;
558 let project_relative_path = &found_path.project.path;
559 let has_worktree = self
560 .project
561 .read(cx)
562 .worktree_for_id(worktree_id, cx)
563 .is_some();
564
565 if !has_worktree {
566 if let Some(absolute_path) = &found_path.absolute {
567 return (
568 absolute_path
569 .file_name()
570 .map_or_else(
571 || project_relative_path.to_string_lossy(),
572 |file_name| file_name.to_string_lossy(),
573 )
574 .to_string(),
575 Vec::new(),
576 absolute_path.to_string_lossy().to_string(),
577 Vec::new(),
578 );
579 }
580 }
581
582 let mut path = Arc::clone(project_relative_path);
583 if project_relative_path.as_ref() == Path::new("") {
584 if let Some(absolute_path) = &found_path.absolute {
585 path = Arc::from(absolute_path.as_path());
586 }
587 }
588
589 let mut path_match = PathMatch {
590 score: ix as f64,
591 positions: Vec::new(),
592 worktree_id: worktree_id.to_usize(),
593 path,
594 path_prefix: "".into(),
595 distance_to_relative_ancestor: usize::MAX,
596 };
597 if let Some(found_path_match) = found_path_match {
598 path_match
599 .positions
600 .extend(found_path_match.0.positions.iter())
601 }
602
603 self.labels_for_path_match(&path_match)
604 }
605 Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
606 };
607
608 if file_name_positions.is_empty() {
609 if let Some(user_home_path) = std::env::var("HOME").ok() {
610 let user_home_path = user_home_path.trim();
611 if !user_home_path.is_empty() {
612 if (&full_path).starts_with(user_home_path) {
613 return (
614 file_name,
615 file_name_positions,
616 full_path.replace(user_home_path, "~"),
617 full_path_positions,
618 );
619 }
620 }
621 }
622 }
623
624 (
625 file_name,
626 file_name_positions,
627 full_path,
628 full_path_positions,
629 )
630 }
631
632 fn labels_for_path_match(
633 &self,
634 path_match: &PathMatch,
635 ) -> (String, Vec<usize>, String, Vec<usize>) {
636 let path = &path_match.path;
637 let path_string = path.to_string_lossy();
638 let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
639 let mut path_positions = path_match.positions.clone();
640
641 let file_name = path.file_name().map_or_else(
642 || path_match.path_prefix.to_string(),
643 |file_name| file_name.to_string_lossy().to_string(),
644 );
645 let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
646 let file_name_positions = path_positions
647 .iter()
648 .filter_map(|pos| {
649 if pos >= &file_name_start {
650 Some(pos - file_name_start)
651 } else {
652 None
653 }
654 })
655 .collect();
656
657 let full_path = full_path.trim_end_matches(&file_name).to_string();
658 path_positions.retain(|idx| *idx < full_path.len());
659
660 (file_name, file_name_positions, full_path, path_positions)
661 }
662
663 fn lookup_absolute_path(
664 &self,
665 query: PathLikeWithPosition<FileSearchQuery>,
666 cx: &mut ViewContext<'_, Picker<Self>>,
667 ) -> Task<()> {
668 cx.spawn(|picker, mut cx| async move {
669 let Some((project, fs)) = picker
670 .update(&mut cx, |picker, cx| {
671 let fs = Arc::clone(&picker.delegate.project.read(cx).fs());
672 (picker.delegate.project.clone(), fs)
673 })
674 .log_err()
675 else {
676 return;
677 };
678
679 let query_path = Path::new(query.path_like.path_query());
680 let mut path_matches = Vec::new();
681 match fs.metadata(query_path).await.log_err() {
682 Some(Some(_metadata)) => {
683 let update_result = project
684 .update(&mut cx, |project, cx| {
685 if let Some((worktree, relative_path)) =
686 project.find_local_worktree(query_path, cx)
687 {
688 path_matches.push(ProjectPanelOrdMatch(PathMatch {
689 score: 1.0,
690 positions: Vec::new(),
691 worktree_id: worktree.read(cx).id().to_usize(),
692 path: Arc::from(relative_path),
693 path_prefix: "".into(),
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 query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
804 Ok::<_, std::convert::Infallible>(FileSearchQuery {
805 raw_query: raw_query.to_owned(),
806 file_query_end: if path_like_str == raw_query {
807 None
808 } else {
809 Some(path_like_str.len())
810 },
811 })
812 })
813 .expect("infallible");
814
815 if Path::new(query.path_like.path_query()).is_absolute() {
816 self.lookup_absolute_path(query, cx)
817 } else {
818 self.spawn_search(query, cx)
819 }
820 }
821 }
822
823 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
824 if let Some(m) = self.matches.get(self.selected_index()) {
825 if let Some(workspace) = self.workspace.upgrade() {
826 let open_task = workspace.update(cx, move |workspace, cx| {
827 let split_or_open =
828 |workspace: &mut Workspace,
829 project_path,
830 cx: &mut ViewContext<Workspace>| {
831 let allow_preview =
832 PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
833 if secondary {
834 workspace.split_path_preview(project_path, allow_preview, cx)
835 } else {
836 workspace.open_path_preview(
837 project_path,
838 None,
839 true,
840 allow_preview,
841 cx,
842 )
843 }
844 };
845 match m {
846 Match::History(history_match, _) => {
847 let worktree_id = history_match.project.worktree_id;
848 if workspace
849 .project()
850 .read(cx)
851 .worktree_for_id(worktree_id, cx)
852 .is_some()
853 {
854 split_or_open(
855 workspace,
856 ProjectPath {
857 worktree_id,
858 path: Arc::clone(&history_match.project.path),
859 },
860 cx,
861 )
862 } else {
863 match history_match.absolute.as_ref() {
864 Some(abs_path) => {
865 if secondary {
866 workspace.split_abs_path(
867 abs_path.to_path_buf(),
868 false,
869 cx,
870 )
871 } else {
872 workspace.open_abs_path(
873 abs_path.to_path_buf(),
874 false,
875 cx,
876 )
877 }
878 }
879 None => split_or_open(
880 workspace,
881 ProjectPath {
882 worktree_id,
883 path: Arc::clone(&history_match.project.path),
884 },
885 cx,
886 ),
887 }
888 }
889 }
890 Match::Search(m) => split_or_open(
891 workspace,
892 ProjectPath {
893 worktree_id: WorktreeId::from_usize(m.0.worktree_id),
894 path: m.0.path.clone(),
895 },
896 cx,
897 ),
898 }
899 });
900
901 let row = self
902 .latest_search_query
903 .as_ref()
904 .and_then(|query| query.row)
905 .map(|row| row.saturating_sub(1));
906 let col = self
907 .latest_search_query
908 .as_ref()
909 .and_then(|query| query.column)
910 .unwrap_or(0)
911 .saturating_sub(1);
912 let finder = self.file_finder.clone();
913
914 cx.spawn(|_, mut cx| async move {
915 let item = open_task.await.log_err()?;
916 if let Some(row) = row {
917 if let Some(active_editor) = item.downcast::<Editor>() {
918 active_editor
919 .downgrade()
920 .update(&mut cx, |editor, cx| {
921 let snapshot = editor.snapshot(cx).display_snapshot;
922 let point = snapshot
923 .buffer_snapshot
924 .clip_point(Point::new(row, col), Bias::Left);
925 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
926 s.select_ranges([point..point])
927 });
928 })
929 .log_err();
930 }
931 }
932 finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
933
934 Some(())
935 })
936 .detach();
937 }
938 }
939 }
940
941 fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
942 self.file_finder
943 .update(cx, |_, cx| cx.emit(DismissEvent))
944 .log_err();
945 }
946
947 fn render_match(
948 &self,
949 ix: usize,
950 selected: bool,
951 cx: &mut ViewContext<Picker<Self>>,
952 ) -> Option<Self::ListItem> {
953 let path_match = self
954 .matches
955 .get(ix)
956 .expect("Invalid matches state: no element for index {ix}");
957
958 let icon = match &path_match {
959 Match::History(_, _) => Icon::new(IconName::HistoryRerun)
960 .color(Color::Muted)
961 .size(IconSize::Small)
962 .into_any_element(),
963 Match::Search(_) => v_flex()
964 .flex_none()
965 .size(IconSize::Small.rems())
966 .into_any_element(),
967 };
968 let (file_name, file_name_positions, full_path, full_path_positions) =
969 self.labels_for_match(path_match, cx, ix);
970
971 Some(
972 ListItem::new(ix)
973 .spacing(ListItemSpacing::Sparse)
974 .end_slot::<AnyElement>(Some(icon))
975 .inset(true)
976 .selected(selected)
977 .child(
978 h_flex()
979 .gap_2()
980 .py_px()
981 .child(HighlightedLabel::new(file_name, file_name_positions))
982 .child(
983 HighlightedLabel::new(full_path, full_path_positions)
984 .size(LabelSize::Small)
985 .color(Color::Muted),
986 ),
987 ),
988 )
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995
996 #[test]
997 fn test_custom_project_search_ordering_in_file_finder() {
998 let mut file_finder_sorted_output = vec![
999 ProjectPanelOrdMatch(PathMatch {
1000 score: 0.5,
1001 positions: Vec::new(),
1002 worktree_id: 0,
1003 path: Arc::from(Path::new("b0.5")),
1004 path_prefix: Arc::from(""),
1005 distance_to_relative_ancestor: 0,
1006 }),
1007 ProjectPanelOrdMatch(PathMatch {
1008 score: 1.0,
1009 positions: Vec::new(),
1010 worktree_id: 0,
1011 path: Arc::from(Path::new("c1.0")),
1012 path_prefix: Arc::from(""),
1013 distance_to_relative_ancestor: 0,
1014 }),
1015 ProjectPanelOrdMatch(PathMatch {
1016 score: 1.0,
1017 positions: Vec::new(),
1018 worktree_id: 0,
1019 path: Arc::from(Path::new("a1.0")),
1020 path_prefix: Arc::from(""),
1021 distance_to_relative_ancestor: 0,
1022 }),
1023 ProjectPanelOrdMatch(PathMatch {
1024 score: 0.5,
1025 positions: Vec::new(),
1026 worktree_id: 0,
1027 path: Arc::from(Path::new("a0.5")),
1028 path_prefix: Arc::from(""),
1029 distance_to_relative_ancestor: 0,
1030 }),
1031 ProjectPanelOrdMatch(PathMatch {
1032 score: 1.0,
1033 positions: Vec::new(),
1034 worktree_id: 0,
1035 path: Arc::from(Path::new("b1.0")),
1036 path_prefix: Arc::from(""),
1037 distance_to_relative_ancestor: 0,
1038 }),
1039 ];
1040 file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
1041
1042 assert_eq!(
1043 file_finder_sorted_output,
1044 vec![
1045 ProjectPanelOrdMatch(PathMatch {
1046 score: 1.0,
1047 positions: Vec::new(),
1048 worktree_id: 0,
1049 path: Arc::from(Path::new("a1.0")),
1050 path_prefix: Arc::from(""),
1051 distance_to_relative_ancestor: 0,
1052 }),
1053 ProjectPanelOrdMatch(PathMatch {
1054 score: 1.0,
1055 positions: Vec::new(),
1056 worktree_id: 0,
1057 path: Arc::from(Path::new("b1.0")),
1058 path_prefix: Arc::from(""),
1059 distance_to_relative_ancestor: 0,
1060 }),
1061 ProjectPanelOrdMatch(PathMatch {
1062 score: 1.0,
1063 positions: Vec::new(),
1064 worktree_id: 0,
1065 path: Arc::from(Path::new("c1.0")),
1066 path_prefix: Arc::from(""),
1067 distance_to_relative_ancestor: 0,
1068 }),
1069 ProjectPanelOrdMatch(PathMatch {
1070 score: 0.5,
1071 positions: Vec::new(),
1072 worktree_id: 0,
1073 path: Arc::from(Path::new("a0.5")),
1074 path_prefix: Arc::from(""),
1075 distance_to_relative_ancestor: 0,
1076 }),
1077 ProjectPanelOrdMatch(PathMatch {
1078 score: 0.5,
1079 positions: Vec::new(),
1080 worktree_id: 0,
1081 path: Arc::from(Path::new("b0.5")),
1082 path_prefix: Arc::from(""),
1083 distance_to_relative_ancestor: 0,
1084 }),
1085 ]
1086 );
1087 }
1088}