Make command dispatching work

Conrad Irwin created

Change summary

Cargo.lock                                     |   1 
crates/command_palette2/src/command_palette.rs | 152 +++++++++++--------
crates/editor2/src/element.rs                  |   4 
crates/gpui2/src/action.rs                     |  26 ++
crates/gpui2/src/interactive.rs                |   1 
crates/gpui2/src/keymap/binding.rs             |  13 -
crates/gpui2/src/keymap/matcher.rs             |   1 
crates/gpui2/src/view.rs                       |   6 
crates/gpui2/src/window.rs                     |  62 ++++++--
crates/picker2/Cargo.toml                      |   1 
crates/picker2/src/picker2.rs                  |  41 +++-
11 files changed, 189 insertions(+), 119 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -6153,6 +6153,7 @@ dependencies = [
  "serde_json",
  "settings2",
  "theme2",
+ "ui2",
  "util",
 ]
 

crates/command_palette2/src/command_palette.rs 🔗

@@ -2,13 +2,14 @@ use anyhow::anyhow;
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Div, Element,
-    EventEmitter, FocusHandle, Keystroke, ParentElement, Render, Styled, View, ViewContext,
-    VisualContext, WeakView,
+    actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Component, Div,
+    Element, EventEmitter, FocusHandle, Keystroke, ParentElement, Render, StatelessInteractive,
+    Styled, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use std::cmp::{self, Reverse};
-use ui::modal;
+use theme::ActiveTheme;
+use ui::{modal, Label};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
@@ -19,29 +20,17 @@ use zed_actions::OpenZedURL;
 actions!(Toggle);
 
 pub fn init(cx: &mut AppContext) {
-    dbg!("init");
     cx.set_global(HitCounts::default());
 
     cx.observe_new_views(
         |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
-            dbg!("new workspace found");
-            workspace
-                .modal_layer()
-                .register_modal(Toggle, |workspace, cx| {
-                    dbg!("hitting cmd-shift-p");
-                    let Some(focus_handle) = cx.focused() else {
-                        return None;
-                    };
-
-                    let available_actions = cx.available_actions();
-                    dbg!(&available_actions);
-
-                    Some(cx.build_view(|cx| {
-                        let delegate =
-                            CommandPaletteDelegate::new(cx.view().downgrade(), focus_handle);
-                        CommandPalette::new(delegate, cx)
-                    }))
-                });
+            workspace.modal_layer().register_modal(Toggle, |_, cx| {
+                let Some(previous_focus_handle) = cx.focused() else {
+                    return None;
+                };
+
+                Some(cx.build_view(|cx| CommandPalette::new(previous_focus_handle, cx)))
+            });
         },
     )
     .detach();
@@ -52,8 +41,35 @@ pub struct CommandPalette {
 }
 
 impl CommandPalette {
-    fn new(delegate: CommandPaletteDelegate, cx: &mut ViewContext<Self>) -> Self {
-        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+    fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext<Self>) -> Self {
+        let filter = cx.try_global::<CommandPaletteFilter>();
+
+        let commands = cx
+            .available_actions()
+            .into_iter()
+            .filter_map(|action| {
+                let name = action.name();
+                let namespace = name.split("::").next().unwrap_or("malformed action name");
+                if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
+                    return None;
+                }
+
+                Some(Command {
+                    name: humanize_action_name(&name),
+                    action,
+                    keystrokes: vec![], // todo!()
+                })
+            })
+            .collect();
+
+        let delegate =
+            CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle, cx);
+
+        let picker = cx.build_view(|cx| {
+            let picker = Picker::new(delegate, cx);
+            picker.focus(cx);
+            picker
+        });
         Self { picker }
     }
 }
