1use fuzzy::{StringMatch, StringMatchCandidate};
2use gpui::{
3 actions,
4 elements::{ChildView, Flex, Label, ParentElement},
5 keymap::Keystroke,
6 Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle,
7};
8use selector::{SelectorModal, SelectorModalDelegate};
9use settings::Settings;
10use std::cmp;
11use workspace::Workspace;
12
13mod selector;
14
15pub fn init(cx: &mut MutableAppContext) {
16 cx.add_action(CommandPalette::toggle);
17 selector::init::<CommandPalette>(cx);
18}
19
20actions!(command_palette, [Toggle]);
21
22pub struct CommandPalette {
23 selector: ViewHandle<SelectorModal<Self>>,
24 actions: Vec<Command>,
25 matches: Vec<StringMatch>,
26 selected_ix: usize,
27 focused_view_id: usize,
28}
29
30pub enum Event {
31 Dismissed,
32}
33
34struct Command {
35 name: &'static str,
36 action: Box<dyn Action>,
37 keystrokes: Vec<Keystroke>,
38}
39
40impl CommandPalette {
41 pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
42 let this = cx.weak_handle();
43 let actions = cx
44 .available_actions(cx.window_id(), focused_view_id)
45 .map(|(name, action, bindings)| Command {
46 name,
47 action,
48 keystrokes: bindings
49 .last()
50 .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
51 })
52 .collect();
53 let selector = cx.add_view(|cx| SelectorModal::new(this, cx));
54 Self {
55 selector,
56 actions,
57 matches: vec![],
58 selected_ix: 0,
59 focused_view_id,
60 }
61 }
62
63 fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
64 let workspace = cx.handle();
65 let window_id = cx.window_id();
66 let focused_view_id = cx.focused_view_id(window_id).unwrap_or(workspace.id());
67
68 cx.as_mut().defer(move |cx| {
69 let this = cx.add_view(window_id, |cx| Self::new(focused_view_id, cx));
70 workspace.update(cx, |workspace, cx| {
71 workspace.toggle_modal(cx, |cx, _| {
72 cx.subscribe(&this, Self::on_event).detach();
73 this
74 });
75 });
76 });
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 => {
87 workspace.dismiss_modal(cx);
88 }
89 }
90 }
91}
92
93impl Entity for CommandPalette {
94 type Event = Event;
95}
96
97impl View for CommandPalette {
98 fn ui_name() -> &'static str {
99 "CommandPalette"
100 }
101
102 fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
103 ChildView::new(self.selector.clone()).boxed()
104 }
105
106 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
107 cx.focus(&self.selector);
108 }
109}
110
111impl SelectorModalDelegate for CommandPalette {
112 fn match_count(&self) -> usize {
113 self.matches.len()
114 }
115
116 fn selected_index(&self) -> usize {
117 self.selected_ix
118 }
119
120 fn set_selected_index(&mut self, ix: usize) {
121 self.selected_ix = ix;
122 }
123
124 fn update_matches(
125 &mut self,
126 query: String,
127 cx: &mut gpui::ViewContext<Self>,
128 ) -> gpui::Task<()> {
129 let candidates = self
130 .actions
131 .iter()
132 .enumerate()
133 .map(|(ix, command)| StringMatchCandidate {
134 id: ix,
135 string: command.name.to_string(),
136 char_bag: command.name.chars().collect(),
137 })
138 .collect::<Vec<_>>();
139 cx.spawn(move |this, mut cx| async move {
140 let matches = fuzzy::match_strings(
141 &candidates,
142 &query,
143 true,
144 10000,
145 &Default::default(),
146 cx.background(),
147 )
148 .await;
149 this.update(&mut cx, |this, _| {
150 this.matches = matches;
151 if this.matches.is_empty() {
152 this.selected_ix = 0;
153 } else {
154 this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1);
155 }
156 });
157 })
158 }
159
160 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
161 cx.emit(Event::Dismissed);
162 }
163
164 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
165 if !self.matches.is_empty() {
166 let window_id = cx.window_id();
167 let action_ix = self.matches[self.selected_ix].candidate_id;
168 cx.dispatch_action_at(
169 window_id,
170 self.focused_view_id,
171 self.actions[action_ix].action.as_ref(),
172 )
173 }
174 cx.emit(Event::Dismissed);
175 }
176
177 fn render_match(&self, ix: usize, selected: bool, cx: &gpui::AppContext) -> gpui::ElementBox {
178 let mat = &self.matches[ix];
179 let command = &self.actions[mat.candidate_id];
180 let settings = cx.global::<Settings>();
181 let theme = &settings.theme;
182 let style = if selected {
183 &theme.selector.active_item
184 } else {
185 &theme.selector.item
186 };
187 let key_style = &theme.command_palette.key;
188 let keystroke_spacing = theme.command_palette.keystroke_spacing;
189
190 Flex::row()
191 .with_child(Label::new(mat.string.clone(), style.label.clone()).boxed())
192 .with_children(command.keystrokes.iter().map(|keystroke| {
193 Flex::row()
194 .with_children(
195 [
196 (keystroke.ctrl, "^"),
197 (keystroke.alt, "⎇"),
198 (keystroke.cmd, "⌘"),
199 (keystroke.shift, "⇧"),
200 ]
201 .into_iter()
202 .filter_map(|(modifier, label)| {
203 if modifier {
204 Some(
205 Label::new(label.into(), key_style.label.clone())
206 .contained()
207 .with_style(key_style.container)
208 .boxed(),
209 )
210 } else {
211 None
212 }
213 }),
214 )
215 .with_child(
216 Label::new(keystroke.key.clone(), key_style.label.clone())
217 .contained()
218 .with_style(key_style.container)
219 .boxed(),
220 )
221 .contained()
222 .with_margin_left(keystroke_spacing)
223 .flex_float()
224 .boxed()
225 }))
226 .contained()
227 .with_style(style.container)
228 .boxed()
229 }
230}
231
232impl std::fmt::Debug for Command {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 f.debug_struct("Command")
235 .field("name", &self.name)
236 .field("keystrokes", &self.keystrokes)
237 .finish()
238 }
239}