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