1mod highlighted_workspace_location;
2
3use fuzzy::{StringMatch, StringMatchCandidate};
4use gpui::{
5 actions,
6 elements::{ChildView, Flex, ParentElement},
7 AnyViewHandle, AppContext, Element, ElementBox, Entity, RenderContext, Task, View, ViewContext,
8 ViewHandle,
9};
10use highlighted_workspace_location::HighlightedWorkspaceLocation;
11use ordered_float::OrderedFloat;
12use picker::{Picker, PickerDelegate};
13use settings::Settings;
14use workspace::{
15 notifications::simple_message_notification::MessageNotification, OpenPaths, Workspace,
16 WorkspaceLocation, WORKSPACE_DB,
17};
18
19actions!(projects, [OpenRecent]);
20
21pub fn init(cx: &mut AppContext) {
22 cx.add_action(RecentProjectsView::toggle);
23 Picker::<RecentProjectsView>::init(cx);
24}
25
26struct RecentProjectsView {
27 picker: ViewHandle<Picker<Self>>,
28 workspace_locations: Vec<WorkspaceLocation>,
29 selected_match_index: usize,
30 matches: Vec<StringMatch>,
31}
32
33impl RecentProjectsView {
34 fn new(workspace_locations: Vec<WorkspaceLocation>, cx: &mut ViewContext<Self>) -> Self {
35 let handle = cx.weak_handle();
36 Self {
37 picker: cx.add_view(|cx| {
38 Picker::new("Recent Projects...", handle, cx).with_max_size(800., 1200.)
39 }),
40 workspace_locations,
41 selected_match_index: 0,
42 matches: Default::default(),
43 }
44 }
45
46 fn toggle(_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>) {
47 cx.spawn(|workspace, mut cx| async move {
48 let workspace_locations: Vec<_> = cx
49 .background()
50 .spawn(async {
51 WORKSPACE_DB
52 .recent_workspaces_on_disk()
53 .await
54 .unwrap_or_default()
55 .into_iter()
56 .map(|(_, location)| location)
57 .collect()
58 })
59 .await;
60
61 workspace.update(&mut cx, |workspace, cx| {
62 if !workspace_locations.is_empty() {
63 workspace.toggle_modal(cx, |_, cx| {
64 let view = cx.add_view(|cx| Self::new(workspace_locations, cx));
65 cx.subscribe(&view, Self::on_event).detach();
66 view
67 });
68 } else {
69 workspace.show_notification(0, cx, |cx| {
70 cx.add_view(|_| {
71 MessageNotification::new_message("No recent projects to open.")
72 })
73 })
74 }
75 });
76 })
77 .detach();
78 }
79
80 fn on_event(
81 workspace: &mut Workspace,
82 _: ViewHandle<Self>,
83 event: &Event,
84 cx: &mut ViewContext<Workspace>,
85 ) {
86 match event {
87 Event::Dismissed => workspace.dismiss_modal(cx),
88 }
89 }
90}
91
92pub enum Event {
93 Dismissed,
94}
95
96impl Entity for RecentProjectsView {
97 type Event = Event;
98}
99
100impl View for RecentProjectsView {
101 fn ui_name() -> &'static str {
102 "RecentProjectsView"
103 }
104
105 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
106 ChildView::new(&self.picker, cx).boxed()
107 }
108
109 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
110 if cx.is_self_focused() {
111 cx.focus(&self.picker);
112 }
113 }
114}
115
116impl PickerDelegate for RecentProjectsView {
117 fn match_count(&self) -> usize {
118 self.matches.len()
119 }
120
121 fn selected_index(&self) -> usize {
122 self.selected_match_index
123 }
124
125 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Self>) {
126 self.selected_match_index = ix;
127 }
128
129 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
130 let query = query.trim_start();
131 let smart_case = query.chars().any(|c| c.is_uppercase());
132 let candidates = self
133 .workspace_locations
134 .iter()
135 .enumerate()
136 .map(|(id, location)| {
137 let combined_string = location
138 .paths()
139 .iter()
140 .map(|path| path.to_string_lossy().to_owned())
141 .collect::<Vec<_>>()
142 .join("");
143 StringMatchCandidate::new(id, combined_string)
144 })
145 .collect::<Vec<_>>();
146 self.matches = smol::block_on(fuzzy::match_strings(
147 candidates.as_slice(),
148 query,
149 smart_case,
150 100,
151 &Default::default(),
152 cx.background().clone(),
153 ));
154 self.matches.sort_unstable_by_key(|m| m.candidate_id);
155
156 self.selected_match_index = self
157 .matches
158 .iter()
159 .enumerate()
160 .rev()
161 .max_by_key(|(_, m)| OrderedFloat(m.score))
162 .map(|(ix, _)| ix)
163 .unwrap_or(0);
164 Task::ready(())
165 }
166
167 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
168 if let Some(selected_match) = &self.matches.get(self.selected_index()) {
169 let workspace_location = &self.workspace_locations[selected_match.candidate_id];
170 cx.dispatch_global_action(OpenPaths {
171 paths: workspace_location.paths().as_ref().clone(),
172 });
173 cx.emit(Event::Dismissed);
174 }
175 }
176
177 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
178 cx.emit(Event::Dismissed);
179 }
180
181 fn render_match(
182 &self,
183 ix: usize,
184 mouse_state: &mut gpui::MouseState,
185 selected: bool,
186 cx: &gpui::AppContext,
187 ) -> ElementBox {
188 let settings = cx.global::<Settings>();
189 let string_match = &self.matches[ix];
190 let style = settings.theme.picker.item.style_for(mouse_state, selected);
191
192 let highlighted_location = HighlightedWorkspaceLocation::new(
193 &string_match,
194 &self.workspace_locations[string_match.candidate_id],
195 );
196
197 Flex::column()
198 .with_child(highlighted_location.names.render(style.label.clone()))
199 .with_children(
200 highlighted_location
201 .paths
202 .into_iter()
203 .map(|highlighted_path| highlighted_path.render(style.label.clone())),
204 )
205 .flex(1., false)
206 .contained()
207 .with_style(style.container)
208 .named("match")
209 }
210}