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