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