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 has_multiple_bindings: bool,
39}
40
41impl CommandPalette {
42 pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
43 let this = cx.weak_handle();
44 let actions = cx
45 .available_actions(cx.window_id(), focused_view_id)
46 .map(|(name, action, bindings)| Command {
47 name,
48 action,
49 keystrokes: bindings
50 .last()
51 .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
52 has_multiple_bindings: bindings.len() > 1,
53 })
54 .collect();
55 let selector = cx.add_view(|cx| SelectorModal::new(this, cx));
56 Self {
57 selector,
58 actions,
59 matches: vec![],
60 selected_ix: 0,
61 focused_view_id,
62 }
63 }
64
65 fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
66 let workspace = cx.handle();
67 let window_id = cx.window_id();
68 let focused_view_id = cx.focused_view_id(window_id).unwrap_or(workspace.id());
69
70 cx.as_mut().defer(move |cx| {
71 let this = cx.add_view(window_id, |cx| Self::new(focused_view_id, cx));
72 workspace.update(cx, |workspace, cx| {
73 workspace.toggle_modal(cx, |cx, _| {
74 cx.subscribe(&this, Self::on_event).detach();
75 this
76 });
77 });
78 });
79 }
80
81 fn on_event(
82 workspace: &mut Workspace,
83 _: ViewHandle<Self>,
84 event: &Event,
85 cx: &mut ViewContext<Workspace>,
86 ) {
87 match event {
88 Event::Dismissed => {
89 workspace.dismiss_modal(cx);
90 }
91 }
92 }
93}
94
95impl Entity for CommandPalette {
96 type Event = Event;
97}
98
99impl View for CommandPalette {
100 fn ui_name() -> &'static str {
101 "CommandPalette"
102 }
103
104 fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
105 ChildView::new(self.selector.clone()).boxed()
106 }
107
108 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
109 cx.focus(&self.selector);
110 }
111}
112
113impl SelectorModalDelegate for CommandPalette {
114 fn match_count(&self) -> usize {
115 self.matches.len()
116 }
117
118 fn selected_index(&self) -> usize {
119 self.selected_ix
120 }
121
122 fn set_selected_index(&mut self, ix: usize) {
123 self.selected_ix = ix;
124 }
125
126 fn update_matches(
127 &mut self,
128 query: String,
129 cx: &mut gpui::ViewContext<Self>,
130 ) -> gpui::Task<()> {
131 let candidates = self
132 .actions
133 .iter()
134 .enumerate()
135 .map(|(ix, command)| StringMatchCandidate {
136 id: ix,
137 string: command.name.to_string(),
138 char_bag: command.name.chars().collect(),
139 })
140 .collect::<Vec<_>>();
141 cx.spawn(move |this, mut cx| async move {
142 let matches = fuzzy::match_strings(
143 &candidates,
144 &query,
145 true,
146 10000,
147 &Default::default(),
148 cx.background(),
149 )
150 .await;
151 this.update(&mut cx, |this, _| {
152 this.matches = matches;
153 if this.matches.is_empty() {
154 this.selected_ix = 0;
155 } else {
156 this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1);
157 }
158 });
159 })
160 }
161
162 fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
163 cx.emit(Event::Dismissed);
164 }
165
166 fn confirm(&mut self, cx: &mut ViewContext<Self>) {
167 if !self.matches.is_empty() {
168 let window_id = cx.window_id();
169 let action_ix = self.matches[self.selected_ix].candidate_id;
170 cx.dispatch_action_at(
171 window_id,
172 self.focused_view_id,
173 self.actions[action_ix].action.as_ref(),
174 )
175 }
176 cx.emit(Event::Dismissed);
177 }
178
179 fn render_match(&self, ix: usize, selected: bool, cx: &gpui::AppContext) -> gpui::ElementBox {
180 let mat = &self.matches[ix];
181 let command = &self.actions[mat.candidate_id];
182 let settings = cx.global::<Settings>();
183 let theme = &settings.theme.selector;
184 let style = if selected {
185 &theme.active_item
186 } else {
187 &theme.item
188 };
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(Label::new(label.into(), style.label.clone()).boxed())
205 } else {
206 None
207 }
208 }),
209 )
210 .with_child(Label::new(keystroke.key.clone(), style.label.clone()).boxed())
211 .contained()
212 .with_margin_left(5.0)
213 .flex_float()
214 .boxed()
215 }))
216 .with_children(if command.has_multiple_bindings {
217 Some(Label::new("+".into(), style.label.clone()).boxed())
218 } else {
219 None
220 })
221 .contained()
222 .with_style(style.container)
223 .boxed()
224 }
225}
226
227impl std::fmt::Debug for Command {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("Command")
230 .field("name", &self.name)
231 .field("keystrokes", &self.keystrokes)
232 .finish()
233 }
234}