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                        RecentProjects::new(delegate, cx)
 80                    });
 81                } else {
 82                    workspace.show_notification(0, cx, |cx| {
 83                        cx.build_view(|_| MessageNotification::new("No recent projects to open."))
 84                    })
 85                }
 86            })?;
 87            Ok(())
 88        }))
 89    }
 90    pub fn open_popover(
 91        workspace: WeakView<Workspace>,
 92        workspaces: Vec<WorkspaceLocation>,
 93        cx: &mut WindowContext<'_>,
 94    ) -> Self {
 95        Self::new(
 96            RecentProjectsDelegate::new(workspace, workspaces, false),
 97            cx,
 98        )
 99    }
100}
101
102impl EventEmitter<DismissEvent> for RecentProjects {}
103
104impl FocusableView for RecentProjects {
105    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
106        self.picker.focus_handle(cx)
107    }
108}
109
110impl Render for RecentProjects {
111    type Element = Div;
112
113    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
114        v_stack().w(rems(34.)).child(self.picker.clone())
115    }
116}
117
118pub struct RecentProjectsDelegate {
119    workspace: WeakView<Workspace>,
120    workspace_locations: Vec<WorkspaceLocation>,
121    selected_match_index: usize,
122    matches: Vec<StringMatch>,
123    render_paths: bool,
124}
125
126impl RecentProjectsDelegate {
127    fn new(
128        workspace: WeakView<Workspace>,
129        workspace_locations: Vec<WorkspaceLocation>,
130        render_paths: bool,
131    ) -> Self {
132        Self {
133            workspace,
134            workspace_locations,
135            selected_match_index: 0,
136            matches: Default::default(),
137            render_paths,
138        }
139    }
140}
141impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
142impl PickerDelegate for RecentProjectsDelegate {
143    type ListItem = ListItem;
144
145    fn placeholder_text(&self) -> Arc<str> {
146        "Recent Projects...".into()
147    }
148
149    fn match_count(&self) -> usize {
150        self.matches.len()
151    }
152
153    fn selected_index(&self) -> usize {
154        self.selected_match_index
155    }
156
157    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
158        self.selected_match_index = ix;
159    }
160
161    fn update_matches(
162        &mut self,
163        query: String,
164        cx: &mut ViewContext<Picker<Self>>,
165    ) -> gpui::Task<()> {
166        let query = query.trim_start();
167        let smart_case = query.chars().any(|c| c.is_uppercase());
168        let candidates = self
169            .workspace_locations
170            .iter()
171            .enumerate()
172            .map(|(id, location)| {
173                let combined_string = location
174                    .paths()
175                    .iter()
176                    .map(|path| path.compact().to_string_lossy().into_owned())
177                    .collect::<Vec<_>>()
178                    .join("");
179                StringMatchCandidate::new(id, combined_string)
180            })
181            .collect::<Vec<_>>();
182        self.matches = smol::block_on(fuzzy::match_strings(
183            candidates.as_slice(),
184            query,
185            smart_case,
186            100,
187            &Default::default(),
188            cx.background_executor().clone(),
189        ));
190        self.matches.sort_unstable_by_key(|m| m.candidate_id);
191
192        self.selected_match_index = self
193            .matches
194            .iter()
195            .enumerate()
196            .rev()
197            .max_by_key(|(_, m)| OrderedFloat(m.score))
198            .map(|(ix, _)| ix)
199            .unwrap_or(0);
200        Task::ready(())
201    }
202
203    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
204        if let Some((selected_match, workspace)) = self
205            .matches
206            .get(self.selected_index())
207            .zip(self.workspace.upgrade())
208        {
209            let workspace_location = &self.workspace_locations[selected_match.candidate_id];
210            workspace
211                .update(cx, |workspace, cx| {
212                    workspace
213                        .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
214                })
215                .detach_and_log_err(cx);
216            cx.emit(DismissEvent);
217        }
218    }
219
220    fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
221
222    fn render_match(
223        &self,
224        ix: usize,
225        selected: bool,
226        _cx: &mut ViewContext<Picker<Self>>,
227    ) -> Option<Self::ListItem> {
228        let Some(r#match) = self.matches.get(ix) else {
229            return None;
230        };
231
232        let highlighted_location = HighlightedWorkspaceLocation::new(
233            &r#match,
234            &self.workspace_locations[r#match.candidate_id],
235        );
236
237        Some(
238            ListItem::new(ix).inset(true).selected(selected).child(
239                v_stack()
240                    .child(highlighted_location.names)
241                    .when(self.render_paths, |this| {
242                        this.children(highlighted_location.paths)
243                    }),
244            ),
245        )
246    }
247}