gpui: Represent no action via `Option`

Lukas Wirth created

Change summary

crates/debugger_ui/src/session/running.rs      |   6 
crates/gpui/src/action.rs                      |  21 ---
crates/gpui/src/key_dispatch.rs                |  13 +
crates/gpui/src/keymap.rs                      | 121 ++++++++++++++-----
crates/gpui/src/keymap/binding.rs              |  34 ++++-
crates/gpui/src/window.rs                      |  17 +-
crates/keymap_editor/src/keymap_editor.rs      |   7 
crates/language_tools/src/key_context_view.rs  |  17 -
crates/settings/src/keymap_file.rs             |  21 +-
crates/terminal_view/src/terminal_panel.rs     |   2 
crates/ui/src/components/keybinding.rs         |  12 
crates/ui/src/components/stories/keybinding.rs |   5 
crates/vim/src/command.rs                      |  16 +-
crates/workspace/src/pane.rs                   |  28 ++-
crates/workspace/src/workspace.rs              |   4 
15 files changed, 194 insertions(+), 130 deletions(-)

Detailed changes

crates/debugger_ui/src/session/running.rs 🔗

@@ -26,8 +26,8 @@ use dap::{
 };
 use futures::{SinkExt, channel::mpsc};
 use gpui::{
-    Action as _, AnyView, AppContext, Axis, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
-    NoAction, Pixels, Point, Subscription, Task, WeakEntity,
+    AnyView, AppContext, Axis, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Pixels,
+    Point, Subscription, Task, WeakEntity,
 };
 use language::Buffer;
 use loaded_source_list::LoadedSourceList;
@@ -385,7 +385,7 @@ pub(crate) fn new_debugger_pane(
             project.clone(),
             Default::default(),
             None,
-            NoAction.boxed_clone(),
+            None,
             window,
             cx,
         );

crates/gpui/src/action.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::{Context as _, Result};
 use collections::HashMap;
 pub use gpui_macros::Action;
-pub use no_action::{NoAction, is_no_action};
+
 use serde_json::json;
 use std::{
     any::{Any, TypeId},
@@ -419,22 +419,3 @@ pub fn generate_list_of_all_registered_actions() -> impl Iterator<Item = MacroAc
         .into_iter()
         .map(|builder| builder.0())
 }
-
-mod no_action {
-    use crate as gpui;
-    use std::any::Any as _;
-
-    actions!(
-        zed,
-        [
-            /// Action with special handling which unbinds the keybinding this is associated with,
-            /// if it is the highest precedence match.
-            NoAction
-        ]
-    );
-
-    /// Returns whether or not this action represents a removed key binding.
-    pub fn is_no_action(action: &dyn gpui::Action) -> bool {
-        action.as_any().type_id() == (NoAction {}).type_id()
-    }
-}

crates/gpui/src/key_dispatch.rs 🔗

@@ -438,7 +438,11 @@ impl DispatchTree {
     ) -> bool {
         let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack);
         if let Some(found) = bindings.iter().next() {
-            found.action.partial_eq(binding.action.as_ref())
+            match (found.action.as_deref(), binding.action.as_deref()) {
+                (None, None) => true,
+                (Some(f), Some(b)) => f.partial_eq(b),
+                (None, Some(_)) | (Some(_), None) => false,
+            }
         } else {
             false
         }
@@ -677,7 +681,12 @@ mod tests {
 
         let keybinding = tree.bindings_for_action(&TestAction, &contexts);
 
-        assert!(keybinding[0].action.partial_eq(&TestAction))
+        assert!(
+            keybinding[0]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&TestAction))
+        );
     }
 
     #[crate::test]

crates/gpui/src/keymap.rs 🔗

@@ -4,7 +4,7 @@ mod context;
 pub use binding::*;
 pub use context::*;
 
-use crate::{Action, AsKeystroke, Keystroke, is_no_action};
+use crate::{Action, AsKeystroke, Keystroke};
 use collections::{HashMap, HashSet};
 use smallvec::SmallVec;
 use std::any::TypeId;
