recent_projects.rs

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