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