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