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;
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),
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
68type RecentProjects = Picker<RecentProjectsDelegate>;
69
70struct RecentProjectsDelegate {
71 workspace: WeakViewHandle<Workspace>,
72 workspace_locations: Vec<WorkspaceLocation>,
73 selected_match_index: usize,
74 matches: Vec<StringMatch>,
75}
76
77impl RecentProjectsDelegate {
78 fn new(
79 workspace: WeakViewHandle<Workspace>,
80 workspace_locations: Vec<WorkspaceLocation>,
81 ) -> Self {
82 Self {
83 workspace,
84 workspace_locations,
85 selected_match_index: 0,
86 matches: Default::default(),
87 }
88 }
89}
90
91impl PickerDelegate for RecentProjectsDelegate {
92 fn placeholder_text(&self) -> Arc<str> {
93 "Recent Projects...".into()
94 }
95
96 fn match_count(&self) -> usize {
97 self.matches.len()
98 }
99
100 fn selected_index(&self) -> usize {
101 self.selected_match_index
102 }
103
104 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
105 self.selected_match_index = ix;
106 }
107
108 fn update_matches(
109 &mut self,
110 query: String,
111 cx: &mut ViewContext<RecentProjects>,
112 ) -> gpui::Task<()> {
113 let query = query.trim_start();
114 let smart_case = query.chars().any(|c| c.is_uppercase());
115 let candidates = self
116 .workspace_locations
117 .iter()
118 .enumerate()
119 .map(|(id, location)| {
120 let combined_string = location
121 .paths()
122 .iter()
123 .map(|path| path.to_string_lossy().to_owned())
124 .collect::<Vec<_>>()
125 .join("");
126 StringMatchCandidate::new(id, combined_string)
127 })
128 .collect::<Vec<_>>();
129 self.matches = smol::block_on(fuzzy::match_strings(
130 candidates.as_slice(),
131 query,
132 smart_case,
133 100,
134 &Default::default(),
135 cx.background().clone(),
136 ));
137 self.matches.sort_unstable_by_key(|m| m.candidate_id);
138
139 self.selected_match_index = self
140 .matches
141 .iter()
142 .enumerate()
143 .rev()
144 .max_by_key(|(_, m)| OrderedFloat(m.score))
145 .map(|(ix, _)| ix)
146 .unwrap_or(0);
147 Task::ready(())
148 }
149
150 fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
151 if let Some((selected_match, workspace)) = self
152 .matches
153 .get(self.selected_index())
154 .zip(self.workspace.upgrade(cx))
155 {
156 let workspace_location = &self.workspace_locations[selected_match.candidate_id];
157 workspace
158 .update(cx, |workspace, cx| {
159 workspace
160 .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
161 })
162 .detach_and_log_err(cx);
163 cx.emit(PickerEvent::Dismiss);
164 }
165 }
166
167 fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
168
169 fn render_match(
170 &self,
171 ix: usize,
172 mouse_state: &mut gpui::MouseState,
173 selected: bool,
174 cx: &gpui::AppContext,
175 ) -> AnyElement<Picker<Self>> {
176 let settings = cx.global::<Settings>();
177 let string_match = &self.matches[ix];
178 let style = settings.theme.picker.item.style_for(mouse_state, selected);
179
180 let highlighted_location = HighlightedWorkspaceLocation::new(
181 &string_match,
182 &self.workspace_locations[string_match.candidate_id],
183 );
184
185 Flex::column()
186 .with_child(highlighted_location.names.render(style.label.clone()))
187 .with_children(
188 highlighted_location
189 .paths
190 .into_iter()
191 .map(|highlighted_path| highlighted_path.render(style.label.clone())),
192 )
193 .flex(1., false)
194 .contained()
195 .with_style(style.container)
196 .into_any_named("match")
197 }
198}