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