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