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