recent_projects.rs

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