Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:54:36.559Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

file_finder.rs

2301 lines · 84.8 KB · rust
1#[cfg(test)]
2mod file_finder_tests;
3#[cfg(test)]
4mod multi_select_tests;
5
6use futures::future::join_all;
7pub use open_path_prompt::OpenPathDelegate;
8
9use channel::ChannelStore;
10use client::ChannelId;
11use collections::HashMap;
12use editor::Editor;
13use file_icons::FileIcons;
14use fuzzy::{StringMatch, StringMatchCandidate};
15use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
16use gpui::{
17    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
18    KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, TaskExt,
19    WeakEntity, Window, actions, rems,
20};
21use language::{BufferSnapshot, Point};
22use open_path_prompt::{
23    OpenPathPrompt,
24    file_finder_settings::{FileFinderSettings, FileFinderWidth},
25};
26use picker::{Picker, PickerDelegate};
27use project::{
28    PathMatchCandidateSet, Project, ProjectPath, WorktreeId, worktree_store::WorktreeStore,
29};
30use project_panel::project_panel_settings::ProjectPanelSettings;
31use settings::Settings;
32use std::{
33    borrow::Cow,
34    cmp, mem,
35    ops::{Range, RangeInclusive},
36    path::{Component, Path, PathBuf},
37    sync::{
38        Arc,
39        atomic::{self, AtomicBool},
40    },
41    time::Duration,
42};
43use ui::{Checkbox, HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*};
44use util::{
45    ResultExt, maybe,
46    paths::{PathStyle, PathWithPosition},
47    post_inc,
48    rel_path::RelPath,
49};
50use workspace::{
51    ModalView, OpenChannelNotesById, OpenOptions, OpenVisible, SplitDirection, Workspace,
52    item::PreviewTabsSettings, notifications::NotifyResultExt, pane,
53};
54use zed_actions::search::ToggleIncludeIgnored;
55
56actions!(
57    file_finder,
58    [
59        /// Selects the previous item in the file finder.
60        SelectPrevious,
61        /// Opens the selected file in the editor without dismissing the file finder,
62        /// so additional files can be opened in sequence.
63        OpenWithoutDismiss
64    ]
65);
66
67impl ModalView for FileFinder {}
68
69pub struct FileFinder {
70    picker: Entity<Picker<FileFinderDelegate>>,
71    picker_focus_handle: FocusHandle,
72    init_modifiers: Option<Modifiers>,
73}
74
75pub fn init(cx: &mut App) {
76    cx.observe_new(FileFinder::register).detach();
77    cx.observe_new(OpenPathPrompt::register).detach();
78    cx.observe_new(OpenPathPrompt::register_new_path).detach();
79}
80
81impl FileFinder {
82    fn register(
83        workspace: &mut Workspace,
84        _window: Option<&mut Window>,
85        _: &mut Context<Workspace>,
86    ) {
87        workspace.register_action(
88            |workspace, action: &workspace::ToggleFileFinder, window, cx| {
89                let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
90                    Self::open(
91                        workspace,
92                        action.separate_history,
93                        action.include_ignored,
94                        window,
95                        cx,
96                    )
97                    .detach();
98                    return;
99                };
100
101                file_finder.update(cx, |file_finder, cx| {
102                    file_finder.init_modifiers = Some(window.modifiers());
103                    file_finder.picker.update(cx, |picker, cx| {
104                        picker.cycle_selection(window, cx);
105                    });
106                });
107            },
108        );
109    }
110
111    fn open(
112        workspace: &mut Workspace,
113        separate_history: bool,
114        include_ignored: Option<bool>,
115        window: &mut Window,
116        cx: &mut Context<Workspace>,
117    ) -> Task<()> {
118        let project = workspace.project().read(cx);
119        let fs = project.fs();
120
121        let currently_opened_path = workspace.active_item(cx).and_then(|item| {
122            let project_path = item.project_path(cx)?;
123            let abs_path = project
124                .worktree_for_id(project_path.worktree_id, cx)?
125                .read(cx)
126                .absolutize(&project_path.path);
127            Some(FoundPath::new(project_path, abs_path))
128        });
129
130        let history_items = workspace
131            .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
132            .into_iter()
133            .filter_map(|(project_path, abs_path)| {
134                if project.entry_for_path(&project_path, cx).is_some() {
135                    return Some(Task::ready(Some(FoundPath::new(project_path, abs_path?))));
136                }
137                let abs_path = abs_path?;
138                if project.is_local() {
139                    let fs = fs.clone();
140                    Some(cx.background_spawn(async move {
141                        if fs.is_file(&abs_path).await {
142                            Some(FoundPath::new(project_path, abs_path))
143                        } else {
144                            None
145                        }
146                    }))
147                } else {
148                    Some(Task::ready(Some(FoundPath::new(project_path, abs_path))))
149                }
150            })
151            .collect::<Vec<_>>();
152        cx.spawn_in(window, async move |workspace, cx| {
153            let history_items = join_all(history_items).await.into_iter().flatten();
154
155            workspace
156                .update_in(cx, |workspace, window, cx| {
157                    let project = workspace.project().clone();
158                    let weak_workspace = cx.entity().downgrade();
159                    workspace.toggle_modal(window, cx, |window, cx| {
160                        let delegate = FileFinderDelegate::new(
161                            cx.entity().downgrade(),
162                            weak_workspace,
163                            project,
164                            currently_opened_path,
165                            history_items.collect(),
166                            separate_history,
167                            include_ignored,
168                            window,
169                            cx,
170                        );
171
172                        FileFinder::new(delegate, window, cx)
173                    });
174                })
175                .ok();
176        })
177    }
178
179    fn new(delegate: FileFinderDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
180        let modal_max_width_setting = FileFinderSettings::get_global(cx).modal_max_width;
181
182        let project = delegate.project.clone();
183        let modal_max_width = Self::modal_max_width(modal_max_width_setting, window);
184        let preview = picker_preview::editor_preview(project, window, cx);
185        let picker = cx.new(|cx| {
186            Picker::uniform_list_with_preview(delegate, preview, window, cx)
187                .initial_width(Rems::from_pixels(modal_max_width, window))
188        });
189        let picker_focus_handle = picker.focus_handle(cx);
190        picker.update(cx, |picker, _| {
191            picker.delegate.focus_handle = picker_focus_handle.clone();
192        });
193        Self {
194            picker,
195            picker_focus_handle,
196            init_modifiers: window.modifiers().modified().then_some(window.modifiers()),
197        }
198    }
199
200    fn handle_modifiers_changed(
201        &mut self,
202        event: &ModifiersChangedEvent,
203        window: &mut Window,
204        cx: &mut Context<Self>,
205    ) {
206        let Some(init_modifiers) = self.init_modifiers.take() else {
207            return;
208        };
209        if self.picker.read(cx).delegate.has_changed_selected_index
210            && (!event.modified() || !init_modifiers.is_subset_of(event))
211        {
212            self.init_modifiers = None;
213            window.dispatch_action(menu::Confirm.boxed_clone(), cx);
214        }
215    }
216
217    fn handle_select_prev(
218        &mut self,
219        _: &SelectPrevious,
220        window: &mut Window,
221        cx: &mut Context<Self>,
222    ) {
223        self.init_modifiers = Some(window.modifiers());
224        window.dispatch_action(Box::new(menu::SelectPrevious), cx);
225    }
226
227    fn handle_toggle_ignored(
228        &mut self,
229        _: &ToggleIncludeIgnored,
230        window: &mut Window,
231        cx: &mut Context<Self>,
232    ) {
233        self.picker.update(cx, |picker, cx| {
234            picker.delegate.include_ignored = match picker.delegate.include_ignored {
235                Some(true) => FileFinderSettings::get_global(cx)
236                    .include_ignored
237                    .map(|_| false),
238                Some(false) => Some(true),
239                None => Some(true),
240            };
241            picker.delegate.include_ignored_refresh =
242                picker.delegate.update_matches(picker.query(cx), window, cx);
243        });
244    }
245
246    fn go_to_file_split_left(
247        &mut self,
248        _: &pane::SplitLeft,
249        window: &mut Window,
250        cx: &mut Context<Self>,
251    ) {
252        self.go_to_file_split_inner(SplitDirection::Left, window, cx)
253    }
254
255    fn go_to_file_split_right(
256        &mut self,
257        _: &pane::SplitRight,
258        window: &mut Window,
259        cx: &mut Context<Self>,
260    ) {
261        self.go_to_file_split_inner(SplitDirection::Right, window, cx)
262    }
263
264    fn go_to_file_split_up(
265        &mut self,
266        _: &pane::SplitUp,
267        window: &mut Window,
268        cx: &mut Context<Self>,
269    ) {
270        self.go_to_file_split_inner(SplitDirection::Up, window, cx)
271    }
272
273    fn go_to_file_split_down(
274        &mut self,
275        _: &pane::SplitDown,
276        window: &mut Window,
277        cx: &mut Context<Self>,
278    ) {
279        self.go_to_file_split_inner(SplitDirection::Down, window, cx)
280    }
281
282    fn go_to_file_split_inner(
283        &mut self,
284        split_direction: SplitDirection,
285        window: &mut Window,
286        cx: &mut Context<Self>,
287    ) {
288        self.picker.update(cx, |picker, cx| {
289            let delegate = &mut picker.delegate;
290            if !delegate.selected_matches.is_empty() {
291                delegate.open_selected_in_one_split(split_direction, window, cx);
292                return;
293            }
294            if let Some(workspace) = delegate.workspace.upgrade()
295                && let Some(m) = delegate.matches.get(delegate.selected_index())
296            {
297                let path = match m {
298                    Match::History { path, .. } => {
299                        let worktree_id = path.project.worktree_id;
300                        ProjectPath {
301                            worktree_id,
302                            path: Arc::clone(&path.project.path),
303                        }
304                    }
305                    Match::Search(m) => project_path_for_search_match(&delegate.project, &m.0, cx),
306                    Match::CreateNew(p) => p.clone(),
307                    Match::Channel { .. } => return,
308                };
309                let open_task = workspace.update(cx, move |workspace, cx| {
310                    workspace.split_path_preview(path, false, Some(split_direction), window, cx)
311                });
312                open_task.detach_and_log_err(cx);
313            }
314        })
315    }
316
317    fn open_without_dismiss(
318        &mut self,
319        _: &OpenWithoutDismiss,
320        window: &mut Window,
321        cx: &mut Context<Self>,
322    ) {
323        self.picker.update(cx, |picker, cx| {
324            picker.delegate.confirm_without_dismiss(window, cx);
325        });
326    }
327
328    pub fn modal_max_width(width_setting: FileFinderWidth, window: &mut Window) -> Pixels {
329        let window_width = window.viewport_size().width;
330        let small_width = rems(34.).to_pixels(window.rem_size());
331
332        match width_setting {
333            FileFinderWidth::Small => small_width,
334            FileFinderWidth::Full => window_width,
335            FileFinderWidth::XLarge => (window_width - px(512.)).max(small_width),
336            FileFinderWidth::Large => (window_width - px(768.)).max(small_width),
337            FileFinderWidth::Medium => (window_width - px(1024.)).max(small_width),
338        }
339    }
340}
341
342impl EventEmitter<DismissEvent> for FileFinder {}
343
344impl Focusable for FileFinder {
345    fn focus_handle(&self, _: &App) -> FocusHandle {
346        self.picker_focus_handle.clone()
347    }
348}
349
350impl Render for FileFinder {
351    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
352        let key_context = self.picker.read(cx).delegate.key_context(window, cx);
353
354        v_flex()
355            .key_context(key_context)
356            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
357            .on_action(cx.listener(Self::handle_select_prev))
358            .on_action(cx.listener(Self::handle_toggle_ignored))
359            .on_action(cx.listener(Self::go_to_file_split_left))
360            .on_action(cx.listener(Self::go_to_file_split_right))
361            .on_action(cx.listener(Self::go_to_file_split_up))
362            .on_action(cx.listener(Self::go_to_file_split_down))
363            .on_action(cx.listener(Self::open_without_dismiss))
364            .child(self.picker.clone())
365    }
366}
367
368pub struct FileFinderDelegate {
369    file_finder: WeakEntity<FileFinder>,
370    workspace: WeakEntity<Workspace>,
371    project: Entity<Project>,
372    channel_store: Option<Entity<ChannelStore>>,
373    search_count: usize,
374    latest_search_id: usize,
375    latest_search_did_cancel: bool,
376    latest_search_query: Option<FileSearchQuery>,
377    currently_opened_path: Option<FoundPath>,
378    matches: Matches,
379    selected_matches: Vec<SelectedMatch>,
380    selected_index: usize,
381    has_changed_selected_index: bool,
382    cancel_flag: Arc<AtomicBool>,
383    search_in_flight: Arc<AtomicBool>,
384    history_items: Vec<FoundPath>,
385    separate_history: bool,
386    first_update: bool,
387    focus_handle: FocusHandle,
388    include_ignored: Option<bool>,
389    include_ignored_refresh: Task<()>,
390    debounce_next_refresh: bool,
391}
392
393/// Use a custom ordering for file finder: the regular one
394/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:
395/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`
396///
397/// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:
398/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`
399/// as the files are shown in the project panel lists.
400#[derive(Debug, Clone, PartialEq, Eq)]
401struct ProjectPanelOrdMatch(PathMatch);
402
403impl Ord for ProjectPanelOrdMatch {
404    fn cmp(&self, other: &Self) -> cmp::Ordering {
405        self.0
406            .score
407            .partial_cmp(&other.0.score)
408            .unwrap_or(cmp::Ordering::Equal)
409            .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))
410            .then_with(|| {
411                other
412                    .0
413                    .distance_to_relative_ancestor
414                    .cmp(&self.0.distance_to_relative_ancestor)
415            })
416            .then_with(|| self.0.path.cmp(&other.0.path).reverse())
417    }
418}
419
420impl PartialOrd for ProjectPanelOrdMatch {
421    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
422        Some(self.cmp(other))
423    }
424}
425
426#[derive(Debug, Default)]
427struct Matches {
428    separate_history: bool,
429    matches: Vec<Match>,
430}
431
432#[derive(Debug, Clone)]
433enum Match {
434    History {
435        path: FoundPath,
436        panel_match: Option<ProjectPanelOrdMatch>,
437    },
438    Search(ProjectPanelOrdMatch),
439    Channel {
440        channel_id: ChannelId,
441        channel_name: SharedString,
442        string_match: StringMatch,
443    },
444    CreateNew(ProjectPath),
445}
446
447impl Match {
448    fn relative_path(&self) -> Option<&Arc<RelPath>> {
449        match self {
450            Match::History { path, .. } => Some(&path.project.path),
451            Match::Search(panel_match) => Some(&panel_match.0.path),
452            Match::Channel { .. } | Match::CreateNew(_) => None,
453        }
454    }
455
456    fn abs_path(&self, project: &Entity<Project>, cx: &App) -> Option<PathBuf> {
457        match self {
458            Match::History { path, .. } => Some(path.absolute.clone()),
459            Match::Search(ProjectPanelOrdMatch(path_match)) => Some(
460                project
461                    .read(cx)
462                    .worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?
463                    .read(cx)
464                    .absolutize(&path_match.path),
465            ),
466            Match::Channel { .. } | Match::CreateNew(_) => None,
467        }
468    }
469
470    fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> {
471        match self {
472            Match::History { panel_match, .. } => panel_match.as_ref(),
473            Match::Search(panel_match) => Some(panel_match),
474            Match::Channel { .. } | Match::CreateNew(_) => None,
475        }
476    }
477}
478
479/// Wrapper with Eq comparing the worktree-qualified path of file matches
480#[derive(Clone)]
481struct SelectedMatch(pub Match);
482
483impl SelectedMatch {
484    fn new(m: Match) -> Option<Self> {
485        match m {
486            Match::History { .. } | Match::Search(_) => Some(Self(m)),
487            Match::Channel { .. } | Match::CreateNew(_) => None,
488        }
489    }
490}
491
492impl PartialEq for SelectedMatch {
493    fn eq(&self, other: &Self) -> bool {
494        *self == other.0
495    }
496}
497
498impl Eq for SelectedMatch {}
499
500impl PartialEq<Match> for SelectedMatch {
501    fn eq(&self, other: &Match) -> bool {
502        match (&self.0, other) {
503            (Match::History { path: a, .. }, Match::History { path: b, .. }) => {
504                a.project == b.project
505            }
506            (Match::Search(a), Match::Search(b)) => {
507                a.0.worktree_id == b.0.worktree_id && a.0.path == b.0.path
508            }
509            (Match::History { path: h, .. }, Match::Search(s))
510            | (Match::Search(s), Match::History { path: h, .. }) => {
511                h.project.worktree_id.to_usize() == s.0.worktree_id && h.project.path == s.0.path
512            }
513            _ => false,
514        }
515    }
516}
517
518impl Matches {
519    fn len(&self) -> usize {
520        self.matches.len()
521    }
522
523    fn get(&self, index: usize) -> Option<&Match> {
524        self.matches.get(index)
525    }
526
527    fn position(
528        &self,
529        entry: &Match,
530        currently_opened: Option<&FoundPath>,
531    ) -> Result<usize, usize> {
532        if let Match::History {
533            path,
534            panel_match: None,
535        } = entry
536        {
537            // Slow case: linear search by path. Should not happen actually,
538            // since we call `position` only if matches set changed, but the query has not changed.
539            // And History entries do not have panel_match if query is empty, so there's no
540            // reason for the matches set to change.
541            self.matches
542                .iter()
543                .position(|m| match m.relative_path() {
544                    Some(p) => path.project.path == *p,
545                    None => false,
546                })
547                .ok_or(0)
548        } else {
549            self.matches.binary_search_by(|m| {
550                // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b.
551                // And we want the better entries go first.
552                Self::cmp_matches(self.separate_history, currently_opened, m, entry).reverse()
553            })
554        }
555    }
556
557    fn push_new_matches<'a>(
558        &'a mut self,
559        worktree_store: Entity<WorktreeStore>,
560        cx: &'a App,
561        history_items: impl IntoIterator<Item = &'a FoundPath> + Clone,
562        currently_opened: Option<&'a FoundPath>,
563        query: Option<&FileSearchQuery>,
564        new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
565        extend_old_matches: bool,
566        path_style: PathStyle,
567    ) {
568        let Some(query) = query else {
569            // assuming that if there's no query, then there's no search matches.
570            self.matches.clear();
571            let path_to_entry = |found_path: &FoundPath| Match::History {
572                path: found_path.clone(),
573                panel_match: None,
574            };
575
576            self.matches
577                .extend(history_items.into_iter().map(path_to_entry));
578            return;
579        };
580
581        let worktree_name_by_id = worktree_names_for_history_matching(&worktree_store, cx);
582        let new_history_matches = matching_history_items(
583            history_items,
584            currently_opened,
585            worktree_name_by_id,
586            query,
587            path_style,
588        );
589        let new_search_matches: Vec<Match> = new_search_matches
590            .filter(|path_match| {
591                !new_history_matches.contains_key(&ProjectPath {
592                    path: path_match.0.path.clone(),
593                    worktree_id: WorktreeId::from_usize(path_match.0.worktree_id),
594                })
595            })
596            .map(Match::Search)
597            .collect();
598
599        if extend_old_matches {
600            // since we take history matches instead of new search matches
601            // and history matches has not changed(since the query has not changed and we do not extend old matches otherwise),
602            // old matches can't contain paths present in history_matches as well.
603            self.matches.retain(|m| matches!(m, Match::Search(_)));
604        } else {
605            self.matches.clear();
606        }
607
608        // At this point we have an unsorted set of new history matches, an unsorted set of new search matches
609        // and a sorted set of old search matches.
610        // It is possible that the new search matches' paths contain some of the old search matches' paths.
611        // History matches' paths are unique, since store in a HashMap by path.
612        // We build a sorted Vec<Match>, eliminating duplicate search matches.
613        // Search matches with the same paths should have equal `ProjectPanelOrdMatch`, so we should
614        // not have any duplicates after building the final list.
615        for new_match in new_history_matches.into_values().chain(new_search_matches) {
616            match self.position(&new_match, currently_opened) {
617                Ok(_duplicate) => continue,
618                Err(i) => {
619                    self.matches.insert(i, new_match);
620                    if self.matches.len() == 100 {
621                        break;
622                    }
623                }
624            }
625        }
626    }
627
628    /// If a < b, then a is a worse match, aligning with the `ProjectPanelOrdMatch` ordering.
629    fn cmp_matches(
630        separate_history: bool,
631        currently_opened: Option<&FoundPath>,
632        a: &Match,
633        b: &Match,
634    ) -> cmp::Ordering {
635        // Handle CreateNew variant - always put it at the end
636        match (a, b) {
637            (Match::CreateNew(_), _) => return cmp::Ordering::Less,
638            (_, Match::CreateNew(_)) => return cmp::Ordering::Greater,
639            _ => {}
640        }
641
642        match (&a, &b) {
643            // bubble currently opened files to the top
644            (Match::History { path, .. }, _) if Some(path) == currently_opened => {
645                return cmp::Ordering::Greater;
646            }
647            (_, Match::History { path, .. }) if Some(path) == currently_opened => {
648                return cmp::Ordering::Less;
649            }
650
651            _ => {}
652        }
653
654        if separate_history {
655            match (a, b) {
656                (Match::History { .. }, Match::Search(_)) => return cmp::Ordering::Greater,
657                (Match::Search(_), Match::History { .. }) => return cmp::Ordering::Less,
658
659                _ => {}
660            }
661        }
662
663        // For file-vs-file matches, use the existing detailed comparison.
664        if let (Some(a_panel), Some(b_panel)) = (a.panel_match(), b.panel_match()) {
665            return a_panel.cmp(b_panel);
666        }
667
668        let a_score = Self::match_score(a);
669        let b_score = Self::match_score(b);
670        // When at least one side is a channel, compare by raw score.
671        a_score
672            .partial_cmp(&b_score)
673            .unwrap_or(cmp::Ordering::Equal)
674    }
675
676    fn match_score(m: &Match) -> f64 {
677        match m {
678            Match::History { panel_match, .. } => panel_match.as_ref().map_or(0.0, |pm| pm.0.score),
679            Match::Search(pm) => pm.0.score,
680            Match::Channel { string_match, .. } => string_match.score,
681            Match::CreateNew(_) => 0.0,
682        }
683    }
684}
685
686fn matching_history_items<'a>(
687    history_items: impl IntoIterator<Item = &'a FoundPath>,
688    currently_opened: Option<&'a FoundPath>,
689    worktree_name_by_id: Option<HashMap<WorktreeId, Arc<RelPath>>>,
690    query: &FileSearchQuery,
691    path_style: PathStyle,
692) -> HashMap<ProjectPath, Match> {
693    let mut candidates_paths = HashMap::default();
694
695    let history_items_by_worktrees = history_items
696        .into_iter()
697        .chain(currently_opened)
698        .map(|found_path| {
699            // Only match history items names, otherwise their paths may match too many queries,
700            // producing false positives. E.g. `foo` would match both `something/foo/bar.rs` and
701            // `something/foo/foo.rs` and if the former is a history item, it would be shown first
702            // always, despite the latter being a better match.
703            let candidate = PathMatchCandidate::new(
704                &found_path.project.path,
705                false,
706                worktree_name_by_id
707                    .as_ref()
708                    .and_then(|m| m.get(&found_path.project.worktree_id))
709                    .map(|prefix| prefix.as_ref()),
710            );
711            candidates_paths.insert(&found_path.project, found_path);
712            (found_path.project.worktree_id, candidate)
713        })
714        .fold(
715            HashMap::default(),
716            |mut candidates, (worktree_id, new_candidate)| {
717                candidates
718                    .entry(worktree_id)
719                    .or_insert_with(Vec::new)
720                    .push(new_candidate);
721                candidates
722            },
723        );
724    let mut matching_history_paths = HashMap::default();
725    for (worktree, candidates) in history_items_by_worktrees {
726        let max_results = candidates.len() + 1;
727        let worktree_root_name = worktree_name_by_id
728            .as_ref()
729            .and_then(|w| w.get(&worktree).cloned());
730
731        matching_history_paths.extend(
732            fuzzy_nucleo::match_fixed_path_set(
733                candidates,
734                worktree.to_usize(),
735                worktree_root_name,
736                query.path_query(),
737                fuzzy_nucleo::Case::Ignore,
738                max_results,
739                path_style,
740            )
741            .into_iter()
742            // filter matches where at least one matched position is in filename portion, to prevent directory matches, nucleo scores them higher as history items are matched against their full path
743            .filter(|path_match| {
744                if let Some(filename) = path_match.path.file_name() {
745                    let filename_start = path_match.path.as_unix_str().len() - filename.len();
746                    path_match
747                        .positions
748                        .iter()
749                        .any(|&pos| pos >= filename_start)
750                } else {
751                    true
752                }
753            })
754            .filter_map(|path_match| {
755                let worktree_id = WorktreeId::from_usize(path_match.worktree_id);
756                let project_path = ProjectPath {
757                    worktree_id,
758                    path: Arc::clone(&path_match.path),
759                };
760                // For single-file worktrees, fuzzy_nucleo moves the worktree root name
761                // into path_match.path (root_is_file handling), so the stored key of ""
762                // won't match. Fall back to an empty-path lookup for those entries.
763                let (_, found_path) =
764                    candidates_paths.remove_entry(&project_path).or_else(|| {
765                        candidates_paths.remove_entry(&ProjectPath {
766                            worktree_id,
767                            path: RelPath::empty_arc(),
768                        })
769                    })?;
770                // Key with path_match.path so the deduplication check in push_new_matches
771                // (which also uses path_match.path) correctly suppresses the search duplicate.
772                Some((
773                    project_path,
774                    Match::History {
775                        path: found_path.clone(),
776                        panel_match: Some(ProjectPanelOrdMatch(path_match)),
777                    },
778                ))
779            }),
780        );
781    }
782    matching_history_paths
783}
784
785fn should_hide_root_in_entry_path(worktree_store: &Entity<WorktreeStore>, cx: &App) -> bool {
786    let multiple_worktrees = worktree_store
787        .read(cx)
788        .visible_worktrees(cx)
789        .filter(|worktree| !worktree.read(cx).is_single_file())
790        .nth(1)
791        .is_some();
792    ProjectPanelSettings::get_global(cx).hide_root && !multiple_worktrees
793}
794
795fn worktree_names_for_history_matching(
796    worktree_store: &Entity<WorktreeStore>,
797    cx: &App,
798) -> Option<HashMap<WorktreeId, Arc<RelPath>>> {
799    let hide_root = should_hide_root_in_entry_path(worktree_store, cx);
800    let names = worktree_store
801        .read(cx)
802        .worktrees()
803        .filter_map(|worktree| {
804            let worktree = worktree.read(cx);
805            if hide_root && !worktree.is_single_file() {
806                None
807            } else {
808                Some((worktree.id(), worktree.root_name().into()))
809            }
810        })
811        .collect::<HashMap<_, _>>();
812
813    if names.is_empty() { None } else { Some(names) }
814}
815
816fn project_path_for_search_match(
817    project: &Entity<Project>,
818    path_match: &PathMatch,
819    cx: &App,
820) -> ProjectPath {
821    let worktree_id = WorktreeId::from_usize(path_match.worktree_id);
822    let path = if project
823        .read(cx)
824        .worktree_for_id(worktree_id, cx)
825        .is_some_and(|worktree| worktree.read(cx).is_single_file())
826    {
827        RelPath::empty_arc()
828    } else {
829        path_match.path.clone()
830    };
831
832    ProjectPath { worktree_id, path }
833}
834
835#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
836struct FoundPath {
837    project: ProjectPath,
838    absolute: PathBuf,
839}
840
841impl FoundPath {
842    fn new(project: ProjectPath, absolute: PathBuf) -> Self {
843        Self { project, absolute }
844    }
845}
846
847const MAX_RECENT_SELECTIONS: usize = 20;
848const SEARCH_DEBOUNCE: Duration = Duration::from_millis(100);
849const WORKTREE_UPDATE_REFRESH_DEBOUNCE: Duration = Duration::from_millis(200);
850
851pub enum Event {
852    Selected(ProjectPath),
853    Dismissed,
854}
855
856#[derive(Debug, Clone)]
857struct FileSearchQuery {
858    raw_query: String,
859    file_query_end: Option<usize>,
860    path_position: PathWithPosition,
861    line_range: Option<RangeInclusive<u32>>,
862}
863
864impl FileSearchQuery {
865    fn path_query(&self) -> &str {
866        match self.file_query_end {
867            Some(file_path_end) => &self.raw_query[..file_path_end],
868            None => &self.raw_query,
869        }
870    }
871
872    fn selection_range(&self, buffer_snapshot: &BufferSnapshot) -> Option<Range<Point>> {
873        if let Some(line_range) = self.line_range.clone() {
874            return Some(buffer_range_for_line_range(buffer_snapshot, line_range));
875        }
876
877        let row = self.path_position.row.map(|row| row.saturating_sub(1))?;
878        let col = self.path_position.column.unwrap_or(0).saturating_sub(1);
879        let point = buffer_snapshot.point_from_external_input(row, col);
880        Some(point..point)
881    }
882}
883
884fn parse_file_search_query(raw_query: &str) -> FileSearchQuery {
885    let raw_query = raw_query.trim().trim_end_matches(':').to_owned();
886
887    if let Some((path_query, start_line, end_line)) = parse_line_range_query(&raw_query) {
888        let path_query = path_query.to_owned();
889        return FileSearchQuery {
890            raw_query,
891            file_query_end: Some(path_query.len()),
892            path_position: PathWithPosition {
893                path: PathBuf::from(&path_query),
894                row: Some(start_line),
895                column: None,
896            },
897            line_range: end_line.map(|end| start_line..=end),
898        };
899    }
900
901    let path_position = PathWithPosition::parse_str(&raw_query);
902    let path_str = path_position.path.to_str();
903    let path_trimmed = path_str.unwrap_or(&raw_query).trim_end_matches(':');
904    let file_query_end = if path_trimmed == raw_query {
905        None
906    } else {
907        path_str.map(str::len)
908    };
909
910    FileSearchQuery {
911        raw_query,
912        file_query_end,
913        path_position,
914        line_range: None,
915    }
916}
917
918fn parse_line_range_query(raw_query: &str) -> Option<(&str, u32, Option<u32>)> {
919    let (path_query, line_range) = raw_query.rsplit_once(':')?;
920    if path_query.is_empty() {
921        return None;
922    }
923
924    let (start_line, end_line) = line_range.split_once('-')?;
925    let start_line = start_line.parse::<u32>().ok()?;
926    if start_line == 0 {
927        return None;
928    }
929
930    let end_line = end_line
931        .parse::<u32>()
932        .ok()
933        .filter(|&end| end != 0 && end >= start_line);
934
935    Some((path_query, start_line, end_line))
936}
937
938fn buffer_range_for_line_range(
939    buffer_snapshot: &BufferSnapshot,
940    line_range: RangeInclusive<u32>,
941) -> Range<Point> {
942    let max_point = buffer_snapshot.max_point();
943    let start_line = line_range.start().saturating_sub(1);
944    let start_point = if start_line > max_point.row {
945        max_point
946    } else {
947        Point::new(start_line, 0)
948    };
949    let end_line = *line_range.end();
950    let end_point = if end_line > max_point.row {
951        max_point
952    } else {
953        Point::new(end_line, 0)
954    };
955
956    start_point..end_point
957}
958
959impl FileFinderDelegate {
960    fn new(
961        file_finder: WeakEntity<FileFinder>,
962        workspace: WeakEntity<Workspace>,
963        project: Entity<Project>,
964        currently_opened_path: Option<FoundPath>,
965        history_items: Vec<FoundPath>,
966        separate_history: bool,
967        include_ignored: Option<bool>,
968        window: &mut Window,
969        cx: &mut Context<FileFinder>,
970    ) -> Self {
971        Self::subscribe_to_updates(&project, window, cx);
972        let channel_store = if FileFinderSettings::get_global(cx).include_channels {
973            ChannelStore::try_global(cx)
974        } else {
975            None
976        };
977        Self {
978            file_finder,
979            workspace,
980            project,
981            channel_store,
982            search_count: 0,
983            latest_search_id: 0,
984            latest_search_did_cancel: false,
985            latest_search_query: None,
986            currently_opened_path,
987            matches: Matches::default(),
988            selected_matches: Vec::new(),
989            has_changed_selected_index: false,
990            selected_index: 0,
991            cancel_flag: Arc::new(AtomicBool::new(false)),
992            search_in_flight: Arc::new(AtomicBool::new(false)),
993            history_items,
994            separate_history,
995            first_update: true,
996            focus_handle: cx.focus_handle(),
997            include_ignored: include_ignored.or(FileFinderSettings::get_global(cx).include_ignored),
998            include_ignored_refresh: Task::ready(()),
999            debounce_next_refresh: false,
1000        }
1001    }
1002
1003    fn subscribe_to_updates(
1004        project: &Entity<Project>,
1005        window: &mut Window,
1006        cx: &mut Context<FileFinder>,
1007    ) {
1008        cx.subscribe_in(project, window, |file_finder, _, event, window, cx| {
1009            match event {
1010                project::Event::WorktreeUpdatedEntries(_, _) => {
1011                    file_finder.picker.update(cx, |picker, cx| {
1012                        picker.delegate.debounce_next_refresh = true;
1013                        picker.refresh(window, cx);
1014                    })
1015                }
1016                project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
1017                    file_finder
1018                        .picker
1019                        .update(cx, |picker, cx| picker.refresh(window, cx))
1020                }
1021                _ => {}
1022            };
1023        })
1024        .detach();
1025    }
1026
1027    fn prepend_selected_matches(&mut self) {
1028        if self.selected_matches.is_empty() {
1029            return;
1030        }
1031        self.matches
1032            .matches
1033            .retain(|m| !self.selected_matches.iter().any(|selected| selected == m));
1034        let mut new: Vec<Match> = self
1035            .selected_matches
1036            .iter()
1037            .map(|selected| selected.0.clone())
1038            .collect();
1039        new.append(&mut self.matches.matches);
1040        self.matches.matches = new;
1041    }
1042
1043    fn spawn_search(
1044        &mut self,
1045        query: FileSearchQuery,
1046        window: &mut Window,
1047        cx: &mut Context<Picker<Self>>,
1048    ) -> Task<()> {
1049        let relative_to = self
1050            .currently_opened_path
1051            .as_ref()
1052            .map(|found_path| Arc::clone(&found_path.project.path));
1053        let worktree_store = self.project.read(cx).worktree_store();
1054        let worktrees = worktree_store
1055            .read(cx)
1056            .visible_worktrees_and_single_files(cx)
1057            .collect::<Vec<_>>();
1058        let include_root_name = !should_hide_root_in_entry_path(&worktree_store, cx);
1059        let candidate_sets = worktrees
1060            .into_iter()
1061            .map(|worktree| {
1062                let worktree = worktree.read(cx);
1063                PathMatchCandidateSet {
1064                    snapshot: worktree.snapshot(),
1065                    include_ignored: self.include_ignored.unwrap_or_else(|| {
1066                        worktree.root_entry().is_some_and(|entry| entry.is_ignored)
1067                    }),
1068                    include_root_name,
1069                    candidates: project::Candidates::Files,
1070                }
1071            })
1072            .collect::<Vec<_>>();
1073
1074        let search_id = util::post_inc(&mut self.search_count);
1075        self.cancel_flag.store(true, atomic::Ordering::Release);
1076        self.cancel_flag = Arc::new(AtomicBool::new(false));
1077        let cancel_flag = self.cancel_flag.clone();
1078        cx.spawn_in(window, async move |picker, cx| {
1079            let matches = fuzzy_nucleo::match_path_sets(
1080                candidate_sets.as_slice(),
1081                query.path_query(),
1082                &relative_to,
1083                fuzzy_nucleo::Case::Ignore,
1084                100,
1085                &cancel_flag,
1086                cx.background_executor().clone(),
1087            )
1088            .await
1089            .into_iter()
1090            .map(ProjectPanelOrdMatch);
1091            let did_cancel = cancel_flag.load(atomic::Ordering::Acquire);
1092            picker
1093                .update(cx, |picker, cx| {
1094                    picker
1095                        .delegate
1096                        .set_search_matches(search_id, did_cancel, query, matches, cx)
1097                })
1098                .log_err();
1099        })
1100    }
1101
1102    fn set_search_matches(
1103        &mut self,
1104        search_id: usize,
1105        did_cancel: bool,
1106        query: FileSearchQuery,
1107        matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
1108        cx: &mut Context<Picker<Self>>,
1109    ) {
1110        if search_id >= self.latest_search_id {
1111            self.latest_search_id = search_id;
1112            let query_changed = Some(query.path_query())
1113                != self
1114                    .latest_search_query
1115                    .as_ref()
1116                    .map(|query| query.path_query());
1117            let extend_old_matches = self.latest_search_did_cancel && !query_changed;
1118
1119            let selected_match = if query_changed {
1120                None
1121            } else {
1122                self.matches.get(self.selected_index).cloned()
1123            };
1124
1125            let path_style = self.project.read(cx).path_style(cx);
1126            self.matches.push_new_matches(
1127                self.project.read(cx).worktree_store(),
1128                cx,
1129                &self.history_items,
1130                self.currently_opened_path.as_ref(),
1131                Some(&query),
1132                matches.into_iter(),
1133                extend_old_matches,
1134                path_style,
1135            );
1136
1137            // Add channel matches
1138            if let Some(channel_store) = &self.channel_store {
1139                let channel_store = channel_store.read(cx);
1140                let channels: Vec<_> = channel_store.channels().cloned().collect();
1141                if !channels.is_empty() {
1142                    let candidates = channels
1143                        .iter()
1144                        .enumerate()
1145                        .map(|(id, channel)| StringMatchCandidate::new(id, &channel.name));
1146                    let channel_query = query.path_query();
1147                    let query_lower = channel_query.to_lowercase();
1148                    let mut channel_matches = Vec::new();
1149                    for candidate in candidates {
1150                        let channel_name = candidate.string;
1151                        let name_lower = channel_name.to_lowercase();
1152
1153                        let mut positions = Vec::new();
1154                        let mut query_idx = 0;
1155                        for (name_idx, name_char) in name_lower.char_indices() {
1156                            if query_idx < query_lower.len() {
1157                                let query_char =
1158                                    query_lower[query_idx..].chars().next().unwrap_or_default();
1159                                if name_char == query_char {
1160                                    positions.push(name_idx);
1161                                    query_idx += query_char.len_utf8();
1162                                }
1163                            }
1164                        }
1165
1166                        if query_idx == query_lower.len() {
1167                            let channel = &channels[candidate.id];
1168                            let score = if name_lower == query_lower {
1169                                1.0
1170                            } else if name_lower.starts_with(&query_lower) {
1171                                0.8
1172                            } else {
1173                                0.5 * (query_lower.len() as f64 / name_lower.len() as f64)
1174                            };
1175                            channel_matches.push(Match::Channel {
1176                                channel_id: channel.id,
1177                                channel_name: channel.name.clone(),
1178                                string_match: StringMatch {
1179                                    candidate_id: candidate.id,
1180                                    score,
1181                                    positions,
1182                                    string: channel_name,
1183                                },
1184                            });
1185                        }
1186                    }
1187                    for channel_match in channel_matches {
1188                        match self
1189                            .matches
1190                            .position(&channel_match, self.currently_opened_path.as_ref())
1191                        {
1192                            Ok(_duplicate) => {}
1193                            Err(ix) => self.matches.matches.insert(ix, channel_match),
1194                        }
1195                    }
1196                }
1197            }
1198
1199            let query_path = query.raw_query.as_str();
1200            if let Ok(mut query_path) = RelPath::new(Path::new(query_path), path_style) {
1201                let available_worktree = self
1202                    .project
1203                    .read(cx)
1204                    .visible_worktrees(cx)
1205                    .filter(|worktree| !worktree.read(cx).is_single_file())
1206                    .collect::<Vec<_>>();
1207                let worktree_count = available_worktree.len();
1208                let mut expect_worktree = available_worktree.first().cloned();
1209                for worktree in &available_worktree {
1210                    let worktree_root = worktree.read(cx).root_name();
1211                    if worktree_count > 1 {
1212                        if let Ok(suffix) = query_path.strip_prefix(worktree_root) {
1213                            query_path = Cow::Owned(suffix.to_owned());
1214                            expect_worktree = Some(worktree.clone());
1215                            break;
1216                        }
1217                    }
1218                }
1219
1220                if let Some(FoundPath { ref project, .. }) = self.currently_opened_path {
1221                    let worktree_id = project.worktree_id;
1222                    let focused_file_in_available_worktree = available_worktree
1223                        .iter()
1224                        .any(|wt| wt.read(cx).id() == worktree_id);
1225
1226                    if focused_file_in_available_worktree {
1227                        expect_worktree = self.project.read(cx).worktree_for_id(worktree_id, cx);
1228                    }
1229                }
1230
1231                if let Some(worktree) = expect_worktree {
1232                    let worktree = worktree.read(cx);
1233                    if worktree.entry_for_path(&query_path).is_none()
1234                        && !query.raw_query.ends_with("/")
1235                        && !(path_style.is_windows() && query.raw_query.ends_with("\\"))
1236                    {
1237                        self.matches.matches.push(Match::CreateNew(ProjectPath {
1238                            worktree_id: worktree.id(),
1239                            path: query_path.into_arc(),
1240                        }));
1241                    }
1242                }
1243            }
1244
1245            self.prepend_selected_matches();
1246
1247            self.selected_index = if !self.selected_matches.is_empty() {
1248                0
1249            } else {
1250                selected_match.map_or_else(
1251                    || self.calculate_selected_index(cx),
1252                    |m| {
1253                        self.matches
1254                            .position(&m, self.currently_opened_path.as_ref())
1255                            .unwrap_or(0)
1256                    },
1257                )
1258            };
1259
1260            self.latest_search_query = Some(query);
1261            self.latest_search_did_cancel = did_cancel;
1262
1263            cx.notify();
1264        }
1265    }
1266
1267    fn labels_for_match(
1268        &self,
1269        path_match: &Match,
1270        window: &mut Window,
1271        cx: &App,
1272    ) -> (HighlightedLabel, HighlightedLabel) {
1273        let path_style = self.project.read(cx).path_style(cx);
1274        let (file_name, file_name_positions, mut full_path, mut full_path_positions) =
1275            match &path_match {
1276                Match::History {
1277                    path: entry_path,
1278                    panel_match,
1279                } => {
1280                    let worktree_id = entry_path.project.worktree_id;
1281                    let worktree = self
1282                        .project
1283                        .read(cx)
1284                        .worktree_for_id(worktree_id, cx)
1285                        .filter(|worktree| worktree.read(cx).is_visible());
1286
1287                    if let Some(panel_match) = panel_match {
1288                        self.labels_for_path_match(&panel_match.0, path_style)
1289                    } else if let Some(worktree) = worktree {
1290                        let worktree_store = self.project.read(cx).worktree_store();
1291                        let full_path = if should_hide_root_in_entry_path(&worktree_store, cx) {
1292                            entry_path.project.path.clone()
1293                        } else {
1294                            worktree
1295                                .read(cx)
1296                                .root_name()
1297                                .join(&entry_path.project.path)
1298                                .into()
1299                        };
1300                        let mut components = full_path.components();
1301                        let filename = components.next_back().unwrap_or("");
1302                        let prefix = components.rest();
1303                        (
1304                            filename.to_string(),
1305                            Vec::new(),
1306                            prefix.display(path_style).to_string() + path_style.primary_separator(),
1307                            Vec::new(),
1308                        )
1309                    } else {
1310                        (
1311                            entry_path
1312                                .absolute
1313                                .file_name()
1314                                .map_or(String::new(), |f| f.to_string_lossy().into_owned()),
1315                            Vec::new(),
1316                            entry_path.absolute.parent().map_or(String::new(), |path| {
1317                                path.to_string_lossy().into_owned() + path_style.primary_separator()
1318                            }),
1319                            Vec::new(),
1320                        )
1321                    }
1322                }
1323                Match::Search(path_match) => self.labels_for_path_match(&path_match.0, path_style),
1324                Match::Channel {
1325                    channel_name,
1326                    string_match,
1327                    ..
1328                } => (
1329                    channel_name.to_string(),
1330                    string_match.positions.clone(),
1331                    "Channel Notes".to_string(),
1332                    vec![],
1333                ),
1334                Match::CreateNew(project_path) => (
1335                    format!("Create File: {}", project_path.path.display(path_style)),
1336                    vec![],
1337                    String::from(""),
1338                    vec![],
1339                ),
1340            };
1341
1342        if file_name_positions.is_empty() {
1343            let user_home_path = util::paths::home_dir().to_string_lossy();
1344            if !user_home_path.is_empty() && full_path.starts_with(&*user_home_path) {
1345                full_path.replace_range(0..user_home_path.len(), "~");
1346                full_path_positions.retain_mut(|pos| {
1347                    if *pos >= user_home_path.len() {
1348                        *pos -= user_home_path.len();
1349                        *pos += 1;
1350                        true
1351                    } else {
1352                        false
1353                    }
1354                })
1355            }
1356        }
1357
1358        if full_path.is_ascii() {
1359            let file_finder_settings = FileFinderSettings::get_global(cx);
1360            let max_width =
1361                FileFinder::modal_max_width(file_finder_settings.modal_max_width, window);
1362            let (normal_em, small_em) = {
1363                let style = window.text_style();
1364                let font_id = window.text_system().resolve_font(&style.font());
1365                let font_size = TextSize::Default.rems(cx).to_pixels(window.rem_size());
1366                let normal = cx
1367                    .text_system()
1368                    .em_width(font_id, font_size)
1369                    .unwrap_or(px(16.));
1370                let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
1371                let small = cx
1372                    .text_system()
1373                    .em_width(font_id, font_size)
1374                    .unwrap_or(px(10.));
1375                (normal, small)
1376            };
1377            let budget = full_path_budget(&file_name, normal_em, small_em, max_width);
1378            // If the computed budget is zero, we certainly won't be able to achieve it,
1379            // so no point trying to elide the path.
1380            if budget > 0 && full_path.len() > budget {
1381                let components = PathComponentSlice::new(&full_path);
1382                if let Some(elided_range) =
1383                    components.elision_range(budget - 1, &full_path_positions)
1384                {
1385                    let elided_len = elided_range.end - elided_range.start;
1386                    let placeholder = "…";
1387                    full_path_positions.retain_mut(|mat| {
1388                        if *mat >= elided_range.end {
1389                            *mat -= elided_len;
1390                            *mat += placeholder.len();
1391                        } else if *mat >= elided_range.start {
1392                            return false;
1393                        }
1394                        true
1395                    });
1396                    full_path.replace_range(elided_range, placeholder);
1397                }
1398            }
1399        }
1400
1401        (
1402            HighlightedLabel::new(file_name, file_name_positions),
1403            HighlightedLabel::new(full_path, full_path_positions)
1404                .size(LabelSize::Small)
1405                .color(Color::Muted),
1406        )
1407    }
1408
1409    fn labels_for_path_match(
1410        &self,
1411        path_match: &PathMatch,
1412        path_style: PathStyle,
1413    ) -> (String, Vec<usize>, String, Vec<usize>) {
1414        let full_path = path_match.path_prefix.join(&path_match.path);
1415        let mut path_positions = path_match.positions.clone();
1416
1417        let file_name = full_path.file_name().unwrap_or("");
1418        let file_name_start = full_path.as_unix_str().len() - file_name.len();
1419        let file_name_positions = path_positions
1420            .iter()
1421            .filter_map(|pos| {
1422                if pos >= &file_name_start {
1423                    Some(pos - file_name_start)
1424                } else {
1425                    None
1426                }
1427            })
1428            .collect::<Vec<_>>();
1429
1430        let full_path = full_path
1431            .display(path_style)
1432            .trim_end_matches(&file_name)
1433            .to_string();
1434        path_positions.retain(|idx| *idx < full_path.len());
1435
1436        debug_assert!(
1437            file_name_positions
1438                .iter()
1439                .all(|ix| file_name[*ix..].chars().next().is_some()),
1440            "invalid file name positions {file_name:?} {file_name_positions:?}"
1441        );
1442        debug_assert!(
1443            path_positions
1444                .iter()
1445                .all(|ix| full_path[*ix..].chars().next().is_some()),
1446            "invalid path positions {full_path:?} {path_positions:?}"
1447        );
1448
1449        (
1450            file_name.to_string(),
1451            file_name_positions,
1452            full_path,
1453            path_positions,
1454        )
1455    }
1456
1457    /// Attempts to resolve an absolute file path and update the search matches if found.
1458    ///
1459    /// If the query path resolves to an absolute file that exists in the project,
1460    /// this method will find the corresponding worktree and relative path, create a
1461    /// match for it, and update the picker's search results.
1462    ///
1463    /// Returns `true` if the absolute path exists, otherwise returns `false`.
1464    fn lookup_absolute_path(
1465        &self,
1466        query: FileSearchQuery,
1467        window: &mut Window,
1468        cx: &mut Context<Picker<Self>>,
1469    ) -> Task<bool> {
1470        cx.spawn_in(window, async move |picker, cx| {
1471            let Some(project) = picker
1472                .read_with(cx, |picker, _| picker.delegate.project.clone())
1473                .log_err()
1474            else {
1475                return false;
1476            };
1477
1478            let query_path = Path::new(query.path_query());
1479            let mut path_matches = Vec::new();
1480
1481            let abs_file_exists = project
1482                .update(cx, |this, cx| {
1483                    this.resolve_abs_file_path(query.path_query(), cx)
1484                })
1485                .await
1486                .is_some();
1487
1488            if abs_file_exists {
1489                project.update(cx, |project, cx| {
1490                    if let Some((worktree, relative_path)) = project.find_worktree(query_path, cx) {
1491                        path_matches.push(ProjectPanelOrdMatch(PathMatch {
1492                            score: 1.0,
1493                            positions: Vec::new(),
1494                            worktree_id: worktree.read(cx).id().to_usize(),
1495                            path: relative_path,
1496                            path_prefix: RelPath::empty_arc(),
1497                            is_dir: false, // File finder doesn't support directories
1498                            distance_to_relative_ancestor: usize::MAX,
1499                        }));
1500                    }
1501                });
1502            }
1503
1504            picker
1505                .update_in(cx, |picker, _, cx| {
1506                    let picker_delegate = &mut picker.delegate;
1507                    let search_id = util::post_inc(&mut picker_delegate.search_count);
1508                    picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
1509
1510                    anyhow::Ok(())
1511                })
1512                .log_err();
1513            abs_file_exists
1514        })
1515    }
1516
1517    /// Skips first history match (that is displayed topmost) if it's currently opened.
1518    fn calculate_selected_index(&self, cx: &mut Context<Picker<Self>>) -> usize {
1519        if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search
1520            && let Some(Match::History { path, .. }) = self.matches.get(0)
1521            && Some(path) == self.currently_opened_path.as_ref()
1522        {
1523            let elements_after_first = self.matches.len() - 1;
1524            if elements_after_first > 0 {
1525                return 1;
1526            }
1527        }
1528
1529        0
1530    }
1531
1532    fn key_context(&self, _window: &Window, _cx: &App) -> KeyContext {
1533        let mut key_context = KeyContext::new_with_defaults();
1534        key_context.add("FileFinder");
1535        key_context
1536    }
1537
1538    /// Shared file-opening logic for both `confirm` and `confirm_without_dismiss`.
1539    ///
1540    /// When `dismiss_after_open` is true this behaves like a normal confirm: the file is focused
1541    /// and the finder is dismissed.  When false the finder stays open so the user can continue
1542    /// opening more files.
1543    fn open_selected_file(
1544        &mut self,
1545        secondary: bool,
1546        dismiss_after_open: bool,
1547        window: &mut Window,
1548        cx: &mut Context<Picker<FileFinderDelegate>>,
1549    ) {
1550        let Some(m) = self.matches.get(self.selected_index()).cloned() else {
1551            return;
1552        };
1553        let allow_preview = PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
1554        self.open_match(m, secondary, dismiss_after_open, allow_preview, window, cx);
1555    }
1556
1557    /// `allow_preview` is forced off for batch opens so every file gets its
1558    /// own tab instead of consecutive opens reusing one preview tab.
1559    fn open_match(
1560        &mut self,
1561        m: Match,
1562        secondary: bool,
1563        dismiss_after_open: bool,
1564        allow_preview: bool,
1565        window: &mut Window,
1566        cx: &mut Context<Picker<FileFinderDelegate>>,
1567    ) {
1568        let Some(workspace) = self.workspace.upgrade() else {
1569            return;
1570        };
1571
1572        // Channel matches always dismiss the finder.
1573        if let Match::Channel { channel_id, .. } = &m {
1574            let channel_id = channel_id.0;
1575            let finder = self.file_finder.clone();
1576            window.dispatch_action(OpenChannelNotesById { channel_id }.boxed_clone(), cx);
1577            finder.update(cx, |_, cx| cx.emit(DismissEvent)).log_err();
1578            return;
1579        }
1580
1581        // Focus the new item only when dismissing — this avoids stealing focus from the modal.
1582        // Always activate (make the tab current) so every opened file is visually reflected.
1583        let focus_item = dismiss_after_open;
1584
1585        let open_task = workspace.update(cx, |workspace, cx| {
1586            let split_or_open = |workspace: &mut Workspace,
1587                                 project_path,
1588                                 window: &mut Window,
1589                                 cx: &mut Context<Workspace>| {
1590                if secondary {
1591                    workspace.split_path_preview(project_path, allow_preview, None, window, cx)
1592                } else {
1593                    workspace.open_path_preview(
1594                        project_path,
1595                        None,
1596                        focus_item,
1597                        allow_preview,
1598                        true,
1599                        window,
1600                        cx,
1601                    )
1602                }
1603            };
1604
1605            match &m {
1606                Match::CreateNew(project_path) => {
1607                    if secondary {
1608                        workspace.split_path_preview(project_path.clone(), false, None, window, cx)
1609                    } else {
1610                        workspace.open_path_preview(
1611                            project_path.clone(),
1612                            None,
1613                            focus_item,
1614                            false,
1615                            true,
1616                            window,
1617                            cx,
1618                        )
1619                    }
1620                }
1621                Match::History { path, .. } => {
1622                    let worktree_id = path.project.worktree_id;
1623                    if workspace
1624                        .project()
1625                        .read(cx)
1626                        .worktree_for_id(worktree_id, cx)
1627                        .is_some()
1628                    {
1629                        split_or_open(
1630                            workspace,
1631                            ProjectPath {
1632                                worktree_id,
1633                                path: Arc::clone(&path.project.path),
1634                            },
1635                            window,
1636                            cx,
1637                        )
1638                    } else if secondary {
1639                        workspace.split_abs_path(path.absolute.clone(), false, window, cx)
1640                    } else {
1641                        workspace.open_abs_path(
1642                            path.absolute.clone(),
1643                            OpenOptions {
1644                                visible: Some(OpenVisible::None),
1645                                ..Default::default()
1646                            },
1647                            window,
1648                            cx,
1649                        )
1650                    }
1651                }
1652                Match::Search(path_match) => {
1653                    let project_path =
1654                        project_path_for_search_match(workspace.project(), &path_match.0, cx);
1655                    split_or_open(workspace, project_path, window, cx)
1656                }
1657                Match::Channel { .. } => unreachable!("handled above"),
1658            }
1659        });
1660
1661        let selection_query = self.latest_search_query.clone();
1662        let finder = self.file_finder.clone();
1663        let workspace = self.workspace.clone();
1664
1665        cx.spawn_in(window, async move |_, mut cx| {
1666            let item = open_task
1667                .await
1668                .notify_workspace_async_err(workspace, &mut cx)?;
1669            if let Some(active_editor) = item.downcast::<Editor>() {
1670                active_editor
1671                    .downgrade()
1672                    .update_in(cx, |editor, window, cx| {
1673                        let Some(buffer) = editor.buffer().read(cx).as_singleton() else {
1674                            return;
1675                        };
1676                        let buffer_snapshot = buffer.read(cx).snapshot();
1677                        let Some(selection_query) = selection_query.as_ref() else {
1678                            return;
1679                        };
1680                        let Some(selection_range) =
1681                            selection_query.selection_range(&buffer_snapshot)
1682                        else {
1683                            return;
1684                        };
1685                        editor.go_to_singleton_buffer_range(selection_range, window, cx);
1686                    })
1687                    .log_err();
1688            }
1689            if dismiss_after_open {
1690                finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?;
1691            }
1692            Some(())
1693        })
1694        .detach();
1695    }
1696
1697    fn confirm_without_dismiss(
1698        &mut self,
1699        window: &mut Window,
1700        cx: &mut Context<Picker<FileFinderDelegate>>,
1701    ) {
1702        self.open_selected_file(false, false, window, cx);
1703    }
1704
1705    /// Opens every multi-selected file as a tab in a single new split, then
1706    /// dismisses the finder.
1707    fn open_selected_in_one_split(
1708        &mut self,
1709        split_direction: SplitDirection,
1710        window: &mut Window,
1711        cx: &mut Context<Picker<FileFinderDelegate>>,
1712    ) {
1713        let Some(workspace) = self.workspace.upgrade() else {
1714            return;
1715        };
1716        let selected = std::mem::take(&mut self.selected_matches);
1717        let paths: Vec<ProjectPath> = selected
1718            .iter()
1719            .filter_map(|selected| match &selected.0 {
1720                Match::History { path, .. } => Some(ProjectPath {
1721                    worktree_id: path.project.worktree_id,
1722                    path: Arc::clone(&path.project.path),
1723                }),
1724                Match::Search(m) => Some(project_path_for_search_match(&self.project, &m.0, cx)),
1725                Match::Channel { .. } | Match::CreateNew(_) => None,
1726            })
1727            .collect();
1728        if paths.is_empty() {
1729            return;
1730        }
1731        workspace.update(cx, |workspace, cx| {
1732            let new_pane =
1733                workspace.split_pane(workspace.active_pane().clone(), split_direction, window, cx);
1734            let count = paths.len();
1735            for (i, path) in paths.into_iter().enumerate() {
1736                let focus_item = i + 1 == count;
1737                workspace
1738                    .open_path_preview(
1739                        path,
1740                        Some(new_pane.downgrade()),
1741                        focus_item,
1742                        false,
1743                        true,
1744                        window,
1745                        cx,
1746                    )
1747                    .detach_and_log_err(cx);
1748            }
1749        });
1750        // Deferred because this runs from a `FileFinder` action handler, so
1751        // the entity is already being updated.
1752        let finder = self.file_finder.clone();
1753        cx.defer(move |cx| {
1754            finder.update(cx, |_, cx| cx.emit(DismissEvent)).log_err();
1755        });
1756    }
1757}
1758
1759fn full_path_budget(
1760    file_name: &str,
1761    normal_em: Pixels,
1762    small_em: Pixels,
1763    max_width: Pixels,
1764) -> usize {
1765    (((max_width / 0.8) - file_name.len() * normal_em) / small_em) as usize
1766}
1767
1768impl PickerDelegate for FileFinderDelegate {
1769    type ListItem = ListItem;
1770
1771    fn name() -> &'static str {
1772        "file finder"
1773    }
1774
1775    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1776        "Search project files...".into()
1777    }
1778
1779    fn searchbar_trailer(
1780        &self,
1781        _window: &mut Window,
1782        cx: &mut Context<Picker<Self>>,
1783    ) -> Option<AnyElement> {
1784        let focus_handle = self.focus_handle.clone();
1785        let including_ignored = self.include_ignored == Some(true);
1786        // Clicking includes ignored files unless they're already included, in
1787        // which case it excludes them again (see `handle_toggle_ignored`).
1788        let tooltip_label = if including_ignored {
1789            "Exclude Ignored Files"
1790        } else {
1791            "Include Ignored Files"
1792        };
1793
1794        let filter_button = IconButton::new("filter-ignored", IconName::FileIgnored)
1795            .icon_size(IconSize::Small)
1796            .toggle_state(including_ignored)
1797            .tooltip(move |_window, cx| {
1798                Tooltip::for_action_in(tooltip_label, &ToggleIncludeIgnored, &focus_handle, cx)
1799            })
1800            .on_click(|_, window, cx| {
1801                window.dispatch_action(ToggleIncludeIgnored.boxed_clone(), cx)
1802            });
1803        Some(
1804            h_flex()
1805                .gap_1()
1806                .child(filter_button)
1807                .children(picker::parts::project_scan_indicator(
1808                    self.latest_search_query.is_some(),
1809                    &self.project,
1810                    cx,
1811                ))
1812                .into_any_element(),
1813        )
1814    }
1815
1816    fn match_count(&self) -> usize {
1817        self.matches.len()
1818    }
1819
1820    fn selected_index(&self) -> usize {
1821        self.selected_index
1822    }
1823
1824    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
1825        self.has_changed_selected_index = true;
1826        self.selected_index = ix;
1827        cx.notify();
1828    }
1829
1830    fn separators_after_indices(&self) -> Vec<usize> {
1831        if self.separate_history {
1832            let first_non_history_index = self
1833                .matches
1834                .matches
1835                .iter()
1836                .enumerate()
1837                .find(|(_, m)| !matches!(m, Match::History { .. }))
1838                .map(|(i, _)| i);
1839            if let Some(first_non_history_index) = first_non_history_index
1840                && first_non_history_index > 0
1841            {
1842                return vec![first_non_history_index - 1];
1843            }
1844        }
1845        Vec::new()
1846    }
1847
1848    fn update_matches(
1849        &mut self,
1850        raw_query: String,
1851        window: &mut Window,
1852        cx: &mut Context<Picker<Self>>,
1853    ) -> Task<()> {
1854        let debounce_refresh = mem::take(&mut self.debounce_next_refresh);
1855        let raw_query = raw_query.trim();
1856
1857        let raw_query = match &raw_query.get(0..2) {
1858            Some(".\\" | "./") => &raw_query[2..],
1859            Some(prefix @ ("a\\" | "a/" | "b\\" | "b/")) => {
1860                if self
1861                    .workspace
1862                    .upgrade()
1863                    .into_iter()
1864                    .flat_map(|workspace| workspace.read(cx).worktrees(cx))
1865                    .all(|worktree| {
1866                        worktree
1867                            .read(cx)
1868                            .entry_for_path(RelPath::from_unix_str(prefix.split_at(1).0).unwrap())
1869                            .is_none_or(|entry| !entry.is_dir())
1870                    })
1871                {
1872                    &raw_query[2..]
1873                } else {
1874                    raw_query
1875                }
1876            }
1877            _ => raw_query,
1878        };
1879
1880        if raw_query.is_empty() {
1881            // if there was no query before, and we already have some (history) matches
1882            // there's no need to update anything, since nothing has changed.
1883            // We also want to populate matches set from history entries on the first update.
1884            if self.latest_search_query.is_some() || self.first_update {
1885                let project = self.project.read(cx);
1886
1887                self.latest_search_id = post_inc(&mut self.search_count);
1888                self.latest_search_query = None;
1889                self.matches = Matches {
1890                    separate_history: self.separate_history,
1891                    ..Matches::default()
1892                };
1893                let path_style = self.project.read(cx).path_style(cx);
1894
1895                self.matches.push_new_matches(
1896                    project.worktree_store(),
1897                    cx,
1898                    self.history_items.iter().filter(|history_item| {
1899                        project
1900                            .worktree_for_id(history_item.project.worktree_id, cx)
1901                            .is_some()
1902                            || project.is_local()
1903                            || project.is_via_remote_server()
1904                    }),
1905                    self.currently_opened_path.as_ref(),
1906                    None,
1907                    None.into_iter(),
1908                    false,
1909                    path_style,
1910                );
1911
1912                self.first_update = false;
1913                self.selected_index = 0;
1914            }
1915            self.prepend_selected_matches();
1916            cx.notify();
1917            self.search_in_flight
1918                .store(false, atomic::Ordering::Release);
1919            Task::ready(())
1920        } else {
1921            let query = parse_file_search_query(raw_query);
1922            let path = query.path_position.path.clone();
1923
1924            let search_in_flight = self.search_in_flight.clone();
1925            let was_in_flight = search_in_flight.swap(true, atomic::Ordering::Relaxed);
1926
1927            cx.spawn_in(window, async move |this, cx| {
1928                if debounce_refresh {
1929                    cx.background_executor()
1930                        .timer(WORKTREE_UPDATE_REFRESH_DEBOUNCE)
1931                        .await;
1932                } else if was_in_flight {
1933                    cx.background_executor().timer(SEARCH_DEBOUNCE).await;
1934                }
1935                let _ = maybe!(async move {
1936                    let is_absolute_path = path.is_absolute();
1937                    let did_resolve_abs_path = is_absolute_path
1938                        && this
1939                            .update_in(cx, |this, window, cx| {
1940                                this.delegate
1941                                    .lookup_absolute_path(query.clone(), window, cx)
1942                            })?
1943                            .await;
1944
1945                    // Only check for relative paths if no absolute paths were
1946                    // found.
1947                    if !did_resolve_abs_path {
1948                        this.update_in(cx, |this, window, cx| {
1949                            this.delegate.spawn_search(query, window, cx)
1950                        })?
1951                        .await;
1952                    }
1953                    anyhow::Ok(())
1954                })
1955                .await;
1956                search_in_flight.store(false, atomic::Ordering::Relaxed);
1957            })
1958        }
1959    }
1960
1961    fn confirm(
1962        &mut self,
1963        secondary: bool,
1964        window: &mut Window,
1965        cx: &mut Context<Picker<FileFinderDelegate>>,
1966    ) {
1967        self.open_selected_file(secondary, true, window, cx);
1968    }
1969
1970    fn supports_multi_select(&self) -> bool {
1971        true
1972    }
1973
1974    fn is_item_selected(&self, ix: usize) -> bool {
1975        let Some(m) = self.matches.get(ix) else {
1976            return false;
1977        };
1978        self.selected_matches.iter().any(|selected| selected == m)
1979    }
1980
1981    fn toggle_item_selected(
1982        &mut self,
1983        ix: usize,
1984        _window: &mut Window,
1985        cx: &mut Context<Picker<FileFinderDelegate>>,
1986    ) {
1987        let Some(m) = self.matches.get(ix).cloned() else {
1988            return;
1989        };
1990        // `new` rejects channels and the `create-new` placeholder.
1991        let Some(selected) = SelectedMatch::new(m) else {
1992            return;
1993        };
1994        if let Some(position) = self
1995            .selected_matches
1996            .iter()
1997            .position(|existing| *existing == selected)
1998        {
1999            self.selected_matches.remove(position);
2000        } else {
2001            self.selected_matches.push(selected);
2002        }
2003        cx.notify();
2004    }
2005
2006    fn selected_item_count(&self) -> usize {
2007        self.selected_matches.len()
2008    }
2009
2010    fn clear_selection(&mut self, _cx: &mut Context<Picker<FileFinderDelegate>>) {
2011        self.selected_matches.clear();
2012    }
2013
2014    fn confirm_multi(
2015        &mut self,
2016        secondary: bool,
2017        window: &mut Window,
2018        cx: &mut Context<Picker<FileFinderDelegate>>,
2019    ) {
2020        let selected = std::mem::take(&mut self.selected_matches);
2021        let count = selected.len();
2022        for (i, selected_match) in selected.into_iter().enumerate() {
2023            let is_last = i + 1 == count;
2024            self.open_match(selected_match.0, secondary, is_last, false, window, cx);
2025        }
2026    }
2027
2028    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<FileFinderDelegate>>) {
2029        self.file_finder
2030            .update(cx, |_, cx| cx.emit(DismissEvent))
2031            .log_err();
2032    }
2033
2034    fn try_get_preview_data_for_match(&self, cx: &App) -> Option<picker::PreviewUpdate> {
2035        let m = self.matches.get(self.selected_index)?;
2036        match m {
2037            Match::CreateNew(project_path) => {
2038                let path_style = self.project.read(cx).path_style(cx);
2039                let path_highlight = gpui::HighlightStyle {
2040                    color: Some(cx.theme().colors().text_accent),
2041                    ..Default::default()
2042                };
2043                let mut message = picker::HighlightedTextBuilder::default();
2044                message.push_plain("Create file ");
2045                message.push_styled(project_path.path.display(path_style), path_highlight);
2046                message.push_plain("?");
2047                Some(picker::PreviewUpdate::message(message.build()))
2048            }
2049            _ => Some(picker::PreviewUpdate::from_path(
2050                m.abs_path(&self.project, cx)?,
2051            )),
2052        }
2053    }
2054
2055    fn render_match(
2056        &self,
2057        ix: usize,
2058        selected: bool,
2059        window: &mut Window,
2060        cx: &mut Context<Picker<Self>>,
2061    ) -> Option<Self::ListItem> {
2062        self.render_match_impl(ix, selected, None, window, cx)
2063    }
2064
2065    fn render_match_with_checkbox(
2066        &self,
2067        ix: usize,
2068        selected: bool,
2069        checkbox: AnyElement,
2070        window: &mut Window,
2071        cx: &mut Context<Picker<Self>>,
2072    ) -> Option<Self::ListItem> {
2073        self.render_match_impl(ix, selected, Some(checkbox), window, cx)
2074    }
2075
2076    fn actions_menu(
2077        &self,
2078        _window: &mut Window,
2079        _cx: &mut Context<Picker<Self>>,
2080    ) -> Vec<picker::PickerAction> {
2081        let open_label: SharedString = if self.selected_matches.len() > 1 {
2082            "Open multiple".into()
2083        } else {
2084            "Open File".into()
2085        };
2086        vec![
2087            picker::PickerAction::header("Split…"),
2088            picker::PickerAction::button("Left", pane::SplitLeft::default().boxed_clone()),
2089            picker::PickerAction::button("Right", pane::SplitRight::default().boxed_clone()),
2090            picker::PickerAction::button("Up", pane::SplitUp::default().boxed_clone()),
2091            picker::PickerAction::button("Down", pane::SplitDown::default().boxed_clone()),
2092            picker::PickerAction::separator(),
2093            picker::PickerAction::button(open_label, menu::Confirm.boxed_clone()),
2094        ]
2095    }
2096}
2097
2098impl FileFinderDelegate {
2099    fn render_match_impl(
2100        &self,
2101        ix: usize,
2102        selected: bool,
2103        checkbox: Option<AnyElement>,
2104        window: &mut Window,
2105        cx: &mut Context<Picker<Self>>,
2106    ) -> Option<ListItem> {
2107        let settings = FileFinderSettings::get_global(cx);
2108
2109        let path_match = self.matches.get(ix)?;
2110
2111        let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx);
2112
2113        let start_icon = match path_match {
2114            Match::CreateNew(_) => Some(Icon::new(IconName::Plus).size(IconSize::Small)),
2115            Match::Channel { .. } => Some(Icon::new(IconName::Hash).color(Color::Muted)),
2116            _ => maybe!({
2117                if !settings.file_icons {
2118                    return None;
2119                }
2120                let abs_path = path_match.abs_path(&self.project, cx)?;
2121                let file_name = abs_path.file_name()?;
2122                let icon = FileIcons::get_icon(file_name.as_ref(), cx)?;
2123                Some(Icon::from_path(icon).color(Color::Muted))
2124            }),
2125        };
2126
2127        let checkbox = checkbox.map(|checkbox| {
2128            if matches!(path_match, Match::CreateNew(_) | Match::Channel { .. }) {
2129                div()
2130                    .flex_none()
2131                    .size(Checkbox::container_size())
2132                    .into_any_element()
2133            } else {
2134                checkbox
2135            }
2136        });
2137
2138        let start_slot: Option<AnyElement> = match (checkbox, start_icon) {
2139            (Some(checkbox), icon) => Some(
2140                h_flex()
2141                    .gap_1p5()
2142                    .child(checkbox)
2143                    .children(icon)
2144                    .into_any_element(),
2145            ),
2146            (None, icon) => icon.map(IntoElement::into_any_element),
2147        };
2148
2149        let end_slot: Option<AnyElement> = match path_match {
2150            Match::History { .. } => Some(
2151                Icon::new(IconName::HistoryRerun)
2152                    .color(Color::Muted)
2153                    .size(IconSize::Small)
2154                    .into_any_element(),
2155            ),
2156            Match::Search(_) | Match::Channel { .. } => Some(
2157                div()
2158                    .flex_none()
2159                    .size(IconSize::Small.rems())
2160                    .into_any_element(),
2161            ),
2162            Match::CreateNew(_) => None,
2163        };
2164
2165        Some(
2166            ListItem::new(ix)
2167                .spacing(ListItemSpacing::Sparse)
2168                .inset(true)
2169                .toggle_state(selected)
2170                .start_slot::<AnyElement>(start_slot)
2171                .child(
2172                    h_flex()
2173                        .w_full()
2174                        .min_w_0()
2175                        .gap_1p5()
2176                        .child(file_name_label.truncate_middle())
2177                        .child(full_path_label.truncate_start()),
2178                )
2179                .end_slot::<AnyElement>(end_slot),
2180        )
2181    }
2182}
2183
2184#[derive(Clone, Debug, PartialEq, Eq)]
2185struct PathComponentSlice<'a> {
2186    path: Cow<'a, Path>,
2187    path_str: Cow<'a, str>,
2188    component_ranges: Vec<(Component<'a>, Range<usize>)>,
2189}
2190
2191impl<'a> PathComponentSlice<'a> {
2192    fn new(path: &'a str) -> Self {
2193        let trimmed_path = Path::new(path).components().as_path().as_os_str();
2194        let mut component_ranges = Vec::new();
2195        let mut components = Path::new(trimmed_path).components();
2196        let len = trimmed_path.as_encoded_bytes().len();
2197        let mut pos = 0;
2198        while let Some(component) = components.next() {
2199            component_ranges.push((component, pos..0));
2200            pos = len - components.as_path().as_os_str().as_encoded_bytes().len();
2201        }
2202        for ((_, range), ancestor) in component_ranges
2203            .iter_mut()
2204            .rev()
2205            .zip(Path::new(trimmed_path).ancestors())
2206        {
2207            range.end = ancestor.as_os_str().as_encoded_bytes().len();
2208        }
2209        Self {
2210            path: Cow::Borrowed(Path::new(path)),
2211            path_str: Cow::Borrowed(path),
2212            component_ranges,
2213        }
2214    }
2215
2216    fn elision_range(&self, budget: usize, matches: &[usize]) -> Option<Range<usize>> {
2217        let eligible_range = {
2218            assert!(matches.is_sorted());
2219            let mut matches = matches.iter().copied().peekable();
2220            let mut longest: Option<Range<usize>> = None;
2221            let mut cur = 0..0;
2222            let mut seen_normal = false;
2223            for (i, (component, range)) in self.component_ranges.iter().enumerate() {
2224                let is_normal = matches!(component, Component::Normal(_));
2225                let is_first_normal = is_normal && !seen_normal;
2226                seen_normal |= is_normal;
2227                let is_last = i == self.component_ranges.len() - 1;
2228                let contains_match = matches.peek().is_some_and(|mat| range.contains(mat));
2229                if contains_match {
2230                    matches.next();
2231                }
2232                if is_first_normal || is_last || !is_normal || contains_match {
2233                    if longest
2234                        .as_ref()
2235                        .is_none_or(|old| old.end - old.start <= cur.end - cur.start)
2236                    {
2237                        longest = Some(cur);
2238                    }
2239                    cur = i + 1..i + 1;
2240                } else {
2241                    cur.end = i + 1;
2242                }
2243            }
2244            if longest
2245                .as_ref()
2246                .is_none_or(|old| old.end - old.start <= cur.end - cur.start)
2247            {
2248                longest = Some(cur);
2249            }
2250            longest
2251        };
2252
2253        let eligible_range = eligible_range?;
2254        assert!(eligible_range.start <= eligible_range.end);
2255        if eligible_range.is_empty() {
2256            return None;
2257        }
2258
2259        let elided_range: Range<usize> = {
2260            let byte_range = self.component_ranges[eligible_range.start].1.start
2261                ..self.component_ranges[eligible_range.end - 1].1.end;
2262            let midpoint = self.path_str.len() / 2;
2263            let distance_from_start = byte_range.start.abs_diff(midpoint);
2264            let distance_from_end = byte_range.end.abs_diff(midpoint);
2265            let pick_from_end = distance_from_start > distance_from_end;
2266            let mut len_with_elision = self.path_str.len();
2267            let mut i = eligible_range.start;
2268            while i < eligible_range.end {
2269                let x = if pick_from_end {
2270                    eligible_range.end - i + eligible_range.start - 1
2271                } else {
2272                    i
2273                };
2274                len_with_elision -= self.component_ranges[x]
2275                    .0
2276                    .as_os_str()
2277                    .as_encoded_bytes()
2278                    .len()
2279                    + 1;
2280                if len_with_elision <= budget {
2281                    break;
2282                }
2283                i += 1;
2284            }
2285            if len_with_elision > budget {
2286                return None;
2287            } else if pick_from_end {
2288                let x = eligible_range.end - i + eligible_range.start - 1;
2289                x..eligible_range.end
2290            } else {
2291                let x = i;
2292                eligible_range.start..x + 1
2293            }
2294        };
2295
2296        let byte_range = self.component_ranges[elided_range.start].1.start
2297            ..self.component_ranges[elided_range.end - 1].1.end;
2298        Some(byte_range)
2299    }
2300}
2301
Served at tenant.openagents/omega Member data and write actions are omitted.