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