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