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