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