@@ -78,19 +94,10 @@ pub struct CommandInterceptResult {
 
 pub struct CommandPaletteDelegate {
     command_palette: WeakView<CommandPalette>,
-    actions: Vec<Command>,
+    commands: Vec<Command>,
     matches: Vec<StringMatch>,
     selected_ix: usize,
-    focus_handle: FocusHandle,
-}
-
-pub enum Event {
-    Dismissed,
-    Confirmed {
-        window: AnyWindowHandle,
-        focused_view_id: usize,
-        action: Box<dyn Action>,
-    },
+    previous_focus_handle: FocusHandle,
 }
 
 struct Command {
@@ -115,10 +122,15 @@ impl Clone for Command {
 struct HitCounts(HashMap<String, usize>);
 
 impl CommandPaletteDelegate {
-    pub fn new(command_palette: WeakView<CommandPalette>, focus_handle: FocusHandle) -> Self {
+    fn new(
+        command_palette: WeakView<CommandPalette>,
+        commands: Vec<Command>,
+        previous_focus_handle: FocusHandle,
+        cx: &ViewContext<CommandPalette>,
+    ) -> Self {
         Self {
             command_palette,
-            actions: Default::default(),
+            commands,
             matches: vec![StringMatch {
                 candidate_id: 0,
                 score: 0.,
@@ -126,7 +138,7 @@ impl CommandPaletteDelegate {
                 string: "Foo my bar".into(),
             }],
             selected_ix: 0,
-            focus_handle,
+            previous_focus_handle,
         }
     }
 }
@@ -151,11 +163,11 @@ impl PickerDelegate for CommandPaletteDelegate {
         query: String,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> gpui::Task<()> {
-        let view_id = &self.focus_handle;
+        let view_id = &self.previous_focus_handle;
         let window = cx.window();
         cx.spawn(move |picker, mut cx| async move {
             let mut actions = picker
-                .update(&mut cx, |this, _| this.delegate.actions.clone())
+                .update(&mut cx, |this, _| this.delegate.commands.clone())
                 .expect("todo: handle picker no longer being around");
             // _ = window
             //     .available_actions(view_id, &cx)
@@ -276,7 +288,7 @@ impl PickerDelegate for CommandPaletteDelegate {
             picker
                 .update(&mut cx, |picker, _| {
                     let delegate = &mut picker.delegate;
-                    delegate.actions = actions;
+                    delegate.commands = actions;
                     delegate.matches = matches;
                     if delegate.matches.is_empty() {
                         delegate.selected_ix = 0;
@@ -290,32 +302,25 @@ impl PickerDelegate for CommandPaletteDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        dbg!("dismissed");
         self.command_palette
-            .update(cx, |command_palette, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
             .log_err();
     }
 
     fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
-        // if !self.matches.is_empty() {
-        //     let window = cx.window();
-        //     let focused_view_id = self.focused_view_id;
-        //     let action_ix = self.matches[self.selected_ix].candidate_id;
-        //     let command = self.actions.remove(action_ix);
-        //     cx.update_default_global(|hit_counts: &mut HitCounts, _| {
-        //         *hit_counts.0.entry(command.name).or_default() += 1;
-        //     });
-        //     let action = command.action;
-
-        //     cx.app_context()
-        //         .spawn(move |mut cx| async move {
-        //             window
-        //                 .dispatch_action(focused_view_id, action.as_ref(), &mut cx)
-        //                 .ok_or_else(|| anyhow!("window was closed"))
-        //         })
-        //         .detach_and_log_err(cx);
-        // }
-        self.dismissed(cx)
+        if self.matches.is_empty() {
+            self.dismissed(cx);
+            return;
+        }
+        let action_ix = self.matches[self.selected_ix].candidate_id;
+        let command = self.commands.swap_remove(action_ix);
+        cx.update_global(|hit_counts: &mut HitCounts, _| {
+            *hit_counts.0.entry(command.name).or_default() += 1;
+        });
+        let action = command.action;
+        cx.focus(&self.previous_focus_handle);
+        cx.dispatch_action(action);
+        self.dismissed(cx);
     }
 
     fn render_match(
@@ -324,7 +329,26 @@ impl PickerDelegate for CommandPaletteDelegate {
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Self::ListItem {
-        div().child("ooh yeah")
+        let colors = cx.theme().colors();
+        let Some(command) = self
+            .matches
+            .get(ix)
+            .and_then(|m| self.commands.get(m.candidate_id))
+        else {
+            return div();
+        };
+
+        div()
+            .text_color(colors.text)
+            .when(selected, |s| {
+                s.border_l_10().border_color(colors.terminal_ansi_yellow)
+            })
+            .hover(|style| {
+                style
+                    .bg(colors.element_active)
+                    .text_color(colors.text_accent)
+            })
+            .child(Label::new(command.name.clone()))
     }
 
     // fn render_match(

crates/editor2/src/element.rs 🔗

@@ -4149,16 +4149,12 @@ fn build_key_listeners(
         build_key_listener(
             move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| {
                 if phase == DispatchPhase::Bubble {
-                    dbg!(&dispatch_context);
                     if let KeyMatch::Some(action) = cx.match_keystroke(
                         &global_element_id,
                         &key_down.keystroke,
                         dispatch_context,
                     ) {
-                        dbg!("got action", &action);
                         return Some(action);
-                    } else {
-                        dbg!("not action");
                     }
                 }
 

crates/gpui2/src/action.rs 🔗

@@ -104,7 +104,17 @@ impl dyn Action {
     pub fn type_id(&self) -> TypeId {
         self.as_any().type_id()
     }
+
+    pub fn name(&self) -> SharedString {
+        ACTION_REGISTRY
+            .read()
+            .names_by_type_id
+            .get(&self.type_id())
+            .expect("type is not a registered action")
+            .clone()
+    }
 }
+
 type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
 
 lazy_static! {
@@ -114,7 +124,7 @@ lazy_static! {
 #[derive(Default)]
 struct ActionRegistry {
     builders_by_name: HashMap<SharedString, ActionBuilder>,
-    builders_by_type_id: HashMap<TypeId, ActionBuilder>,
+    names_by_type_id: HashMap<TypeId, SharedString>,
     all_names: Vec<SharedString>, // So we can return a static slice.
 }
 
@@ -123,20 +133,22 @@ pub fn register_action<A: Action>() {
     let name = A::qualified_name();
     let mut lock = ACTION_REGISTRY.write();
     lock.builders_by_name.insert(name.clone(), A::build);
-    lock.builders_by_type_id.insert(TypeId::of::<A>(), A::build);
+    lock.names_by_type_id
+        .insert(TypeId::of::<A>(), name.clone());
     lock.all_names.push(name);
 }
 
 /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
 pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
     let lock = ACTION_REGISTRY.read();
-
-    let build_action = lock
-        .builders_by_type_id
+    let name = lock
+        .names_by_type_id
         .get(type_id)
-        .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?;
+        .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
+        .clone();
+    drop(lock);
 
-    (build_action)(None)
+    build_action(&name, None)
 }
 
 /// Construct an action based on its name and optional JSON parameters sourced from the keymap.

crates/gpui2/src/interactive.rs 🔗

@@ -414,7 +414,6 @@ pub trait ElementInteractivity<V: 'static>: 'static {
                     Box::new(move |_, key_down, context, phase, cx| {
                         if phase == DispatchPhase::Bubble {
                             let key_down = key_down.downcast_ref::<KeyDownEvent>().unwrap();
-                            dbg!(&context);
                             if let KeyMatch::Some(action) =
                                 cx.match_keystroke(&global_id, &key_down.keystroke, context)
                             {

crates/gpui2/src/keymap/binding.rs 🔗

@@ -44,19 +44,6 @@ impl KeyBinding {
         pending_keystrokes: &[Keystroke],
         contexts: &[&DispatchContext],
     ) -> KeyMatch {
-        let should_debug = self.keystrokes.len() == 1
-            && self.keystrokes[0].key == "p"
-            && self.keystrokes[0].modifiers.command == true
-            && self.keystrokes[0].modifiers.shift == true;
-
-        if false && should_debug {
-            dbg!(
-                &self.keystrokes,
-                &pending_keystrokes,
-                &contexts,
-                &self.matches_context(contexts)
-            );
-        }
         if self.keystrokes.as_ref().starts_with(&pending_keystrokes)
             && self.matches_context(contexts)
         {

crates/gpui2/src/keymap/matcher.rs 🔗

@@ -46,7 +46,6 @@ impl KeyMatcher {
         keystroke: &Keystroke,
         context_stack: &[&DispatchContext],
     ) -> KeyMatch {
-        dbg!(keystroke, &context_stack);
         let keymap = self.keymap.lock();
         // Clear pending keystrokes if the keymap has changed since the last matched keystroke.
         if keymap.version() != self.keymap_version {

crates/gpui2/src/view.rs 🔗

@@ -145,7 +145,7 @@ impl<V> Eq for WeakView<V> {}
 #[derive(Clone, Debug)]
 pub struct AnyView {
     model: AnyModel,
-    initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
+    pub initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
     layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
     paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
 }
@@ -184,6 +184,10 @@ impl AnyView {
             .compute_layout(layout_id, available_space);
         (self.paint)(self, &mut rendered_element, cx);
     }
+
+    pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) {
+        (self.initialize)(self, cx);
+    }
 }
 
 impl<V: 'static> Component<V> for AnyView {

crates/gpui2/src/window.rs 🔗

@@ -228,7 +228,7 @@ pub(crate) struct Frame {
     key_matchers: HashMap<GlobalElementId, KeyMatcher>,
     mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyListener)>>,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
-    key_dispatch_stack: Vec<KeyDispatchStackFrame>,
+    pub(crate) key_dispatch_stack: Vec<KeyDispatchStackFrame>,
     freeze_key_dispatch_stack: bool,
     focus_parents_by_child: HashMap<FocusId, FocusId>,
     pub(crate) scene_builder: SceneBuilder,
@@ -327,7 +327,7 @@ impl Window {
 /// find the focused element. We interleave key listeners with dispatch contexts so we can use the
 /// contexts when matching key events against the keymap. A key listener can be either an action
 /// handler or a [KeyDown] / [KeyUp] event listener.
-enum KeyDispatchStackFrame {
+pub(crate) enum KeyDispatchStackFrame {
     Listener {
         event_type: TypeId,
         listener: AnyKeyListener,
@@ -407,6 +407,9 @@ impl<'a> WindowContext<'a> {
         }
 
         self.window.focus = Some(handle.id);
+
+        // self.window.current_frame.key_dispatch_stack.clear()
+        // self.window.root_view.initialize()
         self.app.push_effect(Effect::FocusChanged {
             window_handle: self.window.handle,
             focused: Some(handle.id),
@@ -428,6 +431,14 @@ impl<'a> WindowContext<'a> {
         self.notify();
     }
 
+    pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
+        self.defer(|cx| {
+            cx.app.propagate_event = true;
+            let stack = cx.dispatch_stack();
+            cx.dispatch_action_internal(action, &stack[..])
+        })
+    }
+
     /// Schedules the given function to be run at the end of the current effect cycle, allowing entities
     /// that are currently on the stack to be returned to the app.
     pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) {
@@ -1055,6 +1066,26 @@ impl<'a> WindowContext<'a> {
         self.window.dirty = false;
     }
 
+    pub(crate) fn dispatch_stack(&mut self) -> Vec<KeyDispatchStackFrame> {
+        let root_view = self.window.root_view.take().unwrap();
+        let window = &mut *self.window;
+        let mut spare_frame = Frame::default();
+        mem::swap(&mut spare_frame, &mut window.previous_frame);
+
+        self.start_frame();
+
+        root_view.draw_dispatch_stack(self);
+
+        let window = &mut *self.window;
+        // restore the old values of current and previous frame,
+        // putting the new frame into spare_frame.
+        mem::swap(&mut window.current_frame, &mut window.previous_frame);
+        mem::swap(&mut spare_frame, &mut window.previous_frame);
+        self.window.root_view = Some(root_view);
+
+        spare_frame.key_dispatch_stack
+    }
+
     /// Rotate the current frame and the previous frame, then clear the current frame.
     /// We repopulate all state in the current frame during each paint.
     fn start_frame(&mut self) {
@@ -1197,7 +1228,7 @@ impl<'a> WindowContext<'a> {
                                 DispatchPhase::Capture,
                                 self,
                             ) {
-                                self.dispatch_action(action, &key_dispatch_stack[..ix]);
+                                self.dispatch_action_internal(action, &key_dispatch_stack[..ix]);
                             }
                             if !self.app.propagate_event {
                                 break;
@@ -1224,7 +1255,10 @@ impl<'a> WindowContext<'a> {
                                     DispatchPhase::Bubble,
                                     self,
                                 ) {
-                                    self.dispatch_action(action, &key_dispatch_stack[..ix]);
+                                    self.dispatch_action_internal(
+                                        action,
+                                        &key_dispatch_stack[..ix],
+                                    );
                                 }
 
                                 if !self.app.propagate_event {
@@ -1296,11 +1330,9 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.prompt(level, msg, answers)
     }
 
-    pub fn available_actions(&mut self) -> Vec<Box<dyn Action>> {
-        let key_dispatch_stack = &self.window.current_frame.key_dispatch_stack;
-        let mut actions = Vec::new();
-        dbg!(key_dispatch_stack.len());
-        for frame in key_dispatch_stack {
+    pub fn available_actions(&self) -> impl Iterator<Item = Box<dyn Action>> + '_ {
+        let key_dispatch_stack = &self.window.previous_frame.key_dispatch_stack;
+        key_dispatch_stack.iter().filter_map(|frame| {
             match frame {
                 // todo!factor out a KeyDispatchStackFrame::Action
                 KeyDispatchStackFrame::Listener {
@@ -1308,21 +1340,19 @@ impl<'a> WindowContext<'a> {
                     listener: _,
                 } => {
                     match build_action_from_type(event_type) {
-                        Ok(action) => {
-                            actions.push(action);
-                        }
+                        Ok(action) => Some(action),
                         Err(err) => {
                             dbg!(err);
+                            None
                         } // we'll hit his if TypeId == KeyDown
                     }
                 }
-                KeyDispatchStackFrame::Context(_) => {}
+                KeyDispatchStackFrame::Context(_) => None,
             }
-        }
-        actions
+        })
     }
 
-    fn dispatch_action(
+    pub(crate) fn dispatch_action_internal(
         &mut self,
         action: Box<dyn Action>,
         dispatch_stack: &[KeyDispatchStackFrame],

crates/picker2/Cargo.toml 🔗

@@ -10,6 +10,7 @@ doctest = false
 
 [dependencies]
 editor = { package = "editor2", path = "../editor2" }
+ui = { package = "ui2", path = "../ui2" }
 gpui = { package = "gpui2", path = "../gpui2" }
 menu = { package = "menu2", path = "../menu2" }
 settings = { package = "settings2", path = "../settings2" }

crates/picker2/src/picker2.rs 🔗

@@ -5,6 +5,8 @@ use gpui::{
     WindowContext,
 };
 use std::cmp;
+use theme::ActiveTheme;
+use ui::v_stack;
 
 pub struct Picker<D: PickerDelegate> {
     pub delegate: D,
@@ -133,7 +135,7 @@ impl<D: PickerDelegate> Picker<D> {
 impl<D: PickerDelegate> Render for Picker<D> {
     type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>;
 
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         div()
             .context("picker")
             .id("picker-container")
@@ -146,18 +148,33 @@ impl<D: PickerDelegate> Render for Picker<D> {
             .on_action(Self::cancel)
             .on_action(Self::confirm)
             .on_action(Self::secondary_confirm)
-            .child(self.editor.clone())
             .child(
-                uniform_list("candidates", self.delegate.match_count(), {
-                    move |this: &mut Self, visible_range, cx| {
-                        let selected_ix = this.delegate.selected_index();
-                        visible_range
-                            .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx))
-                            .collect()
-                    }
-                })
-                .track_scroll(self.scroll_handle.clone())
-                .size_full(),
+                v_stack().gap_px().child(
+                    v_stack()
+                        .py_0p5()
+                        .px_1()
+                        .child(div().px_2().py_0p5().child(self.editor.clone())),
+                ),
+            )
+            .child(
+                div()
+                    .h_px()
+                    .w_full()
+                    .bg(cx.theme().colors().element_background),
+            )
+            .child(
+                v_stack().py_0p5().px_1().grow().max_h_96().child(
+                    uniform_list("candidates", self.delegate.match_count(), {
+                        move |this: &mut Self, visible_range, cx| {
+                            let selected_ix = this.delegate.selected_index();
+                            visible_range
+                                .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx))
+                                .collect()
+                        }
+                    })
+                    .track_scroll(self.scroll_handle.clone())
+                    .size_full(),
+                ),
             )
     }
 }