recent_projects.rs

  1mod highlighted_workspace_location;
  2mod projects;
  3
  4use fuzzy::{StringMatch, StringMatchCandidate};
  5use gpui::{
  6    AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task, View,
  7    ViewContext, WeakView,
  8};
  9use highlighted_workspace_location::HighlightedWorkspaceLocation;
 10use ordered_float::OrderedFloat;
 11use picker::{Picker, PickerDelegate};
 12use std::sync::Arc;
 13use ui::{prelude::*, ListItem};
 14use util::paths::PathExt;
 15use workspace::{
 16    notifications::simple_message_notification::MessageNotification, ModalView, Workspace,
 17    WorkspaceLocation, WORKSPACE_DB,
 18};
 19
 20pub use projects::OpenRecent;
 21
 22pub fn init(cx: &mut AppContext) {
 23    cx.observe_new_views(RecentProjects::register).detach();
 24}
 25
 26#[derive(Clone)]
 27pub struct RecentProjects {
 28    pub picker: View<Picker<RecentProjectsDelegate>>,
 29}
 30
 31impl ModalView for RecentProjects {}
 32
 33impl RecentProjects {
 34    fn new(delegate: RecentProjectsDelegate, cx: &mut WindowContext<'_>) -> Self {
 35        Self {
 36            picker: cx.build_view(|cx| Picker::new(delegate, cx)),
 37        }
 38    }
 39
 40    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 41        workspace.register_action(|workspace, _: &OpenRecent, cx| {
 42            let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
 43                if let Some(handler) = Self::open(workspace, cx) {
 44                    handler.detach_and_log_err(cx);
 45                }
 46                return;
 47            };
 48
 49            recent_projects.update(cx, |recent_projects, cx| {
 50                recent_projects
 51                    .picker
 52                    .update(cx, |picker, cx| picker.cycle_selection(cx))
 53            });
 54        });
 55    }
 56
 57    fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
 58        Some(cx.spawn(|workspace, mut cx| async move {
 59            let workspace_locations: Vec<_> = cx
 60                .background_executor()
 61                .spawn(async {
 62                    WORKSPACE_DB
 63                        .recent_workspaces_on_disk()
 64                        .await
 65                        .unwrap_or_default()
 66                        .into_iter()
 67                        .map(|(_, location)| location)
 68                        .collect()
 69                })
 70                .await;
 71
 72            workspace.update(&mut cx, |workspace, cx| {
 73                if !workspace_locations.is_empty() {
 74                    let weak_workspace = cx.view().downgrade();
 75                    workspace.toggle_modal(cx, |cx| {
 76                        let delegate =
 77                            RecentProjectsDelegate::new(weak_workspace, workspace_locations, true);
 78
 79                        let modal = RecentProjects::new(delegate, cx);
 80                        cx.subscribe(&modal.picker, |_, _, _, cx| cx.emit(DismissEvent))
 81                            .detach();
 82                        modal
 83                    });
 84                } else {
 85                    workspace.show_notification(0, cx, |cx| {
 86                        cx.build_view(|_| MessageNotification::new("No recent projects to open."))
 87                    })
 88                }
 89            })?;
 90            Ok(())
 91        }))
 92    }
 93    pub fn open_popover(
 94        workspace: WeakView<Workspace>,
 95        workspaces: Vec<WorkspaceLocation>,
 96        cx: &mut WindowContext<'_>,
 97    ) -> Self {
 98        Self::new(
 99            RecentProjectsDelegate::new(workspace, workspaces, false),
100            cx,
101        )
102    }
103}
104
105impl EventEmitter<DismissEvent> for RecentProjects {}
106
107impl FocusableView for RecentProjects {
108    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
109        self.picker.focus_handle(cx)
110    }
111}
112
113impl Render for RecentProjects {
114    type Element = Div;
115
116    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
117        v_stack().w(rems(34.)).child(self.picker.clone())
118    }
119}
120
121pub struct RecentProjectsDelegate {
122    workspace: WeakView<Workspace>,
123    workspace_locations: Vec<WorkspaceLocation>,
124    selected_match_index: usize,
125    matches: Vec<StringMatch>,
126    render_paths: bool,
127}
128
129impl RecentProjectsDelegate {
130    fn new(
131        workspace: WeakView<Workspace>,
132        workspace_locations: Vec<WorkspaceLocation>,
133        render_paths: bool,
134    ) -> Self {
135        Self {
136            workspace,
137            workspace_locations,
138            selected_match_index: 0,
139            matches: Default::default(),
140            render_paths,
141        }
142    }
143}
144impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
145impl PickerDelegate for RecentProjectsDelegate {
146    type ListItem = ListItem;
147
148    fn placeholder_text(&self) -> Arc<str> {
149        "Recent Projects...".into()
150    }
151
152    fn match_count(&self) -> usize {
153        self.matches.len()
154    }
155
156    fn selected_index(&self) -> usize {
157        self.selected_match_index
158    }
159
160    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
161        self.selected_match_index = ix;
162    }
163
164    fn update_matches(
165        &mut self,
166        query: String,
167        cx: &mut ViewContext<Picker<Self>>,
168    ) -> gpui::Task<()> {
169        let query = query.trim_start();
170        let smart_case = query.chars().any(|c| c.is_uppercase());
171        let candidates = self
172            .workspace_locations
173            .iter()
174            .enumerate()
175            .map(|(id, location)| {
176                let combined_string = location
177                    .paths()
178                    .iter()
179                    .map(|path| path.compact().to_string_lossy().into_owned())
180                    .collect::<Vec<_>>()
181                    .join("");
182                StringMatchCandidate::new(id, combined_string)
183            })
184            .collect::<Vec<_>>();
185        self.matches = smol::block_on(fuzzy::match_strings(
186            candidates.as_slice(),
187            query,
188            smart_case,
189            100,
190            &Default::default(),
191            cx.background_executor().clone(),
192        ));
193        self.matches.sort_unstable_by_key(|m| m.candidate_id);
194
195        self.selected_match_index = self
196            .matches
197            .iter()
198            .enumerate()
199            .rev()
200            .max_by_key(|(_, m)| OrderedFloat(m.score))
201            .map(|(ix, _)| ix)
202            .unwrap_or(0);
203        Task::ready(())
204    }
205
206    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
207        if let Some((selected_match, workspace)) = self
208            .matches
209            .get(self.selected_index())
210            .zip(self.workspace.upgrade())
211        {
212            let workspace_location = &self.workspace_locations[selected_match.candidate_id];
213            workspace
214                .update(cx, |workspace, cx| {
215                    workspace
216                        .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
217                })
218                .detach_and_log_err(cx);
219            cx.emit(DismissEvent);
220        }
221    }
222
223    fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
224
225    fn render_match(
226        &self,
227        ix: usize,
228        selected: bool,
229        _cx: &mut ViewContext<Picker<Self>>,
230    ) -> Option<Self::ListItem> {
231        let Some(r#match) = self.matches.get(ix) else {
232            return None;
233        };
234
235        let highlighted_location = HighlightedWorkspaceLocation::new(
236            &r#match,
237            &self.workspace_locations[r#match.candidate_id],
238        );
239
240        Some(
241            ListItem::new(ix).inset(true).selected(selected).child(
242                v_stack()
243                    .child(highlighted_location.names)
244                    .when(self.render_paths, |this| {
245                        this.children(highlighted_location.paths)
246                    }),
247            ),
248        )
249    }
250}