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