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, WeakViewHandle,
  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, Workspace, WorkspaceLocation,
 17    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                    let workspace = cx.weak_handle();
 50                    cx.add_view(|cx| {
 51                        RecentProjects::new(
 52                            RecentProjectsDelegate::new(workspace, workspace_locations),
 53                            cx,
 54                        )
 55                        .with_max_size(800., 1200.)
 56                    })
 57                });
 58            } else {
 59                workspace.show_notification(0, cx, |cx| {
 60                    cx.add_view(|_| MessageNotification::new("No recent projects to open."))
 61                })
 62            }
 63        })?;
 64        Ok(())
 65    }))
 66}
 67
 68type RecentProjects = Picker<RecentProjectsDelegate>;
 69
 70struct RecentProjectsDelegate {
 71    workspace: WeakViewHandle<Workspace>,
 72    workspace_locations: Vec<WorkspaceLocation>,
 73    selected_match_index: usize,
 74    matches: Vec<StringMatch>,
 75}
 76
 77impl RecentProjectsDelegate {
 78    fn new(
 79        workspace: WeakViewHandle<Workspace>,
 80        workspace_locations: Vec<WorkspaceLocation>,
 81    ) -> Self {
 82        Self {
 83            workspace,
 84            workspace_locations,
 85            selected_match_index: 0,
 86            matches: Default::default(),
 87        }
 88    }
 89}
 90
 91impl PickerDelegate for RecentProjectsDelegate {
 92    fn placeholder_text(&self) -> Arc<str> {
 93        "Recent Projects...".into()
 94    }
 95
 96    fn match_count(&self) -> usize {
 97        self.matches.len()
 98    }
 99
100    fn selected_index(&self) -> usize {
101        self.selected_match_index
102    }
103
104    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
105        self.selected_match_index = ix;
106    }
107
108    fn update_matches(
109        &mut self,
110        query: String,
111        cx: &mut ViewContext<RecentProjects>,
112    ) -> gpui::Task<()> {
113        let query = query.trim_start();
114        let smart_case = query.chars().any(|c| c.is_uppercase());
115        let candidates = self
116            .workspace_locations
117            .iter()
118            .enumerate()
119            .map(|(id, location)| {
120                let combined_string = location
121                    .paths()
122                    .iter()
123                    .map(|path| path.to_string_lossy().to_owned())
124                    .collect::<Vec<_>>()
125                    .join("");
126                StringMatchCandidate::new(id, combined_string)
127            })
128            .collect::<Vec<_>>();
129        self.matches = smol::block_on(fuzzy::match_strings(
130            candidates.as_slice(),
131            query,
132            smart_case,
133            100,
134            &Default::default(),
135            cx.background().clone(),
136        ));
137        self.matches.sort_unstable_by_key(|m| m.candidate_id);
138
139        self.selected_match_index = self
140            .matches
141            .iter()
142            .enumerate()
143            .rev()
144            .max_by_key(|(_, m)| OrderedFloat(m.score))
145            .map(|(ix, _)| ix)
146            .unwrap_or(0);
147        Task::ready(())
148    }
149
150    fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
151        if let Some((selected_match, workspace)) = self
152            .matches
153            .get(self.selected_index())
154            .zip(self.workspace.upgrade(cx))
155        {
156            let workspace_location = &self.workspace_locations[selected_match.candidate_id];
157            workspace
158                .update(cx, |workspace, cx| {
159                    workspace
160                        .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
161                })
162                .detach_and_log_err(cx);
163            cx.emit(PickerEvent::Dismiss);
164        }
165    }
166
167    fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
168
169    fn render_match(
170        &self,
171        ix: usize,
172        mouse_state: &mut gpui::MouseState,
173        selected: bool,
174        cx: &gpui::AppContext,
175    ) -> AnyElement<Picker<Self>> {
176        let settings = cx.global::<Settings>();
177        let string_match = &self.matches[ix];
178        let style = settings.theme.picker.item.style_for(mouse_state, selected);
179
180        let highlighted_location = HighlightedWorkspaceLocation::new(
181            &string_match,
182            &self.workspace_locations[string_match.candidate_id],
183        );
184
185        Flex::column()
186            .with_child(highlighted_location.names.render(style.label.clone()))
187            .with_children(
188                highlighted_location
189                    .paths
190                    .into_iter()
191                    .map(|highlighted_path| highlighted_path.render(style.label.clone())),
192            )
193            .flex(1., false)
194            .contained()
195            .with_style(style.container)
196            .into_any_named("match")
197    }
198}