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