command_palette.rs

  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}