recent_projects.rs

  1mod highlighted_workspace_location;
  2
  3use fuzzy::{StringMatch, StringMatchCandidate};
  4use gpui::{
  5    actions,
  6    anyhow::Result,
  7    elements::{Flex, ParentElement},
  8    AnyElement, AppContext, Element, Task, ViewContext, WeakViewHandle,
  9};
 10use highlighted_workspace_location::HighlightedWorkspaceLocation;
 11use ordered_float::OrderedFloat;
 12use picker::{Picker, PickerDelegate, PickerEvent};
 13use std::sync::Arc;
 14use workspace::{
 15    notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
 16    WORKSPACE_DB,
 17};
 18
 19actions!(projects, [OpenRecent]);
 20
 21pub fn init(cx: &mut AppContext) {
 22    cx.add_async_action(toggle);
 23    RecentProjects::init(cx);
 24}
 25
 26fn toggle(
 27    _: &mut Workspace,
 28    _: &OpenRecent,
 29    cx: &mut ViewContext<Workspace>,
 30) -> Option<Task<Result<()>>> {
 31    Some(cx.spawn(|workspace, mut cx| async move {
 32        let workspace_locations: Vec<_> = cx
 33            .background()
 34            .spawn(async {
 35                WORKSPACE_DB
 36                    .recent_workspaces_on_disk()
 37                    .await
 38                    .unwrap_or_default()
 39                    .into_iter()
 40                    .map(|(_, location)| location)
 41                    .collect()
 42            })
 43            .await;
 44
 45        workspace.update(&mut cx, |workspace, cx| {
 46            if !workspace_locations.is_empty() {
 47                workspace.toggle_modal(cx, |_, cx| {
 48                    let workspace = cx.weak_handle();
 49                    cx.add_view(|cx| {
 50                        RecentProjects::new(
 51                            RecentProjectsDelegate::new(workspace, workspace_locations, true),
 52                            cx,
 53                        )
 54                        .with_max_size(800., 1200.)
 55                    })
 56                });
 57            } else {
 58                workspace.show_notification(0, cx, |cx| {
 59                    cx.add_view(|_| MessageNotification::new("No recent projects to open."))
 60                })
 61            }
 62        })?;
 63        Ok(())
 64    }))
 65}
 66
 67pub fn build_recent_projects(
 68    workspace: WeakViewHandle<Workspace>,
 69    workspaces: Vec<WorkspaceLocation>,
 70    cx: &mut ViewContext<RecentProjects>,
 71) -> RecentProjects {
 72    Picker::new(
 73        RecentProjectsDelegate::new(workspace, workspaces, false),
 74        cx,
 75    )
 76    .with_theme(|theme| theme.picker.clone())
 77}
 78
 79pub type RecentProjects = Picker<RecentProjectsDelegate>;
 80
 81pub struct RecentProjectsDelegate {
 82    workspace: WeakViewHandle<Workspace>,
 83    workspace_locations: Vec<WorkspaceLocation>,
 84    selected_match_index: usize,
 85    matches: Vec<StringMatch>,
 86    render_paths: bool,
 87}
 88
 89impl RecentProjectsDelegate {
 90    fn new(
 91        workspace: WeakViewHandle<Workspace>,
 92        workspace_locations: Vec<WorkspaceLocation>,
 93        render_paths: bool,
 94    ) -> Self {
 95        Self {
 96            workspace,
 97            workspace_locations,
 98            selected_match_index: 0,
 99            matches: Default::default(),
100            render_paths,
101        }
102    }
103}
104
105impl PickerDelegate for RecentProjectsDelegate {
106    fn placeholder_text(&self) -> Arc<str> {
107        "Recent Projects...".into()
108    }
109
110    fn match_count(&self) -> usize {
111        self.matches.len()
112    }
113
114    fn selected_index(&self) -> usize {
115        self.selected_match_index
116    }
117
118    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
119        self.selected_match_index = ix;
120    }
121
122    fn update_matches(
123        &mut self,
124        query: String,
125        cx: &mut ViewContext<RecentProjects>,
126    ) -> gpui::Task<()> {
127        let query = query.trim_start();
128        let smart_case = query.chars().any(|c| c.is_uppercase());
129        let candidates = self
130            .workspace_locations
131            .iter()
132            .enumerate()
133            .map(|(id, location)| {
134                let combined_string = location
135                    .paths()
136                    .iter()
137                    .map(|path| {
138                        let compact = util::paths::compact(&path);
139                        compact.to_string_lossy().into_owned()
140                    })
141                    .collect::<Vec<_>>()
142                    .join("");
143                StringMatchCandidate::new(id, combined_string)
144            })
145            .collect::<Vec<_>>();
146        self.matches = smol::block_on(fuzzy::match_strings(
147            candidates.as_slice(),
148            query,
149            smart_case,
150            100,
151            &Default::default(),
152            cx.background().clone(),
153        ));
154        self.matches.sort_unstable_by_key(|m| m.candidate_id);
155
156        self.selected_match_index = self
157            .matches
158            .iter()
159            .enumerate()
160            .rev()
161            .max_by_key(|(_, m)| OrderedFloat(m.score))
162            .map(|(ix, _)| ix)
163            .unwrap_or(0);
164        Task::ready(())
165    }
166
167    fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
168        if let Some((selected_match, workspace)) = self
169            .matches
170            .get(self.selected_index())
171            .zip(self.workspace.upgrade(cx))
172        {
173            let workspace_location = &self.workspace_locations[selected_match.candidate_id];
174            workspace
175                .update(cx, |workspace, cx| {
176                    workspace
177                        .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
178                })
179                .detach_and_log_err(cx);
180            cx.emit(PickerEvent::Dismiss);
181        }
182    }
183
184    fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
185
186    fn render_match(
187        &self,
188        ix: usize,
189        mouse_state: &mut gpui::MouseState,
190        selected: bool,
191        cx: &gpui::AppContext,
192    ) -> AnyElement<Picker<Self>> {
193        let theme = theme::current(cx);
194        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
195
196        let string_match = &self.matches[ix];
197
198        let highlighted_location = HighlightedWorkspaceLocation::new(
199            &string_match,
200            &self.workspace_locations[string_match.candidate_id],
201        );
202
203        Flex::column()
204            .with_child(highlighted_location.names.render(style.label.clone()))
205            .with_children(
206                highlighted_location
207                    .paths
208                    .into_iter()
209                    .filter(|_| self.render_paths)
210                    .map(|highlighted_path| highlighted_path.render(style.label.clone())),
211            )
212            .flex(1., false)
213            .contained()
214            .with_style(style.container)
215            .into_any_named("match")
216    }
217}