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