1mod highlighted_workspace_location;
2mod projects;
3
4use fuzzy::{StringMatch, StringMatchCandidate};
5use gpui::{
6 AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Subscription,
7 Task, View, ViewContext, WeakView,
8};
9use highlighted_workspace_location::HighlightedWorkspaceLocation;
10use ordered_float::OrderedFloat;
11use picker::{Picker, PickerDelegate};
12use std::sync::Arc;
13use ui::{prelude::*, ListItem, ListItemSpacing};
14use util::paths::PathExt;
15use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB};
16
17pub use projects::OpenRecent;
18
19pub fn init(cx: &mut AppContext) {
20 cx.observe_new_views(RecentProjects::register).detach();
21}
22
23pub struct RecentProjects {
24 pub picker: View<Picker<RecentProjectsDelegate>>,
25 rem_width: f32,
26 _subscription: Subscription,
27}
28
29impl ModalView for RecentProjects {}
30
31impl RecentProjects {
32 fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
33 let picker = cx.build_view(|cx| Picker::new(delegate, cx));
34 let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
35 // We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap
36 // out workspace locations once the future runs to completion.
37 cx.spawn(|this, mut cx| async move {
38 let workspaces = WORKSPACE_DB
39 .recent_workspaces_on_disk()
40 .await
41 .unwrap_or_default()
42 .into_iter()
43 .map(|(_, location)| location)
44 .collect();
45 this.update(&mut cx, move |this, cx| {
46 this.picker.update(cx, move |picker, cx| {
47 picker.delegate.workspace_locations = workspaces;
48 picker.update_matches(picker.query(cx), cx)
49 })
50 })
51 .ok()
52 })
53 .detach();
54 Self {
55 picker,
56 rem_width,
57 _subscription,
58 }
59 }
60
61 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
62 workspace.register_action(|workspace, _: &OpenRecent, cx| {
63 let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
64 if let Some(handler) = Self::open(workspace, cx) {
65 handler.detach_and_log_err(cx);
66 }
67 return;
68 };
69
70 recent_projects.update(cx, |recent_projects, cx| {
71 recent_projects
72 .picker
73 .update(cx, |picker, cx| picker.cycle_selection(cx))
74 });
75 });
76 }
77
78 fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
79 Some(cx.spawn(|workspace, mut cx| async move {
80 workspace.update(&mut cx, |workspace, cx| {
81 let weak_workspace = cx.view().downgrade();
82 workspace.toggle_modal(cx, |cx| {
83 let delegate = RecentProjectsDelegate::new(weak_workspace, true);
84
85 let modal = RecentProjects::new(delegate, 34., cx);
86 modal
87 });
88 })?;
89 Ok(())
90 }))
91 }
92 pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
93 cx.build_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
94 }
95}
96
97impl EventEmitter<DismissEvent> for RecentProjects {}
98
99impl FocusableView for RecentProjects {
100 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
101 self.picker.focus_handle(cx)
102 }
103}
104
105impl Render for RecentProjects {
106 type Element = Div;
107
108 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
109 v_stack()
110 .w(rems(self.rem_width))
111 .child(self.picker.clone())
112 .on_mouse_down_out(cx.listener(|this, _, cx| {
113 this.picker.update(cx, |this, cx| {
114 this.cancel(&Default::default(), cx);
115 })
116 }))
117 }
118}
119
120pub struct RecentProjectsDelegate {
121 workspace: WeakView<Workspace>,
122 workspace_locations: Vec<WorkspaceLocation>,
123 selected_match_index: usize,
124 matches: Vec<StringMatch>,
125 render_paths: bool,
126}
127
128impl RecentProjectsDelegate {
129 fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
130 Self {
131 workspace,
132 workspace_locations: vec![],
133 selected_match_index: 0,
134 matches: Default::default(),
135 render_paths,
136 }
137 }
138}
139impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
140impl PickerDelegate for RecentProjectsDelegate {
141 type ListItem = ListItem;
142
143 fn placeholder_text(&self) -> Arc<str> {
144 "Recent Projects...".into()
145 }
146
147 fn match_count(&self) -> usize {
148 self.matches.len()
149 }
150
151 fn selected_index(&self) -> usize {
152 self.selected_match_index
153 }
154
155 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
156 self.selected_match_index = ix;
157 }
158
159 fn update_matches(
160 &mut self,
161 query: String,
162 cx: &mut ViewContext<Picker<Self>>,
163 ) -> gpui::Task<()> {
164 let query = query.trim_start();
165 let smart_case = query.chars().any(|c| c.is_uppercase());
166 let candidates = self
167 .workspace_locations
168 .iter()
169 .enumerate()
170 .map(|(id, location)| {
171 let combined_string = location
172 .paths()
173 .iter()
174 .map(|path| path.compact().to_string_lossy().into_owned())
175 .collect::<Vec<_>>()
176 .join("");
177 StringMatchCandidate::new(id, combined_string)
178 })
179 .collect::<Vec<_>>();
180 self.matches = smol::block_on(fuzzy::match_strings(
181 candidates.as_slice(),
182 query,
183 smart_case,
184 100,
185 &Default::default(),
186 cx.background_executor().clone(),
187 ));
188 self.matches.sort_unstable_by_key(|m| m.candidate_id);
189
190 self.selected_match_index = self
191 .matches
192 .iter()
193 .enumerate()
194 .rev()
195 .max_by_key(|(_, m)| OrderedFloat(m.score))
196 .map(|(ix, _)| ix)
197 .unwrap_or(0);
198 Task::ready(())
199 }
200
201 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
202 if let Some((selected_match, workspace)) = self
203 .matches
204 .get(self.selected_index())
205 .zip(self.workspace.upgrade())
206 {
207 let workspace_location = &self.workspace_locations[selected_match.candidate_id];
208 workspace
209 .update(cx, |workspace, cx| {
210 workspace
211 .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
212 })
213 .detach_and_log_err(cx);
214 cx.emit(DismissEvent);
215 }
216 }
217
218 fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
219
220 fn render_match(
221 &self,
222 ix: usize,
223 selected: bool,
224 _cx: &mut ViewContext<Picker<Self>>,
225 ) -> Option<Self::ListItem> {
226 let Some(r#match) = self.matches.get(ix) else {
227 return None;
228 };
229
230 let highlighted_location = HighlightedWorkspaceLocation::new(
231 &r#match,
232 &self.workspace_locations[r#match.candidate_id],
233 );
234
235 Some(
236 ListItem::new(ix)
237 .inset(true)
238 .spacing(ListItemSpacing::Sparse)
239 .selected(selected)
240 .child(
241 v_stack()
242 .child(highlighted_location.names)
243 .when(self.render_paths, |this| {
244 this.children(highlighted_location.paths)
245 }),
246 ),
247 )
248 }
249}