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