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