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 std::sync::Arc;
 14use util::paths::PathExt;
 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, true),
 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
 68pub fn build_recent_projects(
 69    workspace: WeakViewHandle<Workspace>,
 70    workspaces: Vec<WorkspaceLocation>,
 71    cx: &mut ViewContext<RecentProjects>,
 72) -> RecentProjects {
 73    Picker::new(
 74        RecentProjectsDelegate::new(workspace, workspaces, false),
 75        cx,
 76    )
 77    .with_theme(|theme| theme.picker.clone())
 78}
 79
 80pub type RecentProjects = Picker<RecentProjectsDelegate>;
 81
 82pub struct RecentProjectsDelegate {
 83    workspace: WeakViewHandle<Workspace>,
 84    workspace_locations: Vec<WorkspaceLocation>,
 85    selected_match_index: usize,
 86    matches: Vec<StringMatch>,
 87    render_paths: bool,
 88}
 89
 90impl RecentProjectsDelegate {
 91    fn new(
 92        workspace: WeakViewHandle<Workspace>,
 93        workspace_locations: Vec<WorkspaceLocation>,
 94        render_paths: bool,
 95    ) -> Self {
 96        Self {
 97            workspace,
 98            workspace_locations,
 99            selected_match_index: 0,
100            matches: Default::default(),
101            render_paths,
102        }
103    }
104}
105
106impl PickerDelegate for RecentProjectsDelegate {
107    fn placeholder_text(&self) -> Arc<str> {
108        "Recent Projects...".into()
109    }
110
111    fn match_count(&self) -> usize {
112        self.matches.len()
113    }
114
115    fn selected_index(&self) -> usize {
116        self.selected_match_index
117    }
118
119    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
120        self.selected_match_index = ix;
121    }
122
123    fn update_matches(
124        &mut self,
125        query: String,
126        cx: &mut ViewContext<RecentProjects>,
127    ) -> gpui::Task<()> {
128        let query = query.trim_start();
129        let smart_case = query.chars().any(|c| c.is_uppercase());
130        let candidates = self
131            .workspace_locations
132            .iter()
133            .enumerate()
134            .map(|(id, location)| {
135                let combined_string = location
136                    .paths()
137                    .iter()
138                    .map(|path| path.compact().to_string_lossy().into_owned())
139                    .collect::<Vec<_>>()
140                    .join("");
141                StringMatchCandidate::new(id, combined_string)
142            })
143            .collect::<Vec<_>>();
144        self.matches = smol::block_on(fuzzy::match_strings(
145            candidates.as_slice(),
146            query,
147            smart_case,
148            100,
149            &Default::default(),
150            cx.background().clone(),
151        ));
152        self.matches.sort_unstable_by_key(|m| m.candidate_id);
153
154        self.selected_match_index = self
155            .matches
156            .iter()
157            .enumerate()
158            .rev()
159            .max_by_key(|(_, m)| OrderedFloat(m.score))
160            .map(|(ix, _)| ix)
161            .unwrap_or(0);
162        Task::ready(())
163    }
164
165    fn confirm(&mut self, _: bool, cx: &mut ViewContext<RecentProjects>) {
166        if let Some((selected_match, workspace)) = self
167            .matches
168            .get(self.selected_index())
169            .zip(self.workspace.upgrade(cx))
170        {
171            let workspace_location = &self.workspace_locations[selected_match.candidate_id];
172            workspace
173                .update(cx, |workspace, cx| {
174                    workspace
175                        .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
176                })
177                .detach_and_log_err(cx);
178            cx.emit(PickerEvent::Dismiss);
179        }
180    }
181
182    fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
183
184    fn render_match(
185        &self,
186        ix: usize,
187        mouse_state: &mut gpui::MouseState,
188        selected: bool,
189        cx: &gpui::AppContext,
190    ) -> AnyElement<Picker<Self>> {
191        let theme = theme::current(cx);
192        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
193
194        let string_match = &self.matches[ix];
195
196        let highlighted_location = HighlightedWorkspaceLocation::new(
197            &string_match,
198            &self.workspace_locations[string_match.candidate_id],
199        );
200
201        Flex::column()
202            .with_child(highlighted_location.names.render(style.label.clone()))
203            .with_children(
204                highlighted_location
205                    .paths
206                    .into_iter()
207                    .filter(|_| self.render_paths)
208                    .map(|highlighted_path| highlighted_path.render(style.label.clone())),
209            )
210            .flex(1., false)
211            .contained()
212            .with_style(style.container)
213            .into_any_named("match")
214    }
215}