1#[cfg(test)]
2mod file_finder_tests;
3
4pub mod file_finder_settings;
5mod new_path_prompt;
6mod open_path_prompt;
7
8use futures::future::join_all;
9pub use open_path_prompt::OpenPathDelegate;
10
11use collections::HashMap;
12use editor::Editor;
13use file_finder_settings::{FileFinderSettings, FileFinderWidth};
14use file_icons::FileIcons;
15use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
16use gpui::{
17 actions, Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
18 Focusable, KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task,
19 WeakEntity, Window,
20};
21use new_path_prompt::NewPathPrompt;
22use open_path_prompt::OpenPathPrompt;
23use picker::{Picker, PickerDelegate};
24use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
25use settings::Settings;
26use std::{
27 cmp,
28 ops::Range,
29 path::{Path, PathBuf},
30 sync::{
31 atomic::{self, AtomicBool},
32 Arc,
33 },
34};
35use text::Point;
36use ui::{
37 prelude::*, ContextMenu, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, PopoverMenu,
38 PopoverMenuHandle,
39};
40use util::{paths::PathWithPosition, post_inc, ResultExt};
41use workspace::{
42 item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, SplitDirection,
43 Workspace,
44};
45
46actions!(file_finder, [SelectPrev, ToggleMenu]);
47
48impl ModalView for FileFinder {
49 fn on_before_dismiss(
50 &mut self,
51 window: &mut Window,
52 cx: &mut Context<Self>,
53 ) -> workspace::DismissDecision {
54 let submenu_focused = self.picker.update(cx, |picker, cx| {
55 picker.delegate.popover_menu_handle.is_focused(window, cx)
56 });
57 workspace::DismissDecision::Dismiss(!submenu_focused)
58 }
59}
60
61pub struct FileFinder {
62 picker: Entity<Picker<FileFinderDelegate>>,
63 picker_focus_handle: FocusHandle,
64 init_modifiers: Option<Modifiers>,
65}
66
67pub fn init_settings(cx: &mut App) {
68 FileFinderSettings::register(cx);
69}
70
71pub fn init(cx: &mut App) {
72 init_settings(cx);
73 cx.observe_new(FileFinder::register).detach();
74 cx.observe_new(NewPathPrompt::register).detach();
75 cx.observe_new(OpenPathPrompt::register).detach();
76}
77
78impl FileFinder {
79 fn register(
80 workspace: &mut Workspace,
81 _window: Option<&mut Window>,
82 _: &mut Context<Workspace>,
83 ) {
84 workspace.register_action(
85 |workspace, action: &workspace::ToggleFileFinder, window, cx| {
86 let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
87 Self::open(workspace, action.separate_history, window, cx).detach();
88 return;
89 };
90
91 file_finder.update(cx, |file_finder, cx| {
92 file_finder.init_modifiers = Some(window.modifiers());
93 file_finder.picker.update(cx, |picker, cx| {
94 picker.cycle_selection(window, cx);
95 });
96 });
97 },
98 );
99 }
100
101 fn open(
102 workspace: &mut Workspace,
103 separate_history: bool,
104 window: &mut Window,
105 cx: &mut Context<Workspace>,
106 ) -> Task<()> {
107 let project = workspace.project().read(cx);
108 let fs = project.fs();
109
110 let currently_opened_path = workspace
111 .active_item(cx)
112 .and_then(|item| item.project_path(cx))
113 .map(|project_path| {
114 let abs_path = project
115 .worktree_for_id(project_path.worktree_id, cx)
116 .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
117 FoundPath::new(project_path, abs_path)
118 });
119
120 let history_items = workspace
121 .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
122 .into_iter()
123 .filter_map(|(project_path, abs_path)| {
124 if project.entry_for_path(&project_path, cx).is_some() {
125 return Some(Task::ready(Some(FoundPath::new(project_path, abs_path))));
126 }
127 let abs_path = abs_path?;
128 if project.is_local() {
129 let fs = fs.clone();
130 Some(cx.background_spawn(async move {
131 if fs.is_file(&abs_path).await {
132 Some(FoundPath::new(project_path, Some(abs_path)))
133 } else {
134 None
135 }
136 }))
137 } else {
138 Some(Task::ready(Some(FoundPath::new(
139 project_path,
140 Some(abs_path),
141 ))))
142 }
143 })
144 .collect::<Vec<_>>();
145 cx.spawn_in(window, move |workspace, mut cx| async move {
146 let history_items = join_all(history_items).await.into_iter().flatten();
147
148 workspace
149 .update_in(&mut cx, |workspace, window, cx| {
150 let project = workspace.project().clone();
151 let weak_workspace = cx.entity().downgrade();
152 workspace.toggle_modal(window, cx, |window, cx| {
153 let delegate = FileFinderDelegate::new(
154 cx.entity().downgrade(),
155 weak_workspace,
156 project,
157 currently_opened_path,
158 history_items.collect(),
159 separate_history,
160 window,
161 cx,
162 );
163
164 FileFinder::new(delegate, window, cx)
165 });
166 })
167 .ok();
168 })
169 }
170
171 fn new(delegate: FileFinderDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
172 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
173 let picker_focus_handle = picker.focus_handle(cx);
174 picker.update(cx, |picker, _| {
175 picker.delegate.focus_handle = picker_focus_handle.clone();
176 });
177 Self {
178 picker,
179 picker_focus_handle,
180 init_modifiers: window.modifiers().modified().then_some(window.modifiers()),
181 }
182 }
183
184 fn handle_modifiers_changed(
185 &mut self,
186 event: &ModifiersChangedEvent,
187 window: &mut Window,
188 cx: &mut Context<Self>,
189 ) {
190 let Some(init_modifiers) = self.init_modifiers.take() else {
191 return;
192 };
193 if self.picker.read(cx).delegate.has_changed_selected_index {
194 if !event.modified() || !init_modifiers.is_subset_of(&event) {
195 self.init_modifiers = None;
196 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
197 }
198 }
199 }
200
201 fn handle_select_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context<Self>) {
202 self.init_modifiers = Some(window.modifiers());
203 window.dispatch_action(Box::new(menu::SelectPrev), cx);
204 }
205
206 fn handle_toggle_menu(&mut self, _: &ToggleMenu, window: &mut Window, cx: &mut Context<Self>) {
207 self.picker.update(cx, |picker, cx| {
208 let menu_handle = &picker.delegate.popover_menu_handle;
209 if menu_handle.is_deployed() {
210 menu_handle.hide(cx);
211 } else {
212 menu_handle.show(window, cx);
213 }
214 });
215 }
216
217 fn go_to_file_split_left(
218 &mut self,
219 _: &pane::SplitLeft,
220 window: &mut Window,
221 cx: &mut Context<Self>,
222 ) {
223 self.go_to_file_split_inner(SplitDirection::Left, window, cx)
224 }
225
226 fn go_to_file_split_right(
227 &mut self,
228 _: &pane::SplitRight,
229 window: &mut Window,
230 cx: &mut Context<Self>,
231 ) {
232 self.go_to_file_split_inner(SplitDirection::Right, window, cx)
233 }
234
235 fn go_to_file_split_up(
236 &mut self,
237 _: &pane::SplitUp,
238 window: &mut Window,
239 cx: &mut Context<Self>,
240 ) {
241 self.go_to_file_split_inner(SplitDirection::Up, window, cx)
242 }
243
244 fn go_to_file_split_down(
245 &mut self,
246 _: &pane::SplitDown,
247 window: &mut Window,
248 cx: &mut Context<Self>,
249 ) {
250 self.go_to_file_split_inner(SplitDirection::Down, window, cx)
251 }
252
253 fn go_to_file_split_inner(
254 &mut self,
255 split_direction: SplitDirection,
256 window: &mut Window,
257 cx: &mut Context<Self>,
258 ) {
259 self.picker.update(cx, |picker, cx| {
260 let delegate = &mut picker.delegate;
261 if let Some(workspace) = delegate.workspace.upgrade() {
262 if let Some(m) = delegate.matches.get(delegate.selected_index()) {
263 let path = match &m {
264 Match::History { path, .. } => {
265 let worktree_id = path.project.worktree_id;
266 ProjectPath {
267 worktree_id,
268 path: Arc::clone(&path.project.path),
269 }
270 }
271 Match::Search(m) => ProjectPath {
272 worktree_id: WorktreeId::from_usize(m.0.worktree_id),
273 path: m.0.path.clone(),
274 },
275 };
276 let open_task = workspace.update(cx, move |workspace, cx| {
277 workspace.split_path_preview(path, false, Some(split_direction), window, cx)
278 });
279 open_task.detach_and_log_err(cx);
280 }
281 }
282 })
283 }
284
285 pub fn modal_max_width(width_setting: Option<FileFinderWidth>, window: &mut Window) -> Pixels {
286 let window_width = window.viewport_size().width;
287 let small_width = Pixels(545.);
288
289 match width_setting {
290 None | Some(FileFinderWidth::Small) => small_width,
291 Some(FileFinderWidth::Full) => window_width,
292 Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width),
293 Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width),
294 Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width),
295 }
296 }
297}
298
299impl EventEmitter<DismissEvent> for FileFinder {}
300
301impl Focusable for FileFinder {
302 fn focus_handle(&self, _: &App) -> FocusHandle {
303 self.picker_focus_handle.clone()
304 }
305}
306
307impl Render for FileFinder {
308 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
309 let key_context = self.picker.read(cx).delegate.key_context(window, cx);
310
311 let file_finder_settings = FileFinderSettings::get_global(cx);
312 let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, window);
313
314 v_flex()
315 .key_context(key_context)
316 .w(modal_max_width)
317 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
318 .on_action(cx.listener(Self::handle_select_prev))
319 .on_action(cx.listener(Self::handle_toggle_menu))
320 .on_action(cx.listener(Self::go_to_file_split_left))
321 .on_action(cx.listener(Self::go_to_file_split_right))
322 .on_action(cx.listener(Self::go_to_file_split_up))
323 .on_action(cx.listener(Self::go_to_file_split_down))
324 .child(self.picker.clone())
325 }
326}
327
328pub struct FileFinderDelegate {
329 file_finder: WeakEntity<FileFinder>,
330 workspace: WeakEntity<Workspace>,
331 project: Entity<Project>,
332 search_count: usize,
333 latest_search_id: usize,
334 latest_search_did_cancel: bool,
335 latest_search_query: Option<FileSearchQuery>,
336 currently_opened_path: Option<FoundPath>,
337 matches: Matches,
338 selected_index: usize,
339 has_changed_selected_index: bool,
340 cancel_flag: Arc<AtomicBool>,
341 history_items: Vec<FoundPath>,
342 separate_history: bool,
343 first_update: bool,
344 popover_menu_handle: PopoverMenuHandle<ContextMenu>,
345 focus_handle: FocusHandle,
346}
347
348/// Use a custom ordering for file finder: the regular one
349/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:
350/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`
351///
352/// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:
353/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`
354/// as the files are shown in the project panel lists.
355#[derive(Debug, Clone, PartialEq, Eq)]
356struct ProjectPanelOrdMatch(PathMatch);
357
358impl Ord for ProjectPanelOrdMatch {
359 fn cmp(&self, other: &Self) -> cmp::Ordering {
360 self.0
361 .score
362 .partial_cmp(&other.0.score)
363 .unwrap_or(cmp::Ordering::Equal)
364 .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))
365 .then_with(|| {
366 other
367 .0
368 .distance_to_relative_ancestor
369 .cmp(&self.0.distance_to_relative_ancestor)
370 })
371 .then_with(|| self.0.path.cmp(&other.0.path).reverse())
372 }
373}
374
375impl PartialOrd for ProjectPanelOrdMatch {
376 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
377 Some(self.cmp(other))
378 }
379}
380
381#[derive(Debug, Default)]
382struct Matches {
383 separate_history: bool,
384 matches: Vec<Match>,
385 elided_byte_range: Option<Range<usize>>,
386}
387
388#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
389enum Match {
390 History {
391 path: FoundPath,
392 panel_match: Option<ProjectPanelOrdMatch>,
393 },
394 Search(ProjectPanelOrdMatch),
395}
396
397struct MatchLabels {
398 path: String,
399 path_positions: Vec<usize>,
400 file_name: String,
401 file_name_positions: Vec<usize>,
402}
403
404#[derive(Clone, Copy, Debug)]
405struct EmWidths {
406 normal: Pixels,
407 small: Pixels,
408}
409
410fn em_widths(window: &mut Window, cx: &mut App) -> EmWidths {
411 let style = window.text_style();
412 let font_id = window.text_system().resolve_font(&style.font());
413 let font_size = TextSize::Default.rems(cx).to_pixels(window.rem_size());
414 let normal = cx
415 .text_system()
416 .em_width(font_id, font_size)
417 .unwrap_or(px(16.));
418 let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
419 let small = cx
420 .text_system()
421 .em_width(font_id, font_size)
422 .unwrap_or(px(10.));
423 EmWidths { normal, small }
424}
425
426impl Match {
427 fn path(&self) -> &Arc<Path> {
428 match self {
429 Match::History { path, .. } => &path.project.path,
430 Match::Search(panel_match) => &panel_match.0.path,
431 }
432 }
433
434 fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> {
435 match self {
436 Match::History { panel_match, .. } => panel_match.as_ref(),
437 Match::Search(panel_match) => Some(&panel_match),
438 }
439 }
440
441 fn common_prefix<'a>(&self, other: &'a Path) -> &'a Path {
442 let mut prefix = other;
443 let path_positions = match self {
444 Match::History {
445 panel_match: Some(mat),
446 ..
447 }
448 | Match::Search(mat) => mat.0.positions.as_slice(),
449 Match::History {
450 panel_match: None, ..
451 } => &[],
452 };
453 let first_path_position = *path_positions.iter().min().unwrap_or(&0);
454 while self.path().strip_prefix(prefix).is_err()
455 || prefix.to_string_lossy().len() > first_path_position
456 {
457 let Some(parent) = prefix.parent() else {
458 break;
459 };
460 prefix = parent;
461 }
462 prefix
463 }
464
465 fn approx_width(&self, em_widths: EmWidths) -> Pixels {
466 let file_name = self.path().file_name().map_or_else(
467 || self.path().to_string_lossy(),
468 |file_name| file_name.to_string_lossy(),
469 );
470 let parent = self
471 .path()
472 .parent()
473 .map_or_else(|| "".into(), |parent| parent.to_string_lossy());
474 px(file_name.chars().count() as f32) * em_widths.normal
475 + px(parent.chars().count() as f32) * em_widths.small
476 }
477}
478
479impl Matches {
480 fn len(&self) -> usize {
481 self.matches.len()
482 }
483
484 fn get(&self, index: usize) -> Option<&Match> {
485 self.matches.get(index)
486 }
487
488 fn position(
489 &self,
490 entry: &Match,
491 currently_opened: Option<&FoundPath>,
492 ) -> Result<usize, usize> {
493 if let Match::History {
494 path,
495 panel_match: None,
496 } = entry
497 {
498 // Slow case: linear search by path. Should not happen actually,
499 // since we call `position` only if matches set changed, but the query has not changed.
500 // And History entries do not have panel_match if query is empty, so there's no
501 // reason for the matches set to change.
502 self.matches
503 .iter()
504 .position(|m| path.project.path == *m.path())
505 .ok_or(0)
506 } else {
507 self.matches.binary_search_by(|m| {
508 // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b.
509 // And we want the better entries go first.
510 Self::cmp_matches(self.separate_history, currently_opened, &m, &entry).reverse()
511 })
512 }
513 }
514
515 fn push_new_matches<'a>(
516 &'a mut self,
517 history_items: impl IntoIterator<Item = &'a FoundPath> + Clone,
518 currently_opened: Option<&'a FoundPath>,
519 query: Option<&FileSearchQuery>,
520 new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
521 extend_old_matches: bool,
522 em_widths: EmWidths,
523 max_width: Pixels,
524 ) {
525 self.elided_byte_range = None;
526
527 let Some(query) = query else {
528 // assuming that if there's no query, then there's no search matches.
529 self.matches.clear();
530 let path_to_entry = |found_path: &FoundPath| Match::History {
531 path: found_path.clone(),
532 panel_match: None,
533 };
534 self.matches
535 .extend(currently_opened.into_iter().map(path_to_entry));
536
537 self.matches.extend(
538 history_items
539 .into_iter()
540 .filter(|found_path| Some(*found_path) != currently_opened)
541 .map(path_to_entry),
542 );
543 return;
544 };
545
546 let new_history_matches = matching_history_items(history_items, currently_opened, query);
547 let new_search_matches: Vec<Match> = new_search_matches
548 .filter(|path_match| !new_history_matches.contains_key(&path_match.0.path))
549 .map(Match::Search)
550 .collect();
551
552 if extend_old_matches {
553 // since we take history matches instead of new search matches
554 // and history matches has not changed(since the query has not changed and we do not extend old matches otherwise),
555 // old matches can't contain paths present in history_matches as well.
556 self.matches.retain(|m| matches!(m, Match::Search(_)));
557 } else {
558 self.matches.clear();
559 }
560
561 // At this point we have an unsorted set of new history matches, an unsorted set of new search matches
562 // and a sorted set of old search matches.
563 // It is possible that the new search matches' paths contain some of the old search matches' paths.
564 // History matches' paths are unique, since store in a HashMap by path.
565 // We build a sorted Vec<Match>, eliminating duplicate search matches.
566 // Search matches with the same paths should have equal `ProjectPanelOrdMatch`, so we should
567 // not have any duplicates after building the final list.
568 for new_match in new_history_matches
569 .into_values()
570 .chain(new_search_matches.into_iter())
571 {
572 match self.position(&new_match, currently_opened) {
573 Ok(_duplicate) => continue,
574 Err(i) => {
575 self.matches.insert(i, new_match);
576 if self.matches.len() == 100 {
577 break;
578 }
579 }
580 }
581 }
582
583 let Some((first, rest)) = self.matches.split_first() else {
584 return;
585 };
586 let mut prefix = first.path().as_ref();
587 let mut widest_match = first.approx_width(em_widths);
588 for mat in rest {
589 widest_match = widest_match.max(mat.approx_width(em_widths));
590 prefix = mat.common_prefix(prefix);
591 }
592
593 if widest_match > max_width {
594 let components = prefix.components().collect::<Vec<_>>();
595 let prefix = prefix.to_string_lossy();
596 if components.len() > 3 {
597 let after_first = components[1..].iter().collect::<PathBuf>();
598 let after_first = after_first.to_string_lossy();
599 let start = prefix.len() - after_first.len();
600 let after_first_before_last = components[1..components.len() - 2]
601 .iter()
602 .collect::<PathBuf>();
603 let after_first_before_last = after_first_before_last.to_string_lossy();
604 self.elided_byte_range = Some(start..start + after_first_before_last.len());
605 }
606 }
607 }
608
609 /// If a < b, then a is a worse match, aligning with the `ProjectPanelOrdMatch` ordering.
610 fn cmp_matches(
611 separate_history: bool,
612 currently_opened: Option<&FoundPath>,
613 a: &Match,
614 b: &Match,
615 ) -> cmp::Ordering {
616 debug_assert!(a.panel_match().is_some() && b.panel_match().is_some());
617
618 match (&a, &b) {
619 // bubble currently opened files to the top
620 (Match::History { path, .. }, _) if Some(path) == currently_opened => {
621 cmp::Ordering::Greater
622 }
623 (_, Match::History { path, .. }) if Some(path) == currently_opened => {
624 cmp::Ordering::Less
625 }
626
627 (Match::History { .. }, Match::Search(_)) if separate_history => cmp::Ordering::Greater,
628 (Match::Search(_), Match::History { .. }) if separate_history => cmp::Ordering::Less,
629
630 _ => a.panel_match().cmp(&b.panel_match()),
631 }
632 }
633}
634
635impl MatchLabels {
636 fn check(&self) {
637 let file_name = &self.file_name;
638 for i in &self.file_name_positions {
639 assert!(
640 self.file_name.is_char_boundary(*i),
641 "{i} is not a valid char boundary in file name {file_name:?}"
642 );
643 }
644 let path = &self.path;
645 for i in &self.path_positions {
646 assert!(
647 self.path.is_char_boundary(*i),
648 "{i} is not a valid char boundary in path {path:?}"
649 );
650 }
651 }
652}
653
654fn matching_history_items<'a>(
655 history_items: impl IntoIterator<Item = &'a FoundPath>,
656 currently_opened: Option<&'a FoundPath>,
657 query: &FileSearchQuery,
658) -> HashMap<Arc<Path>, Match> {
659 let mut candidates_paths = HashMap::default();
660
661 let history_items_by_worktrees = history_items
662 .into_iter()
663 .chain(currently_opened)
664 .filter_map(|found_path| {
665 let candidate = PathMatchCandidate {
666 is_dir: false, // You can't open directories as project items
667 path: &found_path.project.path,
668 // Only match history items names, otherwise their paths may match too many queries, producing false positives.
669 // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
670 // it would be shown first always, despite the latter being a better match.
671 char_bag: CharBag::from_iter(
672 found_path
673 .project
674 .path
675 .file_name()?
676 .to_string_lossy()
677 .to_lowercase()
678 .chars(),
679 ),
680 };
681 candidates_paths.insert(&found_path.project, found_path);
682 Some((found_path.project.worktree_id, candidate))
683 })
684 .fold(
685 HashMap::default(),
686 |mut candidates, (worktree_id, new_candidate)| {
687 candidates
688 .entry(worktree_id)
689 .or_insert_with(Vec::new)
690 .push(new_candidate);
691 candidates
692 },
693 );
694 let mut matching_history_paths = HashMap::default();
695 for (worktree, candidates) in history_items_by_worktrees {
696 let max_results = candidates.len() + 1;
697 matching_history_paths.extend(
698 fuzzy::match_fixed_path_set(
699 candidates,
700 worktree.to_usize(),
701 query.path_query(),
702 false,
703 max_results,
704 )
705 .into_iter()
706 .filter_map(|path_match| {
707 candidates_paths
708 .remove_entry(&ProjectPath {
709 worktree_id: WorktreeId::from_usize(path_match.worktree_id),
710 path: Arc::clone(&path_match.path),
711 })
712 .map(|(_, found_path)| {
713 (
714 Arc::clone(&path_match.path),
715 Match::History {
716 path: found_path.clone(),
717 panel_match: Some(ProjectPanelOrdMatch(path_match)),
718 },
719 )
720 })
721 }),
722 );
723 }
724 matching_history_paths
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
728struct FoundPath {
729 project: ProjectPath,
730 absolute: Option<PathBuf>,
731}
732
733impl FoundPath {
734 fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
735 Self { project, absolute }
736 }
737}
738
739const MAX_RECENT_SELECTIONS: usize = 20;
740
741pub enum Event {
742 Selected(ProjectPath),
743 Dismissed,
744}
745
746#[derive(Debug, Clone)]
747struct FileSearchQuery {
748 raw_query: String,
749 file_query_end: Option<usize>,
750 path_position: PathWithPosition,
751}
752
753impl FileSearchQuery {
754 fn path_query(&self) -> &str {
755 match self.file_query_end {
756 Some(file_path_end) => &self.raw_query[..file_path_end],
757 None => &self.raw_query,
758 }
759 }
760}
761
762impl FileFinderDelegate {
763 fn new(
764 file_finder: WeakEntity<FileFinder>,
765 workspace: WeakEntity<Workspace>,
766 project: Entity<Project>,
767 currently_opened_path: Option<FoundPath>,
768 history_items: Vec<FoundPath>,
769 separate_history: bool,
770 window: &mut Window,
771 cx: &mut Context<FileFinder>,
772 ) -> Self {
773 Self::subscribe_to_updates(&project, window, cx);
774 Self {
775 file_finder,
776 workspace,
777 project,
778 search_count: 0,
779 latest_search_id: 0,
780 latest_search_did_cancel: false,
781 latest_search_query: None,
782 currently_opened_path,
783 matches: Matches::default(),
784 has_changed_selected_index: false,
785 selected_index: 0,
786 cancel_flag: Arc::new(AtomicBool::new(false)),
787 history_items,
788 separate_history,
789 first_update: true,
790 popover_menu_handle: PopoverMenuHandle::default(),
791 focus_handle: cx.focus_handle(),
792 }
793 }
794
795 fn subscribe_to_updates(
796 project: &Entity<Project>,
797 window: &mut Window,
798 cx: &mut Context<FileFinder>,
799 ) {
800 cx.subscribe_in(project, window, |file_finder, _, event, window, cx| {
801 match event {
802 project::Event::WorktreeUpdatedEntries(_, _)
803 | project::Event::WorktreeAdded(_)
804 | project::Event::WorktreeRemoved(_) => file_finder
805 .picker
806 .update(cx, |picker, cx| picker.refresh(window, cx)),
807 _ => {}
808 };
809 })
810 .detach();
811 }
812
813 fn spawn_search(
814 &mut self,
815 query: FileSearchQuery,
816 window: &mut Window,
817 cx: &mut Context<Picker<Self>>,
818 ) -> Task<()> {
819 let relative_to = self
820 .currently_opened_path
821 .as_ref()
822 .map(|found_path| Arc::clone(&found_path.project.path));
823 let worktrees = self
824 .project
825 .read(cx)
826 .visible_worktrees(cx)
827 .collect::<Vec<_>>();
828 let include_root_name = worktrees.len() > 1;
829 let candidate_sets = worktrees
830 .into_iter()
831 .map(|worktree| {
832 let worktree = worktree.read(cx);
833 PathMatchCandidateSet {
834 snapshot: worktree.snapshot(),
835 include_ignored: worktree
836 .root_entry()
837 .map_or(false, |entry| entry.is_ignored),
838 include_root_name,
839 candidates: project::Candidates::Files,
840 }
841 })
842 .collect::<Vec<_>>();
843
844 let search_id = util::post_inc(&mut self.search_count);
845 self.cancel_flag.store(true, atomic::Ordering::Relaxed);
846 self.cancel_flag = Arc::new(AtomicBool::new(false));
847 let cancel_flag = self.cancel_flag.clone();
848 cx.spawn_in(window, |picker, mut cx| async move {
849 let matches = fuzzy::match_path_sets(
850 candidate_sets.as_slice(),
851 query.path_query(),
852 relative_to,
853 false,
854 100,
855 &cancel_flag,
856 cx.background_executor().clone(),
857 )
858 .await
859 .into_iter()
860 .map(ProjectPanelOrdMatch);
861 let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
862 picker
863 .update_in(&mut cx, |picker, window, cx| {
864 picker
865 .delegate
866 .set_search_matches(search_id, did_cancel, query, matches, window, cx)
867 })
868 .log_err();
869 })
870 }
871
872 fn set_search_matches(
873 &mut self,
874 search_id: usize,
875 did_cancel: bool,
876 query: FileSearchQuery,
877 matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
878 window: &mut Window,
879 cx: &mut Context<Picker<Self>>,
880 ) {
881 let em_widths = em_widths(window, cx);
882 let file_finder_settings = FileFinderSettings::get_global(cx);
883 let max_width = FileFinder::modal_max_width(file_finder_settings.modal_max_width, window);
884
885 if search_id >= self.latest_search_id {
886 self.latest_search_id = search_id;
887 let query_changed = Some(query.path_query())
888 != self
889 .latest_search_query
890 .as_ref()
891 .map(|query| query.path_query());
892 let extend_old_matches = self.latest_search_did_cancel && !query_changed;
893
894 let selected_match = if query_changed {
895 None
896 } else {
897 self.matches.get(self.selected_index).cloned()
898 };
899
900 self.matches.push_new_matches(
901 &self.history_items,
902 self.currently_opened_path.as_ref(),
903 Some(&query),
904 matches.into_iter(),
905 extend_old_matches,
906 em_widths,
907 max_width,
908 );
909
910 self.selected_index = selected_match.map_or_else(
911 || self.calculate_selected_index(),
912 |m| {
913 self.matches
914 .position(&m, self.currently_opened_path.as_ref())
915 .unwrap_or(0)
916 },
917 );
918
919 self.latest_search_query = Some(query);
920 self.latest_search_did_cancel = did_cancel;
921
922 cx.notify();
923 }
924 }
925
926 fn labels_for_match(&self, path_match: &Match, cx: &App, ix: usize) -> MatchLabels {
927 let mut labels = match &path_match {
928 Match::History {
929 path: entry_path,
930 panel_match,
931 } => {
932 let worktree_id = entry_path.project.worktree_id;
933 let project_relative_path = &entry_path.project.path;
934 let has_worktree = self
935 .project
936 .read(cx)
937 .worktree_for_id(worktree_id, cx)
938 .is_some();
939
940 if !has_worktree {
941 if let Some(absolute_path) = &entry_path.absolute {
942 return MatchLabels {
943 file_name: absolute_path
944 .file_name()
945 .map_or_else(
946 || project_relative_path.to_string_lossy(),
947 |file_name| file_name.to_string_lossy(),
948 )
949 .to_string(),
950 file_name_positions: Vec::new(),
951 path: absolute_path.to_string_lossy().to_string(),
952 path_positions: Vec::new(),
953 };
954 }
955 }
956
957 let mut path = Arc::clone(project_relative_path);
958 if project_relative_path.as_ref() == Path::new("") {
959 if let Some(absolute_path) = &entry_path.absolute {
960 path = Arc::from(absolute_path.as_path());
961 }
962 }
963
964 let mut path_match = PathMatch {
965 score: ix as f64,
966 positions: Vec::new(),
967 worktree_id: worktree_id.to_usize(),
968 path,
969 is_dir: false, // File finder doesn't support directories
970 path_prefix: "".into(),
971 distance_to_relative_ancestor: usize::MAX,
972 };
973 if let Some(found_path_match) = &panel_match {
974 path_match
975 .positions
976 .extend(found_path_match.0.positions.iter())
977 }
978
979 self.labels_for_path_match(&path_match)
980 }
981 Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
982 };
983
984 if labels.file_name_positions.is_empty() {
985 if let Some(user_home_path) = std::env::var("HOME").ok() {
986 let user_home_path = user_home_path.trim();
987 if !user_home_path.is_empty() {
988 if labels.path.starts_with(user_home_path) {
989 labels.path.replace_range(0..user_home_path.len(), "~");
990 labels.path_positions.retain_mut(|position| {
991 if *position >= user_home_path.len() {
992 *position -= user_home_path.len();
993 *position += 1;
994 true
995 } else {
996 false
997 }
998 })
999 }
1000 }
1001 }
1002 }
1003
1004 labels.check();
1005 labels
1006 }
1007
1008 fn labels_for_path_match(&self, path_match: &PathMatch) -> MatchLabels {
1009 let mut path_positions = path_match.positions.clone();
1010
1011 let file_name = path_match.path.file_name().map_or_else(
1012 || path_match.path_prefix.to_string(),
1013 |file_name| file_name.to_string_lossy().to_string(),
1014 );
1015 let mut path = path_match.path.to_string_lossy().to_string();
1016 let file_name_start = path_match.path_prefix.len() + path.len() - file_name.len();
1017 let file_name_positions = path_positions
1018 .iter()
1019 .filter_map(|pos| {
1020 if pos >= &file_name_start {
1021 Some(pos - file_name_start)
1022 } else {
1023 None
1024 }
1025 })
1026 .collect();
1027
1028 path.drain(file_name_start.saturating_sub(path_match.path_prefix.len())..);
1029 path_positions.retain_mut(|idx| {
1030 if *idx < path.len() {
1031 if let Some(elided_range) = &self.matches.elided_byte_range {
1032 if *idx >= elided_range.end {
1033 *idx += '…'.len_utf8();
1034 *idx -= elided_range.len();
1035 }
1036 }
1037 true
1038 } else {
1039 false
1040 }
1041 });
1042
1043 if let Some(elided_range) = &self.matches.elided_byte_range {
1044 path.replace_range(elided_range.clone(), "…");
1045 }
1046
1047 MatchLabels {
1048 file_name,
1049 file_name_positions,
1050 path,
1051 path_positions,
1052 }
1053 }
1054
1055 fn lookup_absolute_path(
1056 &self,
1057 query: FileSearchQuery,
1058 window: &mut Window,
1059 cx: &mut Context<Picker<Self>>,
1060 ) -> Task<()> {
1061 cx.spawn_in(window, |picker, mut cx| async move {
1062 let Some(project) = picker
1063 .update(&mut cx, |picker, _| picker.delegate.project.clone())
1064 .log_err()
1065 else {
1066 return;
1067 };
1068
1069 let query_path = Path::new(query.path_query());
1070 let mut path_matches = Vec::new();
1071
1072 let abs_file_exists = if let Ok(task) = project.update(&mut cx, |this, cx| {
1073 this.resolve_abs_file_path(query.path_query(), cx)
1074 }) {
1075 task.await.is_some()
1076 } else {
1077 false
1078 };
1079
1080 if abs_file_exists {
1081 let update_result = project
1082 .update(&mut cx, |project, cx| {
1083 if let Some((worktree, relative_path)) =
1084 project.find_worktree(query_path, cx)
1085 {
1086 path_matches.push(ProjectPanelOrdMatch(PathMatch {
1087 score: 1.0,
1088 positions: Vec::new(),
1089 worktree_id: worktree.read(cx).id().to_usize(),
1090 path: Arc::from(relative_path),
1091 path_prefix: "".into(),
1092 is_dir: false, // File finder doesn't support directories
1093 distance_to_relative_ancestor: usize::MAX,
1094 }));
1095 }
1096 })
1097 .log_err();
1098 if update_result.is_none() {
1099 return;
1100 }
1101 }
1102
1103 picker
1104 .update_in(&mut cx, |picker, window, cx| {
1105 let picker_delegate = &mut picker.delegate;
1106 let search_id = util::post_inc(&mut picker_delegate.search_count);
1107 picker_delegate.set_search_matches(
1108 search_id,
1109 false,
1110 query,
1111 path_matches,
1112 window,
1113 cx,
1114 );
1115
1116 anyhow::Ok(())
1117 })
1118 .log_err();
1119 })
1120 }
1121
1122 /// Skips first history match (that is displayed topmost) if it's currently opened.
1123 fn calculate_selected_index(&self) -> usize {
1124 if let Some(Match::History { path, .. }) = self.matches.get(0) {
1125 if Some(path) == self.currently_opened_path.as_ref() {
1126 let elements_after_first = self.matches.len() - 1;
1127 if elements_after_first > 0 {
1128 return 1;
1129 }
1130 }
1131 }
1132
1133 0
1134 }
1135
1136 fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
1137 let mut key_context = KeyContext::new_with_defaults();
1138 key_context.add("FileFinder");
1139 if self.popover_menu_handle.is_focused(window, cx) {
1140 key_context.add("menu_open");
1141 }
1142 key_context
1143 }
1144}
1145
1146impl PickerDelegate for FileFinderDelegate {
1147 type ListItem = ListItem;
1148
1149 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1150 "Search project files...".into()
1151 }
1152
1153 fn match_count(&self) -> usize {
1154 self.matches.len()
1155 }
1156
1157 fn selected_index(&self) -> usize {
1158 self.selected_index
1159 }
1160
1161 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
1162 self.has_changed_selected_index = true;
1163 self.selected_index = ix;
1164 cx.notify();
1165 }
1166
1167 fn separators_after_indices(&self) -> Vec<usize> {
1168 if self.separate_history {
1169 let first_non_history_index = self
1170 .matches
1171 .matches
1172 .iter()
1173 .enumerate()
1174 .find(|(_, m)| !matches!(m, Match::History { .. }))
1175 .map(|(i, _)| i);
1176 if let Some(first_non_history_index) = first_non_history_index {
1177 if first_non_history_index > 0 {
1178 return vec![first_non_history_index - 1];
1179 }
1180 }
1181 }
1182 Vec::new()
1183 }
1184
1185 fn update_matches(
1186 &mut self,
1187 raw_query: String,
1188 window: &mut Window,
1189 cx: &mut Context<Picker<Self>>,
1190 ) -> Task<()> {
1191 let em_widths = em_widths(window, cx);
1192 let file_finder_settings = FileFinderSettings::get_global(cx);
1193 let max_width = FileFinder::modal_max_width(file_finder_settings.modal_max_width, window);
1194
1195 let raw_query = raw_query.replace(' ', "");
1196 let raw_query = raw_query.trim();
1197 if raw_query.is_empty() {
1198 // if there was no query before, and we already have some (history) matches
1199 // there's no need to update anything, since nothing has changed.
1200 // We also want to populate matches set from history entries on the first update.
1201 if self.latest_search_query.is_some() || self.first_update {
1202 let project = self.project.read(cx);
1203
1204 self.latest_search_id = post_inc(&mut self.search_count);
1205 self.latest_search_query = None;
1206 self.matches = Matches {
1207 separate_history: self.separate_history,
1208 ..Matches::default()
1209 };
1210 self.matches.push_new_matches(
1211 self.history_items.iter().filter(|history_item| {
1212 project
1213 .worktree_for_id(history_item.project.worktree_id, cx)
1214 .is_some()
1215 || ((project.is_local() || project.is_via_ssh())
1216 && history_item.absolute.is_some())
1217 }),
1218 self.currently_opened_path.as_ref(),
1219 None,
1220 None.into_iter(),
1221 false,
1222 em_widths,
1223 max_width,
1224 );
1225
1226 self.first_update = false;
1227 self.selected_index = 0;
1228 }
1229 cx.notify();
1230 Task::ready(())
1231 } else {
1232 let path_position = PathWithPosition::parse_str(&raw_query);
1233
1234 let query = FileSearchQuery {
1235 raw_query: raw_query.trim().to_owned(),
1236 file_query_end: if path_position.path.to_str().unwrap_or(raw_query) == raw_query {
1237 None
1238 } else {
1239 // Safe to unwrap as we won't get here when the unwrap in if fails
1240 Some(path_position.path.to_str().unwrap().len())
1241 },
1242 path_position,
1243 };
1244
1245 if Path::new(query.path_query()).is_absolute() {
1246 self.lookup_absolute_path(query, window, cx)
1247 } else {
1248 self.spawn_search(query, window, cx)
1249 }
1250 }
1251 }
1252
1253 fn confirm(
1254 &mut self,
1255 secondary: bool,
1256 window: &mut Window,
1257 cx: &mut Context<Picker<FileFinderDelegate>>,
1258 ) {
1259 if let Some(m) = self.matches.get(self.selected_index()) {
1260 if let Some(workspace) = self.workspace.upgrade() {
1261 let open_task = workspace.update(cx, |workspace, cx| {
1262 let split_or_open =
1263 |workspace: &mut Workspace,
1264 project_path,
1265 window: &mut Window,
1266 cx: &mut Context<Workspace>| {
1267 let allow_preview =
1268 PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
1269 if secondary {
1270 workspace.split_path_preview(
1271 project_path,
1272 allow_preview,
1273 None,
1274 window,
1275 cx,
1276 )
1277 } else {
1278 workspace.open_path_preview(
1279 project_path,
1280 None,
1281 true,
1282 allow_preview,
1283 window,
1284 cx,
1285 )
1286 }
1287 };
1288 match &m {
1289 Match::History { path, .. } => {
1290 let worktree_id = path.project.worktree_id;
1291 if workspace
1292 .project()
1293 .read(cx)
1294 .worktree_for_id(worktree_id, cx)
1295 .is_some()
1296 {
1297 split_or_open(
1298 workspace,
1299 ProjectPath {
1300 worktree_id,
1301 path: Arc::clone(&path.project.path),
1302 },
1303 window,
1304 cx,
1305 )
1306 } else {
1307 match path.absolute.as_ref() {
1308 Some(abs_path) => {
1309 if secondary {
1310 workspace.split_abs_path(
1311 abs_path.to_path_buf(),
1312 false,
1313 window,
1314 cx,
1315 )
1316 } else {
1317 workspace.open_abs_path(
1318 abs_path.to_path_buf(),
1319 false,
1320 window,
1321 cx,
1322 )
1323 }
1324 }
1325 None => split_or_open(
1326 workspace,
1327 ProjectPath {
1328 worktree_id,
1329 path: Arc::clone(&path.project.path),
1330 },
1331 window,
1332 cx,
1333 ),
1334 }
1335 }
1336 }
1337 Match::Search(m) => split_or_open(
1338 workspace,
1339 ProjectPath {
1340 worktree_id: WorktreeId::from_usize(m.0.worktree_id),
1341 path: m.0.path.clone(),
1342 },
1343 window,
1344 cx,
1345 ),
1346 }
1347 });
1348
1349 let row = self
1350 .latest_search_query
1351 .as_ref()
1352 .and_then(|query| query.path_position.row)
1353 .map(|row| row.saturating_sub(1));
1354 let col = self
1355 .latest_search_query
1356 .as_ref()
1357 .and_then(|query| query.path_position.column)
1358 .unwrap_or(0)
1359 .saturating_sub(1);
1360 let finder = self.file_finder.clone();
1361
1362 cx.spawn_in(window, |_, mut cx| async move {
1363 let item = open_task.await.notify_async_err(&mut cx)?;
1364 if let Some(row) = row {
1365 if let Some(active_editor) = item.downcast::<Editor>() {
1366 active_editor
1367 .downgrade()
1368 .update_in(&mut cx, |editor, window, cx| {
1369 editor.go_to_singleton_buffer_point(
1370 Point::new(row, col),
1371 window,
1372 cx,
1373 );
1374 })
1375 .log_err();
1376 }
1377 }
1378 finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
1379
1380 Some(())
1381 })
1382 .detach();
1383 }
1384 }
1385 }
1386
1387 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<FileFinderDelegate>>) {
1388 self.file_finder
1389 .update(cx, |_, cx| cx.emit(DismissEvent))
1390 .log_err();
1391 }
1392
1393 fn render_match(
1394 &self,
1395 ix: usize,
1396 selected: bool,
1397 _: &mut Window,
1398 cx: &mut Context<Picker<Self>>,
1399 ) -> Option<Self::ListItem> {
1400 let settings = FileFinderSettings::get_global(cx);
1401
1402 let path_match = self
1403 .matches
1404 .get(ix)
1405 .expect("Invalid matches state: no element for index {ix}");
1406
1407 let history_icon = match &path_match {
1408 Match::History { .. } => Icon::new(IconName::HistoryRerun)
1409 .color(Color::Muted)
1410 .size(IconSize::Small)
1411 .into_any_element(),
1412 Match::Search(_) => v_flex()
1413 .flex_none()
1414 .size(IconSize::Small.rems())
1415 .into_any_element(),
1416 };
1417 let labels = self.labels_for_match(path_match, cx, ix);
1418
1419 let file_icon = if settings.file_icons {
1420 FileIcons::get_icon(Path::new(&labels.file_name), cx)
1421 .map(Icon::from_path)
1422 .map(|icon| icon.color(Color::Muted))
1423 } else {
1424 None
1425 };
1426
1427 Some(
1428 ListItem::new(ix)
1429 .spacing(ListItemSpacing::Sparse)
1430 .start_slot::<Icon>(file_icon)
1431 .end_slot::<AnyElement>(history_icon)
1432 .inset(true)
1433 .toggle_state(selected)
1434 .child(
1435 h_flex()
1436 .gap_2()
1437 .py_px()
1438 .child(HighlightedLabel::new(
1439 labels.file_name,
1440 labels.file_name_positions,
1441 ))
1442 .child(
1443 HighlightedLabel::new(labels.path, labels.path_positions)
1444 .size(LabelSize::Small)
1445 .color(Color::Muted),
1446 ),
1447 ),
1448 )
1449 }
1450
1451 fn render_footer(
1452 &self,
1453 window: &mut Window,
1454 cx: &mut Context<Picker<Self>>,
1455 ) -> Option<AnyElement> {
1456 let context = self.focus_handle.clone();
1457 Some(
1458 h_flex()
1459 .w_full()
1460 .p_2()
1461 .gap_2()
1462 .justify_end()
1463 .border_t_1()
1464 .border_color(cx.theme().colors().border_variant)
1465 .child(
1466 Button::new("open-selection", "Open")
1467 .key_binding(KeyBinding::for_action(&menu::Confirm, window, cx))
1468 .on_click(|_, window, cx| {
1469 window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1470 }),
1471 )
1472 .child(
1473 PopoverMenu::new("menu-popover")
1474 .with_handle(self.popover_menu_handle.clone())
1475 .attach(gpui::Corner::TopRight)
1476 .anchor(gpui::Corner::BottomRight)
1477 .trigger(
1478 Button::new("actions-trigger", "Split Options")
1479 .selected_label_color(Color::Accent)
1480 .key_binding(KeyBinding::for_action_in(
1481 &ToggleMenu,
1482 &context,
1483 window,
1484 cx,
1485 )),
1486 )
1487 .menu({
1488 move |window, cx| {
1489 Some(ContextMenu::build(window, cx, {
1490 let context = context.clone();
1491 move |menu, _, _| {
1492 menu.context(context)
1493 .action("Split Left", pane::SplitLeft.boxed_clone())
1494 .action("Split Right", pane::SplitRight.boxed_clone())
1495 .action("Split Up", pane::SplitUp.boxed_clone())
1496 .action("Split Down", pane::SplitDown.boxed_clone())
1497 }
1498 }))
1499 }
1500 }),
1501 )
1502 .into_any(),
1503 )
1504 }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509 use super::*;
1510
1511 #[test]
1512 fn test_custom_project_search_ordering_in_file_finder() {
1513 let mut file_finder_sorted_output = vec![
1514 ProjectPanelOrdMatch(PathMatch {
1515 score: 0.5,
1516 positions: Vec::new(),
1517 worktree_id: 0,
1518 path: Arc::from(Path::new("b0.5")),
1519 path_prefix: Arc::default(),
1520 distance_to_relative_ancestor: 0,
1521 is_dir: false,
1522 }),
1523 ProjectPanelOrdMatch(PathMatch {
1524 score: 1.0,
1525 positions: Vec::new(),
1526 worktree_id: 0,
1527 path: Arc::from(Path::new("c1.0")),
1528 path_prefix: Arc::default(),
1529 distance_to_relative_ancestor: 0,
1530 is_dir: false,
1531 }),
1532 ProjectPanelOrdMatch(PathMatch {
1533 score: 1.0,
1534 positions: Vec::new(),
1535 worktree_id: 0,
1536 path: Arc::from(Path::new("a1.0")),
1537 path_prefix: Arc::default(),
1538 distance_to_relative_ancestor: 0,
1539 is_dir: false,
1540 }),
1541 ProjectPanelOrdMatch(PathMatch {
1542 score: 0.5,
1543 positions: Vec::new(),
1544 worktree_id: 0,
1545 path: Arc::from(Path::new("a0.5")),
1546 path_prefix: Arc::default(),
1547 distance_to_relative_ancestor: 0,
1548 is_dir: false,
1549 }),
1550 ProjectPanelOrdMatch(PathMatch {
1551 score: 1.0,
1552 positions: Vec::new(),
1553 worktree_id: 0,
1554 path: Arc::from(Path::new("b1.0")),
1555 path_prefix: Arc::default(),
1556 distance_to_relative_ancestor: 0,
1557 is_dir: false,
1558 }),
1559 ];
1560 file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
1561
1562 assert_eq!(
1563 file_finder_sorted_output,
1564 vec![
1565 ProjectPanelOrdMatch(PathMatch {
1566 score: 1.0,
1567 positions: Vec::new(),
1568 worktree_id: 0,
1569 path: Arc::from(Path::new("a1.0")),
1570 path_prefix: Arc::default(),
1571 distance_to_relative_ancestor: 0,
1572 is_dir: false,
1573 }),
1574 ProjectPanelOrdMatch(PathMatch {
1575 score: 1.0,
1576 positions: Vec::new(),
1577 worktree_id: 0,
1578 path: Arc::from(Path::new("b1.0")),
1579 path_prefix: Arc::default(),
1580 distance_to_relative_ancestor: 0,
1581 is_dir: false,
1582 }),
1583 ProjectPanelOrdMatch(PathMatch {
1584 score: 1.0,
1585 positions: Vec::new(),
1586 worktree_id: 0,
1587 path: Arc::from(Path::new("c1.0")),
1588 path_prefix: Arc::default(),
1589 distance_to_relative_ancestor: 0,
1590 is_dir: false,
1591 }),
1592 ProjectPanelOrdMatch(PathMatch {
1593 score: 0.5,
1594 positions: Vec::new(),
1595 worktree_id: 0,
1596 path: Arc::from(Path::new("a0.5")),
1597 path_prefix: Arc::default(),
1598 distance_to_relative_ancestor: 0,
1599 is_dir: false,
1600 }),
1601 ProjectPanelOrdMatch(PathMatch {
1602 score: 0.5,
1603 positions: Vec::new(),
1604 worktree_id: 0,
1605 path: Arc::from(Path::new("b0.5")),
1606 path_prefix: Arc::default(),
1607 distance_to_relative_ancestor: 0,
1608 is_dir: false,
1609 }),
1610 ]
1611 );
1612 }
1613}