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