1mod highlighted_workspace_location;
2
3use fuzzy::{StringMatch, StringMatchCandidate};
4use gpui::{
5 actions,
6 anyhow::Result,
7 elements::{Flex, ParentElement},
8 AnyElement, AppContext, Element, Task, ViewContext, WeakViewHandle,
9};
10use highlighted_workspace_location::HighlightedWorkspaceLocation;
11use ordered_float::OrderedFloat;
12use picker::{Picker, PickerDelegate, PickerEvent};
13use std::sync::Arc;
14use workspace::{
15 notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
16 WORKSPACE_DB,
17};
18
19actions!(projects, [OpenRecent]);
20
21pub fn init(cx: &mut AppContext) {
22 cx.add_async_action(toggle);
23 RecentProjects::init(cx);
24}
25
26fn toggle(
27 _: &mut Workspace,
28 _: &OpenRecent,
29 cx: &mut ViewContext<Workspace>,
30) -> Option<Task<Result<()>>> {
31 Some(cx.spawn(|workspace, mut cx| async move {
32 let workspace_locations: Vec<_> = cx
33 .background()
34 .spawn(async {
35 WORKSPACE_DB
36 .recent_workspaces_on_disk()
37 .await
38 .unwrap_or_default()
39 .into_iter()
40 .map(|(_, location)| location)
41 .collect()
42 })
43 .await;
44
45 workspace.update(&mut cx, |workspace, cx| {
46 if !workspace_locations.is_empty() {
47 workspace.toggle_modal(cx, |_, cx| {
48 let workspace = cx.weak_handle();
49 cx.add_view(|cx| {
50 RecentProjects::new(
51 RecentProjectsDelegate::new(workspace, workspace_locations, true),
52 cx,
53 )
54 .with_max_size(800., 1200.)
55 })
56 });
57 } else {
58 workspace.show_notification(0, cx, |cx| {
59 cx.add_view(|_| MessageNotification::new("No recent projects to open."))
60 })
61 }
62 })?;
63 Ok(())
64 }))
65}
66
67pub fn build_recent_projects(
68 workspace: WeakViewHandle<Workspace>,
69 workspaces: Vec<WorkspaceLocation>,
70 cx: &mut ViewContext<RecentProjects>,
71) -> RecentProjects {
72 Picker::new(
73 RecentProjectsDelegate::new(workspace, workspaces, false),
74 cx,
75 )
76 .with_theme(|theme| theme.picker.clone())
77}
78
79pub type RecentProjects = Picker<RecentProjectsDelegate>;
80
81pub struct RecentProjectsDelegate {
82 workspace: WeakViewHandle<Workspace>,
83 workspace_locations: Vec<WorkspaceLocation>,
84 selected_match_index: usize,
85 matches: Vec<StringMatch>,
86 render_paths: bool,
87}
88
89impl RecentProjectsDelegate {
90 fn new(
91 workspace: WeakViewHandle<Workspace>,
92 workspace_locations: Vec<WorkspaceLocation>,
93 render_paths: bool,
94 ) -> Self {
95 Self {
96 workspace,
97 workspace_locations,
98 selected_match_index: 0,
99 matches: Default::default(),
100 render_paths,
101 }
102 }
103}
104
105impl PickerDelegate for RecentProjectsDelegate {
106 fn placeholder_text(&self) -> Arc<str> {
107 "Recent Projects...".into()
108 }
109
110 fn match_count(&self) -> usize {
111 self.matches.len()
112 }
113
114 fn selected_index(&self) -> usize {
115 self.selected_match_index
116 }
117
118 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
119 self.selected_match_index = ix;
120 }
121
122 fn update_matches(
123 &mut self,
124 query: String,
125 cx: &mut ViewContext<RecentProjects>,
126 ) -> gpui::Task<()> {
127 let query = query.trim_start();
128 let smart_case = query.chars().any(|c| c.is_uppercase());
129 let candidates = self
130 .workspace_locations
131 .iter()
132 .enumerate()
133 .map(|(id, location)| {
134 let combined_string = location
135 .paths()
136 .iter()
137 .map(|path| {
138 let compact = util::paths::compact(&path);
139 compact.to_string_lossy().into_owned()
140 })
141 .collect::<Vec<_>>()
142 .join("");
143 StringMatchCandidate::new(id, combined_string)
144 })
145 .collect::<Vec<_>>();
146 self.matches = smol::block_on(fuzzy::match_strings(
147 candidates.as_slice(),
148 query,
149 smart_case,
150 100,
151 &Default::default(),
152 cx.background().clone(),
153 ));
154 self.matches.sort_unstable_by_key(|m| m.candidate_id);
155
156 self.selected_match_index = self
157 .matches
158 .iter()
159 .enumerate()
160 .rev()
161 .max_by_key(|(_, m)| OrderedFloat(m.score))
162 .map(|(ix, _)| ix)
163 .unwrap_or(0);
164 Task::ready(())
165 }
166
167 fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
168 if let Some((selected_match, workspace)) = self
169 .matches
170 .get(self.selected_index())
171 .zip(self.workspace.upgrade(cx))
172 {
173 let workspace_location = &self.workspace_locations[selected_match.candidate_id];
174 workspace
175 .update(cx, |workspace, cx| {
176 workspace
177 .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
178 })
179 .detach_and_log_err(cx);
180 cx.emit(PickerEvent::Dismiss);
181 }
182 }
183
184 fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
185
186 fn render_match(
187 &self,
188 ix: usize,
189 mouse_state: &mut gpui::MouseState,
190 selected: bool,
191 cx: &gpui::AppContext,
192 ) -> AnyElement<Picker<Self>> {
193 let theme = theme::current(cx);
194 let style = theme.picker.item.in_state(selected).style_for(mouse_state);
195
196 let string_match = &self.matches[ix];
197
198 let highlighted_location = HighlightedWorkspaceLocation::new(
199 &string_match,
200 &self.workspace_locations[string_match.candidate_id],
201 );
202
203 Flex::column()
204 .with_child(highlighted_location.names.render(style.label.clone()))
205 .with_children(
206 highlighted_location
207 .paths
208 .into_iter()
209 .filter(|_| self.render_paths)
210 .map(|highlighted_path| highlighted_path.render(style.label.clone())),
211 )
212 .flex(1., false)
213 .contained()
214 .with_style(style.container)
215 .into_any_named("match")
216 }
217}