recent_projects.rs

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