@@ -43,14 +43,14 @@ impl Keymap {
     /// Add more bindings to the keymap.
     pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
         for binding in bindings {
-            let action_id = binding.action().as_any().type_id();
-            if is_no_action(&*binding.action) {
-                self.no_action_binding_indices.push(self.bindings.len());
-            } else {
+            if let Some(action) = binding.action.as_deref() {
+                let action_id = action.type_id();
                 self.binding_indices_by_action_id
                     .entry(action_id)
                     .or_default()
                     .push(self.bindings.len());
+            } else {
+                self.no_action_binding_indices.push(self.bindings.len());
             }
             self.bindings.push(binding);
         }
@@ -86,7 +86,10 @@ impl Keymap {
 
         binding_indices.filter_map(|ix| {
             let binding = &self.bindings[*ix];
-            if !binding.action().partial_eq(action) {
+            if binding
+                .action()
+                .is_none_or(|action| !action.partial_eq(action))
+            {
                 return None;
             }
 
@@ -170,7 +173,7 @@ impl Keymap {
         let mut first_binding_index = None;
 
         for (_, ix, binding) in matched_bindings {
-            if is_no_action(&*binding.action) {
+            if binding.action.is_none() {
                 // Only break if this is a user-defined NoAction binding
                 // This allows user keymaps to override base keymap NoAction bindings
                 if let Some(meta) = binding.meta {
@@ -195,7 +198,7 @@ impl Keymap {
             {
                 continue;
             }
-            if is_no_action(&*binding.action) {
+            if binding.action.is_none() {
                 pending.remove(&&binding.keystrokes);
                 continue;
             }
@@ -204,8 +207,9 @@ impl Keymap {
 
         (bindings, !pending.is_empty())
     }
+
     /// Check if the given binding is enabled, given a certain key context.
-    /// Returns the deepest depth at which the binding matches, or None if it doesn't match.
+    /// Returns the deepest depth at which the binding matches, or [`None`] if it doesn't match.
     fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
         if let Some(predicate) = &binding.context_predicate {
             predicate.depth_of(contexts)
@@ -219,7 +223,6 @@ impl Keymap {
 mod tests {
     use super::*;
     use crate as gpui;
-    use gpui::NoAction;
 
     actions!(
         test_only,
@@ -287,8 +290,18 @@ mod tests {
 
         assert!(!pending);
         assert_eq!(result.len(), 2);
-        assert!(result[0].action.partial_eq(&ActionGamma {}));
-        assert!(result[1].action.partial_eq(&ActionBeta {}));
+        assert!(
+            result[0]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionGamma {}))
+        );
+        assert!(
+            result[1]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionBeta {}))
+        );
     }
 
     #[test]
@@ -296,8 +309,8 @@ mod tests {
         let bindings = [
             KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")),
             KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")),
-            KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")),
-            KeyBinding::new("ctrl-b", NoAction {}, None),
+            KeyBinding::new_no_action("ctrl-a", Some("editor && mode==full")),
+            KeyBinding::new_no_action("ctrl-b", None),
         ];
 
         let mut keymap = Keymap::default();
@@ -351,7 +364,7 @@ mod tests {
     fn test_multiple_keystroke_binding_disabled() {
         let bindings = [
             KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
-            KeyBinding::new("space w w", NoAction {}, Some("editor")),
+            KeyBinding::new_no_action("space w w", Some("editor")),
         ];
 
         let mut keymap = Keymap::default();
@@ -403,7 +416,7 @@ mod tests {
         // that should result in pending
         let bindings = [
             KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
-            KeyBinding::new("space w w", NoAction {}, Some("editor")),
+            KeyBinding::new_no_action("space w w", Some("editor")),
             KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
         ];
         let mut keymap = Keymap::default();
@@ -418,7 +431,7 @@ mod tests {
         let bindings = [
             KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
             KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
-            KeyBinding::new("space w w", NoAction {}, Some("editor")),
+            KeyBinding::new_no_action("space w w", Some("editor")),
         ];
         let mut keymap = Keymap::default();
         keymap.add_bindings(bindings);
@@ -432,7 +445,7 @@ mod tests {
         let bindings = [
             KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
             KeyBinding::new("space w x", ActionAlpha {}, Some("workspace")),
-            KeyBinding::new("space w w", NoAction {}, Some("editor")),
+            KeyBinding::new_no_action("space w w", Some("editor")),
         ];
         let mut keymap = Keymap::default();
         keymap.add_bindings(bindings);
@@ -446,7 +459,7 @@ mod tests {
     fn test_override_multikey() {
         let bindings = [
             KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
-            KeyBinding::new("ctrl-w", NoAction {}, Some("editor")),
+            KeyBinding::new_no_action("ctrl-w", Some("editor")),
         ];
 
         let mut keymap = Keymap::default();
@@ -481,7 +494,7 @@ mod tests {
     fn test_simple_disable() {
         let bindings = [
             KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")),
-            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")),
+            KeyBinding::new_no_action("ctrl-x", Some("editor")),
         ];
 
         let mut keymap = Keymap::default();
@@ -501,7 +514,7 @@ mod tests {
         // disabled at the wrong level
         let bindings = [
             KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")),
-            KeyBinding::new("ctrl-x", NoAction {}, Some("workspace")),
+            KeyBinding::new_no_action("ctrl-x", Some("workspace")),
         ];
 
         let mut keymap = Keymap::default();
@@ -523,7 +536,7 @@ mod tests {
     fn test_disable_deeper() {
         let bindings = [
             KeyBinding::new("ctrl-x", ActionAlpha {}, Some("workspace")),
-            KeyBinding::new("ctrl-x", NoAction {}, Some("editor")),
+            KeyBinding::new_no_action("ctrl-x", Some("editor")),
         ];
 
         let mut keymap = Keymap::default();
@@ -560,7 +573,12 @@ mod tests {
             .map(Result::unwrap),
         );
         assert_eq!(matched.0.len(), 1);
-        assert!(matched.0[0].action.partial_eq(&ActionBeta));
+        assert!(
+            matched.0[0]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionBeta))
+        );
         assert!(matched.1);
     }
 
@@ -568,7 +586,7 @@ mod tests {
     fn test_pending_match_enabled_extended() {
         let bindings = [
             KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
-            KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")),
+            KeyBinding::new_no_action("ctrl-x 0", Some("Workspace")),
         ];
         let mut keymap = Keymap::default();
         keymap.add_bindings(bindings);
@@ -583,11 +601,16 @@ mod tests {
             .map(Result::unwrap),
         );
         assert_eq!(matched.0.len(), 1);
-        assert!(matched.0[0].action.partial_eq(&ActionBeta));
+        assert!(
+            matched.0[0]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionBeta))
+        );
         assert!(!matched.1);
         let bindings = [
             KeyBinding::new("ctrl-x", ActionBeta, Some("Workspace")),
-            KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")),
+            KeyBinding::new_no_action("ctrl-x 0", Some("vim_mode == normal")),
         ];
         let mut keymap = Keymap::default();
         keymap.add_bindings(bindings);
@@ -602,7 +625,12 @@ mod tests {
             .map(Result::unwrap),
         );
         assert_eq!(matched.0.len(), 1);
-        assert!(matched.0[0].action.partial_eq(&ActionBeta));
+        assert!(
+            matched.0[0]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionBeta))
+        );
         assert!(!matched.1);
     }
 
@@ -625,7 +653,12 @@ mod tests {
             .map(Result::unwrap),
         );
         assert_eq!(matched.0.len(), 1);
-        assert!(matched.0[0].action.partial_eq(&ActionBeta));
+        assert!(
+            matched.0[0]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionBeta))
+        );
         assert!(!matched.1);
     }
 
@@ -652,8 +685,18 @@ mod tests {
 
         // Both bindings should be returned, but Editor binding should be first (highest precedence)
         assert_eq!(result.len(), 2);
-        assert!(result[0].action.partial_eq(&ActionBeta {})); // Editor binding first
-        assert!(result[1].action.partial_eq(&ActionAlpha {})); // Workspace binding second
+        assert!(
+            result[0]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionBeta {}))
+        ); // Editor binding first
+        assert!(
+            result[1]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionAlpha {}))
+        ); // Workspace binding second
     }
 
     #[test]
@@ -662,8 +705,8 @@ mod tests {
             KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")),
             KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")),
             KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")),
-            KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")),
-            KeyBinding::new("ctrl-b", NoAction {}, Some("editor")),
+            KeyBinding::new_no_action("ctrl-a", Some("pane && active")),
+            KeyBinding::new_no_action("ctrl-b", Some("editor")),
         ];
 
         let mut keymap = Keymap::default();
@@ -707,7 +750,17 @@ mod tests {
 
         // User binding should take precedence over default binding
         assert_eq!(result.len(), 2);
-        assert!(result[0].action.partial_eq(&ActionBeta {}));
-        assert!(result[1].action.partial_eq(&ActionAlpha {}));
+        assert!(
+            result[0]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionBeta {}))
+        );
+        assert!(
+            result[1]
+                .action
+                .as_deref()
+                .is_some_and(|it| it.partial_eq(&ActionAlpha {}))
+        );
     }
 }

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

@@ -8,7 +8,7 @@ use smallvec::SmallVec;
 
 /// A keybinding and its associated metadata, from the keymap.
 pub struct KeyBinding {
-    pub(crate) action: Box<dyn Action>,
+    pub(crate) action: Option<Box<dyn Action>>,
     pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
     pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
     pub(crate) meta: Option<KeyBindingMetaIndex>,
@@ -19,7 +19,7 @@ pub struct KeyBinding {
 impl Clone for KeyBinding {
     fn clone(&self) -> Self {
         KeyBinding {
-            action: self.action.boxed_clone(),
+            action: self.action.as_ref().map(|action| action.boxed_clone()),
             keystrokes: self.keystrokes.clone(),
             context_predicate: self.context_predicate.clone(),
             meta: self.meta,
@@ -35,7 +35,22 @@ impl KeyBinding {
             context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
         Self::load(
             keystrokes,
-            Box::new(action),
+            Some(Box::new(action) as Box<dyn Action>),
+            context_predicate,
+            false,
+            None,
+            &DummyKeyboardMapper,
+        )
+        .unwrap()
+    }
+
+    /// Construct a new keybinding from the given data with no action associated to it. Panics on parse error.
+    pub fn new_no_action(keystrokes: &str, context: Option<&str>) -> Self {
+        let context_predicate =
+            context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
+        Self::load(
+            keystrokes,
+            None,
             context_predicate,
             false,
             None,
@@ -47,7 +62,7 @@ impl KeyBinding {
     /// Load a keybinding from the given raw data.
     pub fn load(
         keystrokes: &str,
-        action: Box<dyn Action>,
+        action: impl Into<Option<Box<dyn Action>>>,
         context_predicate: Option<Rc<KeyBindingContextPredicate>>,
         use_key_equivalents: bool,
         action_input: Option<SharedString>,
@@ -67,7 +82,7 @@ impl KeyBinding {
 
         Ok(Self {
             keystrokes,
-            action,
+            action: action.into(),
             context_predicate,
             meta: None,
             action_input,
@@ -106,8 +121,8 @@ impl KeyBinding {
     }
 
     /// Get the action associated with this binding
-    pub fn action(&self) -> &dyn Action {
-        self.action.as_ref()
+    pub fn action(&self) -> Option<&dyn Action> {
+        self.action.as_deref()
     }
 
     /// Get the predicate used to match this binding
@@ -131,7 +146,10 @@ impl std::fmt::Debug for KeyBinding {
         f.debug_struct("KeyBinding")
             .field("keystrokes", &self.keystrokes)
             .field("context_predicate", &self.context_predicate)
-            .field("action", &self.action.name())
+            .field(
+                "action",
+                &self.action.as_deref().map_or("null", |it| it.name()),
+            )
             .finish()
     }
 }

crates/gpui/src/window.rs 🔗

@@ -3800,11 +3800,13 @@ impl Window {
         }
 
         for binding in match_result.bindings {
-            self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
+            if let Some(action) = binding.action.as_deref() {
+                self.dispatch_action_on_node(node_id, action, cx);
+            }
             if !cx.propagate_event {
                 self.dispatch_keystroke_observers(
                     event,
-                    Some(binding.action),
+                    binding.action,
                     match_result.context_stack,
                     cx,
                 );
@@ -3922,14 +3924,11 @@ impl Window {
 
             cx.propagate_event = true;
             for binding in replay.bindings {
-                self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
+                if let Some(action) = binding.action.as_deref() {
+                    self.dispatch_action_on_node(node_id, action, cx);
+                }
                 if !cx.propagate_event {
-                    self.dispatch_keystroke_observers(
-                        &event,
-                        Some(binding.action),
-                        Vec::default(),
-                        cx,
-                    );
+                    self.dispatch_keystroke_observers(&event, binding.action, Vec::default(), cx);
                     continue 'replay;
                 }
             }

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -677,6 +677,9 @@ impl KeymapEditor {
         let mut string_match_candidates = Vec::new();
 
         for key_binding in key_bindings {
+            let Some(action) = key_binding.action() else {
+                continue;
+            };
             let source = key_binding
                 .meta()
                 .map(KeybindSource::from_meta)
@@ -696,7 +699,7 @@ impl KeymapEditor {
                 })
                 .unwrap_or(KeybindContextString::Global);
 
-            let action_name = key_binding.action().name();
+            let action_name = action.name();
             unmapped_action_names.remove(&action_name);
 
             let action_arguments = key_binding
@@ -1814,7 +1817,7 @@ impl Render for KeymapEditor {
                                     let action = div()
                                         .id(("keymap action", index))
                                         .child({
-                                            if action_name != gpui::NoAction.name() {
+                                            if !action_name.is_empty() {
                                                 binding
                                                     .action()
                                                     .humanized_name

crates/language_tools/src/key_context_view.rs 🔗

@@ -62,7 +62,7 @@ impl KeyContextView {
                 .map(|binding| {
                     let match_state = if let Some(predicate) = binding.predicate() {
                         if this.matches(&predicate) {
-                            if this.action_matches(&e.action, binding.action()) {
+                            if this.action_matches(e.action.as_deref(), binding.action()) {
                                 Some(true)
                             } else {
                                 Some(false)
@@ -70,7 +70,7 @@ impl KeyContextView {
                         } else {
                             None
                         }
-                    } else if this.action_matches(&e.action, binding.action()) {
+                    } else if this.action_matches(e.action.as_deref(), binding.action()) {
                         Some(true)
                     } else {
                         Some(false)
@@ -80,10 +80,7 @@ impl KeyContextView {
                     } else {
                         "".to_string()
                     };
-                    let mut name = binding.action().name();
-                    if name == "zed::NoAction" {
-                        name = "(null)"
-                    }
+                    let name = binding.action().map_or("(null)", |action| action.name());
 
                     (
                         name.to_owned().into(),
@@ -130,12 +127,8 @@ impl KeyContextView {
         predicate.depth_of(&self.context_stack).is_some()
     }
 
-    fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
-        if let Some(last_action) = a {
-            last_action.partial_eq(b)
-        } else {
-            b.name() == "zed::NoAction"
-        }
+    fn action_matches(&self, a: Option<&dyn Action>, b: Option<&dyn Action>) -> bool {
+        a.zip(b).is_some_and(|(a, b)| a.partial_eq(b))
     }
 }
 

crates/settings/src/keymap_file.rs 🔗

@@ -2,9 +2,8 @@ use anyhow::{Context as _, Result};
 use collections::{BTreeMap, HashMap, IndexMap};
 use fs::Fs;
 use gpui::{
-    Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
-    KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke,
-    NoAction, SharedString,
+    ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, KeyBinding,
+    KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, SharedString,
 };
 use schemars::{JsonSchema, json_schema};
 use serde::Deserialize;
@@ -350,12 +349,12 @@ impl KeymapFile {
                 let action_input = items[1].clone();
                 let action_input_string = action_input.to_string();
                 (
-                    cx.build_action(name, Some(action_input)),
+                    cx.build_action(name, Some(action_input)).map(Some),
                     Some(action_input_string),
                 )
             }
-            Value::String(name) => (cx.build_action(name, None), None),
-            Value::Null => (Ok(NoAction.boxed_clone()), None),
+            Value::String(name) => (cx.build_action(name, None).map(Some), None),
+            Value::Null => (Ok(None), None),
             _ => {
                 return Err(format!(
                     "expected two-element array of `[name, input]`. \
@@ -410,7 +409,9 @@ impl KeymapFile {
             }
         };
 
-        if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) {
+        if let Some(action) = key_binding.action()
+            && let Some(validator) = KEY_BINDING_VALIDATORS.get(&action.type_id())
+        {
             match validator.validate(&key_binding) {
                 Ok(()) => Ok(key_binding),
                 Err(error) => Err(error.0),
@@ -502,7 +503,7 @@ impl KeymapFile {
 
         let mut empty_schema_action_names = vec![];
         for (name, action_schema) in action_schemas.into_iter() {
-            let deprecation = if name == NoAction.name() {
+            let deprecation = if name.is_empty() {
                 Some("null")
             } else {
                 deprecations.get(name).copied()
@@ -635,7 +636,7 @@ impl KeymapFile {
                 target_keybind_source,
             } if target_keybind_source != KeybindSource::User => {
                 let mut source = target.clone();
-                source.action_name = gpui::NoAction.name();
+                source.action_name = "";
                 source.action_arguments.take();
                 operation = KeybindUpdateOperation::Add {
                     source,
@@ -930,7 +931,7 @@ pub struct KeybindUpdateTarget<'a> {
 
 impl<'a> KeybindUpdateTarget<'a> {
     fn action_value(&self) -> Result<Value> {
-        if self.action_name == gpui::NoAction.name() {
+        if self.action_name.is_empty() {
             return Ok(Value::Null);
         }
         let action_name: Value = self.action_name.into();

crates/ui/src/components/keybinding.rs 🔗

@@ -528,7 +528,7 @@ impl Component for KeyBinding {
                             single_example(
                                 "Default",
                                 KeyBinding::new_from_gpui(
-                                    gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
+                                    gpui::KeyBinding::new_no_action("ctrl-s", None),
                                     cx,
                                 )
                                 .into_any_element(),
@@ -536,7 +536,7 @@ impl Component for KeyBinding {
                             single_example(
                                 "Mac Style",
                                 KeyBinding::new_from_gpui(
-                                    gpui::KeyBinding::new("cmd-s", gpui::NoAction, None),
+                                    gpui::KeyBinding::new_no_action("cmd-s", None),
                                     cx,
                                 )
                                 .platform_style(PlatformStyle::Mac)
@@ -545,7 +545,7 @@ impl Component for KeyBinding {
                             single_example(
                                 "Windows Style",
                                 KeyBinding::new_from_gpui(
-                                    gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
+                                    gpui::KeyBinding::new_no_action("ctrl-s", None),
                                     cx,
                                 )
                                 .platform_style(PlatformStyle::Windows)
@@ -558,7 +558,7 @@ impl Component for KeyBinding {
                         vec![single_example(
                             "Vim Mode Enabled",
                             KeyBinding::new_from_gpui(
-                                gpui::KeyBinding::new("dd", gpui::NoAction, None),
+                                gpui::KeyBinding::new_no_action("dd", None),
                                 cx,
                             )
                             .vim_mode(true)
@@ -571,7 +571,7 @@ impl Component for KeyBinding {
                             single_example(
                                 "Multiple Keys",
                                 KeyBinding::new_from_gpui(
-                                    gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None),
+                                    gpui::KeyBinding::new_no_action("ctrl-k ctrl-b", None),
                                     cx,
                                 )
                                 .into_any_element(),
@@ -579,7 +579,7 @@ impl Component for KeyBinding {
                             single_example(
                                 "With Shift",
                                 KeyBinding::new_from_gpui(
-                                    gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None),
+                                    gpui::KeyBinding::new_no_action("shift-cmd-p", None),
                                     cx,
                                 )
                                 .into_any_element(),

crates/ui/src/components/stories/keybinding.rs 🔗

@@ -1,4 +1,3 @@
-use gpui::NoAction;
 use gpui::Render;
 use itertools::Itertools;
 use story::Story;
@@ -8,7 +7,7 @@ use crate::{KeyBinding, prelude::*};
 pub struct KeybindingStory;
 
 pub fn binding(key: &str) -> gpui::KeyBinding {
-    gpui::KeyBinding::new(key, NoAction {}, None)
+    gpui::KeyBinding::new_no_action(key, None)
 }
 
 impl Render for KeybindingStory {
@@ -42,7 +41,7 @@ impl Render for KeybindingStory {
                                 .py_3()
                                 .children(chunk.map(|permutation| {
                                     KeyBinding::new_from_gpui(
-                                        binding(&(permutation.join("-") + "-x")),
+                                        binding(&(permutation.into_iter().join("-") + "-x")),
                                         cx,
                                     )
                                 }))

crates/vim/src/command.rs 🔗

@@ -1244,14 +1244,14 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
             .range(act_on_range),
         VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range),
         VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range),
-        VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
-            Some(
-                YankCommand {
-                    range: range.clone(),
-                }
-                .boxed_clone(),
-            )
-        }),
+        // VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| {
+        //     Some(
+        //         YankCommand {
+        //             range: range.clone(),
+        //         }
+        //         .boxed_clone(),
+        //     )
+        // }),
         VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
         VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
         VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),

crates/workspace/src/pane.rs 🔗

@@ -370,7 +370,7 @@ pub struct Pane {
     /// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
     /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
     display_nav_history_buttons: Option<bool>,
-    double_click_dispatch_action: Box<dyn Action>,
+    double_click_dispatch_action: Option<Box<dyn Action>>,
     save_modals_spawned: HashSet<EntityId>,
     close_pane_if_empty: bool,
     pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -458,7 +458,7 @@ impl Pane {
         project: Entity<Project>,
         next_timestamp: Arc<AtomicUsize>,
         can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static>>,
-        double_click_dispatch_action: Box<dyn Action>,
+        double_click_dispatch_action: Option<Box<dyn Action>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -3066,10 +3066,14 @@ impl Pane {
                             }))
                             .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
                                 if event.click_count() == 2 {
-                                    window.dispatch_action(
-                                        this.double_click_dispatch_action.boxed_clone(),
-                                        cx,
-                                    );
+                                    if let Some(double_click_dispatch_action) =
+                                        &this.double_click_dispatch_action
+                                    {
+                                        window.dispatch_action(
+                                            double_click_dispatch_action.boxed_clone(),
+                                            cx,
+                                        );
+                                    }
                                 }
                             })),
                     ),
@@ -3744,10 +3748,14 @@ impl Render for Pane {
                                 .on_click(cx.listener(
                                     move |this, event: &ClickEvent, window, cx| {
                                         if event.click_count() == 2 {
-                                            window.dispatch_action(
-                                                this.double_click_dispatch_action.boxed_clone(),
-                                                cx,
-                                            );
+                                            if let Some(double_click_dispatch_action) =
+                                                &this.double_click_dispatch_action
+                                            {
+                                                window.dispatch_action(
+                                                    double_click_dispatch_action.boxed_clone(),
+                                                    cx,
+                                                );
+                                            }
                                         }
                                     },
                                 ));

crates/workspace/src/workspace.rs 🔗

@@ -1306,7 +1306,7 @@ impl Workspace {
                 project.clone(),
                 pane_history_timestamp.clone(),
                 None,
-                NewFile.boxed_clone(),
+                Some(NewFile.boxed_clone()),
                 window,
                 cx,
             );
@@ -3187,7 +3187,7 @@ impl Workspace {
                 self.project.clone(),
                 self.pane_history_timestamp.clone(),
                 None,
-                NewFile.boxed_clone(),
+                Some(NewFile.boxed_clone()),
                 window,
                 cx,
             );