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::{OpenPaths, Workspace, WorkspaceLocation, WORKSPACE_DB};
 15
 16actions!(recent_projects, [Toggle]);
 17
 18pub fn init(cx: &mut MutableAppContext) {
 19    cx.add_action(RecentProjectsView::toggle);
 20    Picker::<RecentProjectsView>::init(cx);
 21}
 22
 23struct RecentProjectsView {
 24    picker: ViewHandle<Picker<Self>>,
 25    workspace_locations: Vec<WorkspaceLocation>,
 26    selected_match_index: usize,
 27    matches: Vec<StringMatch>,
 28}
 29
 30impl RecentProjectsView {
 31    fn new(workspace_locations: Vec<WorkspaceLocation>, cx: &mut ViewContext<Self>) -> Self {
 32        let handle = cx.weak_handle();
 33        Self {
 34            picker: cx.add_view(|cx| {
 35                Picker::new("Recent Projects...", handle, cx).with_max_size(800., 1200.)
 36            }),
 37            workspace_locations,
 38            selected_match_index: 0,
 39            matches: Default::default(),
 40        }
 41    }
 42
 43    fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
 44        cx.spawn(|workspace, mut cx| async move {
 45            let workspace_locations = cx
 46                .background()
 47                .spawn(async {
 48                    WORKSPACE_DB
 49                        .recent_workspaces_on_disk()
 50                        .await
 51                        .unwrap_or_default()
 52                        .into_iter()
 53                        .map(|(_, location)| location)
 54                        .collect()
 55                })
 56                .await;
 57
 58            workspace.update(&mut cx, |workspace, cx| {
 59                workspace.toggle_modal(cx, |_, cx| {
 60                    let view = cx.add_view(|cx| Self::new(workspace_locations, cx));
 61                    cx.subscribe(&view, Self::on_event).detach();
 62                    view
 63                });
 64            })
 65        })
 66        .detach();
 67    }
 68
 69    fn on_event(
 70        workspace: &mut Workspace,
 71        _: ViewHandle<Self>,
 72        event: &Event,
 73        cx: &mut ViewContext<Workspace>,
 74    ) {
 75        match event {
 76            Event::Dismissed => workspace.dismiss_modal(cx),
 77        }
 78    }
 79}
 80
 81pub enum Event {
 82    Dismissed,
 83}
 84
 85impl Entity for RecentProjectsView {
 86    type Event = Event;
 87}
 88
 89impl View for RecentProjectsView {
 90    fn ui_name() -> &'static str {
 91        "RecentProjectsView"
 92    }
 93
 94    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
 95        ChildView::new(self.picker.clone(), cx).boxed()
 96    }
 97
 98    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 99        if cx.is_self_focused() {
100            cx.focus(&self.picker);
101        }
102    }
103}
104
105impl PickerDelegate for RecentProjectsView {
106    fn match_count(&self) -> usize {
107        self.matches.len()
108    }
109
110    fn selected_index(&self) -> usize {
111        self.selected_match_index
112    }
113
114    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Self>) {
115        self.selected_match_index = ix;
116    }
117
118    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
119        let query = query.trim_start();
120        let smart_case = query.chars().any(|c| c.is_uppercase());
121        let candidates = self
122            .workspace_locations
123            .iter()
124            .enumerate()
125            .map(|(id, location)| {
126                let combined_string = location
127                    .paths()
128                    .iter()
129                    .map(|path| path.to_string_lossy().to_owned())
130                    .collect::<Vec<_>>()
131                    .join("");
132                StringMatchCandidate::new(id, combined_string)
133            })
134            .collect::<Vec<_>>();
135        self.matches = smol::block_on(fuzzy::match_strings(
136            candidates.as_slice(),
137            query,
138            smart_case,
139            100,
140            &Default::default(),
141            cx.background().clone(),
142        ));
143        self.matches.sort_unstable_by_key(|m| m.candidate_id);
144
145        self.selected_match_index = self
146            .matches
147            .iter()
148            .enumerate()
149            .rev()
150            .max_by_key(|(_, m)| OrderedFloat(m.score))
151            .map(|(ix, _)| ix)
152            .unwrap_or(0);
153        Task::ready(())
154    }
155
156    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
157        let selected_match = &self.matches[self.selected_index()];
158        let workspace_location = &self.workspace_locations[selected_match.candidate_id];
159        cx.dispatch_global_action(OpenPaths {
160            paths: workspace_location.paths().as_ref().clone(),
161        });
162        cx.emit(Event::Dismissed);
163    }
164
165    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
166        cx.emit(Event::Dismissed);
167    }
168
169    fn render_match(
170        &self,
171        ix: usize,
172        mouse_state: &mut gpui::MouseState,
173        selected: bool,
174        cx: &gpui::AppContext,
175    ) -> ElementBox {
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            .named("match")
197    }
198}