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