441 lines · 15.3 KB · Rust
RawDownload
use std::sync::Arc; use fuzzy_nucleo::{StringMatch, StringMatchCandidate, match_strings};use gpui::{    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,    Subscription, Task, TaskExt, WeakEntity, Window,};use picker::{    Picker, PickerDelegate,    highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},};use remote::RemoteConnectionOptions;use settings::Settings;use ui::{ButtonLike, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};use util::{ResultExt, paths::PathExt};use workspace::{    MultiWorkspace, OpenMode, OpenOptions, ProjectGroupKey, RecentWorkspace,    SerializedWorkspaceLocation, Workspace, WorkspaceDb, notifications::DetachAndPromptErr,}; use zed_actions::OpenRemote; use crate::{highlights_for_path, icon_for_remote_connection, open_remote_project}; pub struct SidebarRecentProjects {    pub picker: Entity<Picker<SidebarRecentProjectsDelegate>>,    _subscription: Subscription,} impl SidebarRecentProjects {    pub fn popover(        workspace: WeakEntity<Workspace>,        window_project_groups: Vec<ProjectGroupKey>,        _focus_handle: FocusHandle,        window: &mut Window,        cx: &mut App,    ) -> Entity<Self> {        let fs = workspace            .upgrade()            .map(|ws| ws.read(cx).app_state().fs.clone());         cx.new(|cx| {            let delegate = SidebarRecentProjectsDelegate {                workspace,                window_project_groups,                workspaces: Vec::new(),                filtered_workspaces: Vec::new(),                selected_index: 0,                has_any_non_local_projects: false,                focus_handle: cx.focus_handle(),            };             let picker: Entity<Picker<SidebarRecentProjectsDelegate>> = cx.new(|cx| {                Picker::list(delegate, window, cx)                    .list_measure_all()                    .show_scrollbar(true)                    .initial_width(rems(18.))                    .popover()            });             let picker_focus_handle = picker.focus_handle(cx);            picker.update(cx, |picker, _| {                picker.delegate.focus_handle = picker_focus_handle;            });             let _subscription =                cx.subscribe(&picker, |_this: &mut Self, _, _, cx| cx.emit(DismissEvent));             let db = WorkspaceDb::global(cx);            cx.spawn_in(window, async move |this, cx| {                let Some(fs) = fs else { return };                let workspaces = db                    .recent_project_workspaces(fs.as_ref())                    .await                    .log_err()                    .unwrap_or_default();                this.update_in(cx, move |this, window, cx| {                    this.picker.update(cx, move |picker, cx| {                        picker.delegate.set_workspaces(workspaces);                        picker.update_matches(picker.query(cx), window, cx)                    })                })                .ok();            })            .detach();             picker.focus_handle(cx).focus(window, cx);             Self {                picker,                _subscription,            }        })    }} impl EventEmitter<DismissEvent> for SidebarRecentProjects {} impl Focusable for SidebarRecentProjects {    fn focus_handle(&self, cx: &App) -> FocusHandle {        self.picker.focus_handle(cx)    }} impl Render for SidebarRecentProjects {    fn render(&mut self, _: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {        v_flex()            .key_context("SidebarRecentProjects")            .w(rems(18.))            .child(self.picker.clone())    }} pub struct SidebarRecentProjectsDelegate {    workspace: WeakEntity<Workspace>,    window_project_groups: Vec<ProjectGroupKey>,    workspaces: Vec<RecentWorkspace>,    filtered_workspaces: Vec<StringMatch>,    selected_index: usize,    has_any_non_local_projects: bool,    focus_handle: FocusHandle,} impl SidebarRecentProjectsDelegate {    pub fn set_workspaces(&mut self, workspaces: Vec<RecentWorkspace>) {        self.has_any_non_local_projects = workspaces            .iter()            .any(|workspace| !matches!(workspace.location, SerializedWorkspaceLocation::Local));        self.workspaces = workspaces;    }} impl EventEmitter<DismissEvent> for SidebarRecentProjectsDelegate {} impl PickerDelegate for SidebarRecentProjectsDelegate {    type ListItem = AnyElement;     fn name() -> &'static str {        "sidebar recent projects"    }     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {        "Search projects…".into()    }     fn match_count(&self) -> usize {        self.filtered_workspaces.len()    }     fn selected_index(&self) -> usize {        self.selected_index    }     fn set_selected_index(        &mut self,        ix: usize,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) {        self.selected_index = ix;    }     fn update_matches(        &mut self,        query: String,        _: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Task<()> {        let query = query.trim_start();        let case = fuzzy_nucleo::Case::smart_if_uppercase_in(query);        let is_empty_query = query.is_empty();         let current_workspace_id = self            .workspace            .upgrade()            .and_then(|ws| ws.read(cx).database_id());         let candidates: Vec<_> = self            .workspaces            .iter()            .enumerate()            .filter(|(_, workspace)| {                Some(workspace.workspace_id) != current_workspace_id                    && !self                        .window_project_groups                        .iter()                        .any(|key| key.matches(&workspace.project_group_key()))            })            .map(|(id, workspace)| {                let combined_string = workspace                    .identity_paths                    .ordered_paths()                    .map(|path| path.compact().to_string_lossy().into_owned())                    .collect::<Vec<_>>()                    .concat();                StringMatchCandidate::new(id, &combined_string)            })            .collect();         if is_empty_query {            self.filtered_workspaces = candidates                .into_iter()                .map(|candidate| StringMatch {                    candidate_id: candidate.id,                    score: 0.0,                    positions: Vec::new(),                    string: candidate.string,                })                .collect();        } else {            self.filtered_workspaces = match_strings(                &candidates,                query,                case,                fuzzy_nucleo::LengthPenalty::On,                100,            );        }         self.selected_index = 0;        Task::ready(())    }     fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let Some(hit) = self.filtered_workspaces.get(self.selected_index) else {            return;        };        let Some(recent_workspace) = self.workspaces.get(hit.candidate_id) else {            return;        };         let Some(workspace) = self.workspace.upgrade() else {            return;        };         match &recent_workspace.location {            SerializedWorkspaceLocation::Local => {                if let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() {                    let paths = recent_workspace.paths.paths().to_vec();                    cx.defer(move |cx| {                        if let Some(task) = handle                            .update(cx, |multi_workspace, window, cx| {                                multi_workspace.open_project(paths, OpenMode::Activate, window, cx)                            })                            .log_err()                        {                            task.detach_and_log_err(cx);                        }                    });                }            }            SerializedWorkspaceLocation::Remote(connection) => {                let mut connection = connection.clone();                workspace.update(cx, |workspace, cx| {                    let app_state = workspace.app_state().clone();                    let replace_window = window.window_handle().downcast::<MultiWorkspace>();                    let open_options = OpenOptions {                        requesting_window: replace_window,                        ..Default::default()                    };                    if let RemoteConnectionOptions::Ssh(connection) = &mut connection {                        crate::RemoteSettings::get_global(cx)                            .fill_connection_options_from_settings(connection);                    };                    let paths = recent_workspace.paths.paths().to_vec();                    cx.spawn_in(window, async move |_, cx| {                        open_remote_project(connection.clone(), paths, app_state, open_options, cx)                            .await                    })                    .detach_and_prompt_err(                        "Failed to open project",                        window,                        cx,                        |_, _, _| None,                    );                });            }        }        cx.emit(DismissEvent);    }     fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}     fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {        let text = if self.workspaces.is_empty() {            "Recently opened projects will show up here"        } else {            "No matches"        };        Some(text.into())    }     fn render_match(        &self,        ix: usize,        selected: bool,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Option<Self::ListItem> {        let hit = self.filtered_workspaces.get(ix)?;        let workspace = self.workspaces.get(hit.candidate_id)?;         let ordered_paths: Vec<_> = workspace            .identity_paths            .ordered_paths()            .map(|p| p.compact().to_string_lossy().to_string())            .collect();         let tooltip_path: SharedString = match &workspace.location {            SerializedWorkspaceLocation::Remote(options) => {                let host = options.display_name();                if ordered_paths.len() == 1 {                    format!("{} ({})", ordered_paths[0], host).into()                } else {                    format!("{}\n({})", ordered_paths.join("\n"), host).into()                }            }            _ => ordered_paths.join("\n").into(),        };         let mut path_start_offset = 0;        let match_labels: Vec<_> = workspace            .identity_paths            .ordered_paths()            .map(|p| p.compact())            .map(|path| {                let (label, path_match) =                    highlights_for_path(path.as_ref(), &hit.positions, path_start_offset);                path_start_offset += path_match.text.len();                label            })            .collect();         let prefix = match &workspace.location {            SerializedWorkspaceLocation::Remote(options) => {                Some(SharedString::from(options.display_name()))            }            _ => None,        };         let highlighted_match = HighlightedMatchWithPaths {            prefix,            match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "),            paths: Vec::new(),            active: false,        };         let icon = icon_for_remote_connection(match &workspace.location {            SerializedWorkspaceLocation::Local => None,            SerializedWorkspaceLocation::Remote(options) => Some(options),        });         Some(            ListItem::new(ix)                .toggle_state(selected)                .inset(true)                .spacing(ListItemSpacing::Sparse)                .child(                    h_flex()                        .w_full()                        .min_w_0()                        .gap_3()                        .flex_grow_1()                        .when(self.has_any_non_local_projects, |this| {                            this.child(Icon::new(icon).color(Color::Muted))                        })                        .child(highlighted_match.render(window, cx)),                )                .tooltip(move |_, cx| {                    Tooltip::with_meta(                        "Open Project in This Window",                        None,                        tooltip_path.clone(),                        cx,                    )                })                .into_any_element(),        )    }     fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {        let focus_handle = self.focus_handle.clone();         Some(            v_flex()                .p_1p5()                .flex_1()                .gap_1()                .border_t_1()                .border_color(cx.theme().colors().border_variant)                .child({                    let open_action = workspace::Open {                        create_new_window: Some(false),                    };                     ButtonLike::new("open_local_folder")                        .child(                            h_flex()                                .w_full()                                .gap_1()                                .justify_between()                                .child(Label::new("Open Local Folders"))                                .child(KeyBinding::for_action_in(&open_action, &focus_handle, cx)),                        )                        .on_click(cx.listener(move |_, _, window, cx| {                            window.dispatch_action(open_action.boxed_clone(), cx);                            cx.emit(DismissEvent);                        }))                })                .child(                    ButtonLike::new("open_remote_folder")                        .child(                            h_flex()                                .w_full()                                .gap_1()                                .justify_between()                                .child(Label::new("Open Remote Folder"))                                .child(KeyBinding::for_action(                                    &OpenRemote {                                        from_existing_connection: false,                                        create_new_window: Some(false),                                    },                                    cx,                                )),                        )                        .on_click(cx.listener(|_, _, window, cx| {                            window.dispatch_action(                                OpenRemote {                                    from_existing_connection: false,                                    create_new_window: Some(false),                                }                                .boxed_clone(),                                cx,                            );                            cx.emit(DismissEvent);                        })),                )                .into_any(),        )    }}