recent_projects.rs

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