recent_projects.rs

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