recent_projects.rs

  1mod highlighted_workspace_location;
  2
  3use fuzzy::{StringMatch, StringMatchCandidate};
  4use gpui::{
  5    actions,
  6    anyhow::Result,
  7    elements::{Flex, ParentElement},
  8    AnyElement, AppContext, Element, Task, ViewContext,
  9};
 10use highlighted_workspace_location::HighlightedWorkspaceLocation;
 11use ordered_float::OrderedFloat;
 12use picker::{Picker, PickerDelegate, PickerEvent};
 13use settings::Settings;
 14use std::sync::Arc;
 15use workspace::{
 16    notifications::simple_message_notification::MessageNotification, OpenPaths, Workspace,
 17    WorkspaceLocation, WORKSPACE_DB,
 18};
 19
 20actions!(projects, [OpenRecent]);
 21
 22pub fn init(cx: &mut AppContext) {
 23    cx.add_async_action(toggle);
 24    RecentProjects::init(cx);
 25}
 26
 27fn toggle(
 28    _: &mut Workspace,
 29    _: &OpenRecent,
 30    cx: &mut ViewContext<Workspace>,
 31) -> Option<Task<Result<()>>> {
 32    Some(cx.spawn(|workspace, mut cx| async move {
 33        let workspace_locations: Vec<_> = cx
 34            .background()
 35            .spawn(async {
 36                WORKSPACE_DB
 37                    .recent_workspaces_on_disk()
 38                    .await
 39                    .unwrap_or_default()
 40                    .into_iter()
 41                    .map(|(_, location)| location)
 42                    .collect()
 43            })
 44            .await;
 45
 46        workspace.update(&mut cx, |workspace, cx| {
 47            if !workspace_locations.is_empty() {
 48                workspace.toggle_modal(cx, |_, cx| {
 49                    cx.add_view(|cx| {
 50                        RecentProjects::new(RecentProjectsDelegate::new(workspace_locations), cx)
 51                            .with_max_size(800., 1200.)
 52                    })
 53                });
 54            } else {
 55                workspace.show_notification(0, cx, |cx| {
 56                    cx.add_view(|_| MessageNotification::new_message("No recent projects to open."))
 57                })
 58            }
 59        })?;
 60        Ok(())
 61    }))
 62}
 63
 64type RecentProjects = Picker<RecentProjectsDelegate>;
 65
 66struct RecentProjectsDelegate {
 67    workspace_locations: Vec<WorkspaceLocation>,
 68    selected_match_index: usize,
 69    matches: Vec<StringMatch>,
 70}
 71
 72impl RecentProjectsDelegate {
 73    fn new(workspace_locations: Vec<WorkspaceLocation>) -> Self {
 74        Self {
 75            workspace_locations,
 76            selected_match_index: 0,
 77            matches: Default::default(),
 78        }
 79    }
 80}
 81
 82impl PickerDelegate for RecentProjectsDelegate {
 83    fn placeholder_text(&self) -> Arc<str> {
 84        "Recent Projects...".into()
 85    }
 86
 87    fn match_count(&self) -> usize {
 88        self.matches.len()
 89    }
 90
 91    fn selected_index(&self) -> usize {
 92        self.selected_match_index
 93    }
 94
 95    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
 96        self.selected_match_index = ix;
 97    }
 98
 99    fn update_matches(
100        &mut self,
101        query: String,
102        cx: &mut ViewContext<RecentProjects>,
103    ) -> gpui::Task<()> {
104        let query = query.trim_start();
105        let smart_case = query.chars().any(|c| c.is_uppercase());
106        let candidates = self
107            .workspace_locations
108            .iter()
109            .enumerate()
110            .map(|(id, location)| {
111                let combined_string = location
112                    .paths()
113                    .iter()
114                    .map(|path| path.to_string_lossy().to_owned())
115                    .collect::<Vec<_>>()
116                    .join("");
117                StringMatchCandidate::new(id, combined_string)
118            })
119            .collect::<Vec<_>>();
120        self.matches = smol::block_on(fuzzy::match_strings(
121            candidates.as_slice(),
122            query,
123            smart_case,
124            100,
125            &Default::default(),
126            cx.background().clone(),
127        ));
128        self.matches.sort_unstable_by_key(|m| m.candidate_id);
129
130        self.selected_match_index = self
131            .matches
132            .iter()
133            .enumerate()
134            .rev()
135            .max_by_key(|(_, m)| OrderedFloat(m.score))
136            .map(|(ix, _)| ix)
137            .unwrap_or(0);
138        Task::ready(())
139    }
140
141    fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
142        if let Some(selected_match) = &self.matches.get(self.selected_index()) {
143            let workspace_location = &self.workspace_locations[selected_match.candidate_id];
144            cx.dispatch_action(OpenPaths {
145                paths: workspace_location.paths().as_ref().clone(),
146            });
147            cx.emit(PickerEvent::Dismiss);
148        }
149    }
150
151    fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
152
153    fn render_match(
154        &self,
155        ix: usize,
156        mouse_state: &mut gpui::MouseState,
157        selected: bool,
158        cx: &gpui::AppContext,
159    ) -> AnyElement<Picker<Self>> {
160        let settings = cx.global::<Settings>();
161        let string_match = &self.matches[ix];
162        let style = settings.theme.picker.item.style_for(mouse_state, selected);
163
164        let highlighted_location = HighlightedWorkspaceLocation::new(
165            &string_match,
166            &self.workspace_locations[string_match.candidate_id],
167        );
168
169        Flex::column()
170            .with_child(highlighted_location.names.render(style.label.clone()))
171            .with_children(
172                highlighted_location
173                    .paths
174                    .into_iter()
175                    .map(|highlighted_path| highlighted_path.render(style.label.clone())),
176            )
177            .flex(1., false)
178            .contained()
179            .with_style(style.container)
180            .into_any_named("match")
181    }
182}