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