Piotr OsiewiczRust 1.85 (#25272)e4e758d1 year agoHistory
1521 lines · 55.5 KB · Rust
RawDownload
#[cfg(test)]mod file_finder_tests; pub mod file_finder_settings;mod new_path_prompt;mod open_path_prompt; use futures::future::join_all;pub use open_path_prompt::OpenPathDelegate; use collections::HashMap;use editor::Editor;use file_finder_settings::{FileFinderSettings, FileFinderWidth};use file_icons::FileIcons;use fuzzy::{CharBag, PathMatch, PathMatchCandidate};use gpui::{    actions, Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle,    Focusable, KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task,    WeakEntity, Window,};use new_path_prompt::NewPathPrompt;use open_path_prompt::OpenPathPrompt;use picker::{Picker, PickerDelegate};use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};use settings::Settings;use std::{    borrow::Cow,    cmp,    ops::Range,    path::{Component, Path, PathBuf},    sync::{        atomic::{self, AtomicBool},        Arc,    },};use text::Point;use ui::{    prelude::*, ContextMenu, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenu,    PopoverMenuHandle,};use util::{maybe, paths::PathWithPosition, post_inc, ResultExt};use workspace::{    item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, SplitDirection,    Workspace,}; actions!(file_finder, [SelectPrev, ToggleMenu]); impl ModalView for FileFinder {    fn on_before_dismiss(        &mut self,        window: &mut Window,        cx: &mut Context<Self>,    ) -> workspace::DismissDecision {        let submenu_focused = self.picker.update(cx, |picker, cx| {            picker.delegate.popover_menu_handle.is_focused(window, cx)        });        workspace::DismissDecision::Dismiss(!submenu_focused)    }} pub struct FileFinder {    picker: Entity<Picker<FileFinderDelegate>>,    picker_focus_handle: FocusHandle,    init_modifiers: Option<Modifiers>,} pub fn init_settings(cx: &mut App) {    FileFinderSettings::register(cx);} pub fn init(cx: &mut App) {    init_settings(cx);    cx.observe_new(FileFinder::register).detach();    cx.observe_new(NewPathPrompt::register).detach();    cx.observe_new(OpenPathPrompt::register).detach();} impl FileFinder {    fn register(        workspace: &mut Workspace,        _window: Option<&mut Window>,        _: &mut Context<Workspace>,    ) {        workspace.register_action(            |workspace, action: &workspace::ToggleFileFinder, window, cx| {                let Some(file_finder) = workspace.active_modal::<Self>(cx) else {                    Self::open(workspace, action.separate_history, window, cx).detach();                    return;                };                 file_finder.update(cx, |file_finder, cx| {                    file_finder.init_modifiers = Some(window.modifiers());                    file_finder.picker.update(cx, |picker, cx| {                        picker.cycle_selection(window, cx);                    });                });            },        );    }     fn open(        workspace: &mut Workspace,        separate_history: bool,        window: &mut Window,        cx: &mut Context<Workspace>,    ) -> Task<()> {        let project = workspace.project().read(cx);        let fs = project.fs();         let currently_opened_path = workspace            .active_item(cx)            .and_then(|item| item.project_path(cx))            .map(|project_path| {                let abs_path = project                    .worktree_for_id(project_path.worktree_id, cx)                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));                FoundPath::new(project_path, abs_path)            });         let history_items = workspace            .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)            .into_iter()            .filter_map(|(project_path, abs_path)| {                if project.entry_for_path(&project_path, cx).is_some() {                    return Some(Task::ready(Some(FoundPath::new(project_path, abs_path))));                }                let abs_path = abs_path?;                if project.is_local() {                    let fs = fs.clone();                    Some(cx.background_spawn(async move {                        if fs.is_file(&abs_path).await {                            Some(FoundPath::new(project_path, Some(abs_path)))                        } else {                            None                        }                    }))                } else {                    Some(Task::ready(Some(FoundPath::new(                        project_path,                        Some(abs_path),                    ))))                }            })            .collect::<Vec<_>>();        cx.spawn_in(window, move |workspace, mut cx| async move {            let history_items = join_all(history_items).await.into_iter().flatten();             workspace                .update_in(&mut cx, |workspace, window, cx| {                    let project = workspace.project().clone();                    let weak_workspace = cx.entity().downgrade();                    workspace.toggle_modal(window, cx, |window, cx| {                        let delegate = FileFinderDelegate::new(                            cx.entity().downgrade(),                            weak_workspace,                            project,                            currently_opened_path,                            history_items.collect(),                            separate_history,                            window,                            cx,                        );                         FileFinder::new(delegate, window, cx)                    });                })                .ok();        })    }     fn new(delegate: FileFinderDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));        let picker_focus_handle = picker.focus_handle(cx);        picker.update(cx, |picker, _| {            picker.delegate.focus_handle = picker_focus_handle.clone();        });        Self {            picker,            picker_focus_handle,            init_modifiers: window.modifiers().modified().then_some(window.modifiers()),        }    }     fn handle_modifiers_changed(        &mut self,        event: &ModifiersChangedEvent,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let Some(init_modifiers) = self.init_modifiers.take() else {            return;        };        if self.picker.read(cx).delegate.has_changed_selected_index {            if !event.modified() || !init_modifiers.is_subset_of(&event) {                self.init_modifiers = None;                window.dispatch_action(menu::Confirm.boxed_clone(), cx);            }        }    }     fn handle_select_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context<Self>) {        self.init_modifiers = Some(window.modifiers());        window.dispatch_action(Box::new(menu::SelectPrev), cx);    }     fn handle_toggle_menu(&mut self, _: &ToggleMenu, window: &mut Window, cx: &mut Context<Self>) {        self.picker.update(cx, |picker, cx| {            let menu_handle = &picker.delegate.popover_menu_handle;            if menu_handle.is_deployed() {                menu_handle.hide(cx);            } else {                menu_handle.show(window, cx);            }        });    }     fn go_to_file_split_left(        &mut self,        _: &pane::SplitLeft,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.go_to_file_split_inner(SplitDirection::Left, window, cx)    }     fn go_to_file_split_right(        &mut self,        _: &pane::SplitRight,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.go_to_file_split_inner(SplitDirection::Right, window, cx)    }     fn go_to_file_split_up(        &mut self,        _: &pane::SplitUp,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.go_to_file_split_inner(SplitDirection::Up, window, cx)    }     fn go_to_file_split_down(        &mut self,        _: &pane::SplitDown,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.go_to_file_split_inner(SplitDirection::Down, window, cx)    }     fn go_to_file_split_inner(        &mut self,        split_direction: SplitDirection,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.picker.update(cx, |picker, cx| {            let delegate = &mut picker.delegate;            if let Some(workspace) = delegate.workspace.upgrade() {                if let Some(m) = delegate.matches.get(delegate.selected_index()) {                    let path = match &m {                        Match::History { path, .. } => {                            let worktree_id = path.project.worktree_id;                            ProjectPath {                                worktree_id,                                path: Arc::clone(&path.project.path),                            }                        }                        Match::Search(m) => ProjectPath {                            worktree_id: WorktreeId::from_usize(m.0.worktree_id),                            path: m.0.path.clone(),                        },                    };                    let open_task = workspace.update(cx, move |workspace, cx| {                        workspace.split_path_preview(path, false, Some(split_direction), window, cx)                    });                    open_task.detach_and_log_err(cx);                }            }        })    }     pub fn modal_max_width(width_setting: Option<FileFinderWidth>, window: &mut Window) -> Pixels {        let window_width = window.viewport_size().width;        let small_width = Pixels(545.);         match width_setting {            None | Some(FileFinderWidth::Small) => small_width,            Some(FileFinderWidth::Full) => window_width,            Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width),            Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width),            Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width),        }    }} impl EventEmitter<DismissEvent> for FileFinder {} impl Focusable for FileFinder {    fn focus_handle(&self, _: &App) -> FocusHandle {        self.picker_focus_handle.clone()    }} impl Render for FileFinder {    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let key_context = self.picker.read(cx).delegate.key_context(window, cx);         let file_finder_settings = FileFinderSettings::get_global(cx);        let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, window);         v_flex()            .key_context(key_context)            .w(modal_max_width)            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))            .on_action(cx.listener(Self::handle_select_prev))            .on_action(cx.listener(Self::handle_toggle_menu))            .on_action(cx.listener(Self::go_to_file_split_left))            .on_action(cx.listener(Self::go_to_file_split_right))            .on_action(cx.listener(Self::go_to_file_split_up))            .on_action(cx.listener(Self::go_to_file_split_down))            .child(self.picker.clone())    }} pub struct FileFinderDelegate {    file_finder: WeakEntity<FileFinder>,    workspace: WeakEntity<Workspace>,    project: Entity<Project>,    search_count: usize,    latest_search_id: usize,    latest_search_did_cancel: bool,    latest_search_query: Option<FileSearchQuery>,    currently_opened_path: Option<FoundPath>,    matches: Matches,    selected_index: usize,    has_changed_selected_index: bool,    cancel_flag: Arc<AtomicBool>,    history_items: Vec<FoundPath>,    separate_history: bool,    first_update: bool,    popover_menu_handle: PopoverMenuHandle<ContextMenu>,    focus_handle: FocusHandle,} /// Use a custom ordering for file finder: the regular one/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`////// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`/// as the files are shown in the project panel lists.#[derive(Debug, Clone, PartialEq, Eq)]struct ProjectPanelOrdMatch(PathMatch); impl Ord for ProjectPanelOrdMatch {    fn cmp(&self, other: &Self) -> cmp::Ordering {        self.0            .score            .partial_cmp(&other.0.score)            .unwrap_or(cmp::Ordering::Equal)            .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))            .then_with(|| {                other                    .0                    .distance_to_relative_ancestor                    .cmp(&self.0.distance_to_relative_ancestor)            })            .then_with(|| self.0.path.cmp(&other.0.path).reverse())    }} impl PartialOrd for ProjectPanelOrdMatch {    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {        Some(self.cmp(other))    }} #[derive(Debug, Default)]struct Matches {    separate_history: bool,    matches: Vec<Match>,} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]enum Match {    History {        path: FoundPath,        panel_match: Option<ProjectPanelOrdMatch>,    },    Search(ProjectPanelOrdMatch),} impl Match {    fn path(&self) -> &Arc<Path> {        match self {            Match::History { path, .. } => &path.project.path,            Match::Search(panel_match) => &panel_match.0.path,        }    }     fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> {        match self {            Match::History { panel_match, .. } => panel_match.as_ref(),            Match::Search(panel_match) => Some(&panel_match),        }    }} impl Matches {    fn len(&self) -> usize {        self.matches.len()    }     fn get(&self, index: usize) -> Option<&Match> {        self.matches.get(index)    }     fn position(        &self,        entry: &Match,        currently_opened: Option<&FoundPath>,    ) -> Result<usize, usize> {        if let Match::History {            path,            panel_match: None,        } = entry        {            // Slow case: linear search by path. Should not happen actually,            // since we call `position` only if matches set changed, but the query has not changed.            // And History entries do not have panel_match if query is empty, so there's no            // reason for the matches set to change.            self.matches                .iter()                .position(|m| path.project.path == *m.path())                .ok_or(0)        } else {            self.matches.binary_search_by(|m| {                // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b.                // And we want the better entries go first.                Self::cmp_matches(self.separate_history, currently_opened, &m, &entry).reverse()            })        }    }     fn push_new_matches<'a>(        &'a mut self,        history_items: impl IntoIterator<Item = &'a FoundPath> + Clone,        currently_opened: Option<&'a FoundPath>,        query: Option<&FileSearchQuery>,        new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,        extend_old_matches: bool,    ) {        let Some(query) = query else {            // assuming that if there's no query, then there's no search matches.            self.matches.clear();            let path_to_entry = |found_path: &FoundPath| Match::History {                path: found_path.clone(),                panel_match: None,            };            self.matches                .extend(currently_opened.into_iter().map(path_to_entry));             self.matches.extend(                history_items                    .into_iter()                    .filter(|found_path| Some(*found_path) != currently_opened)                    .map(path_to_entry),            );            return;        };         let new_history_matches = matching_history_items(history_items, currently_opened, query);        let new_search_matches: Vec<Match> = new_search_matches            .filter(|path_match| !new_history_matches.contains_key(&path_match.0.path))            .map(Match::Search)            .collect();         if extend_old_matches {            // since we take history matches instead of new search matches            // and history matches has not changed(since the query has not changed and we do not extend old matches otherwise),            // old matches can't contain paths present in history_matches as well.            self.matches.retain(|m| matches!(m, Match::Search(_)));        } else {            self.matches.clear();        }         // At this point we have an unsorted set of new history matches, an unsorted set of new search matches        // and a sorted set of old search matches.        // It is possible that the new search matches' paths contain some of the old search matches' paths.        // History matches' paths are unique, since store in a HashMap by path.        // We build a sorted Vec<Match>, eliminating duplicate search matches.        // Search matches with the same paths should have equal `ProjectPanelOrdMatch`, so we should        // not have any duplicates after building the final list.        for new_match in new_history_matches            .into_values()            .chain(new_search_matches.into_iter())

Lines 1–500 of 1521.