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