Improve keymap json schema (#23044)

Michael Sloan created

Also:

* Adds `impl_internal_actions!` for deriving the `Action` trait without
registering.

* Removes some deserializers that immediately fail in favor of
`#[serde(skip)]` on fields where they were used. This also omits them
from the schema.

Release Notes:

- Keymap settings file now has more JSON schema information to inform
`json-language-server` completions and info, particularly for actions
that take input.

Change summary

Cargo.lock                                |   6 
crates/assistant/src/assistant.rs         |   6 
crates/editor/src/actions.rs              |  61 +++++----
crates/gpui/src/action.rs                 | 163 +++++++++++++++++++++---
crates/gpui/src/app.rs                    |  18 +
crates/gpui/src/gpui.rs                   |   2 
crates/gpui/tests/action_macros.rs        |   3 
crates/gpui_macros/src/register_action.rs |   5 
crates/language/src/language.rs           |   5 
crates/languages/Cargo.toml               |   1 
crates/languages/src/json.rs              |  16 +
crates/picker/Cargo.toml                  |   1 
crates/picker/src/picker.rs               |   3 
crates/project_panel/src/project_panel.rs |  17 +-
crates/search/Cargo.toml                  |   1 
crates/search/src/buffer_search.rs        |   3 
crates/settings/src/keymap_file.rs        | 154 ++++++++++++++++-------
crates/tab_switcher/Cargo.toml            |   1 
crates/tab_switcher/src/tab_switcher.rs   |   3 
crates/terminal_view/Cargo.toml           |   1 
crates/terminal_view/src/terminal_view.rs |   7 
crates/title_bar/Cargo.toml               |   1 
crates/title_bar/src/application_menu.rs  |   7 
crates/vim/src/command.rs                 |  45 ++---
crates/vim/src/digraph.rs                 |   3 
crates/vim/src/motion.rs                  |  31 ++--
crates/vim/src/normal/increment.rs        |   8 
crates/vim/src/normal/paste.rs            |   6 
crates/vim/src/normal/search.rs           |  24 +-
crates/vim/src/object.rs                  |  12 
crates/vim/src/state.rs                   |  51 +++++--
crates/vim/src/surrounds.rs               |  12 -
crates/vim/src/vim.rs                     |  12 
crates/workspace/src/pane.rs              |  22 +-
crates/workspace/src/pane_group.rs        |   5 
crates/workspace/src/workspace.rs         |  34 ++--
crates/zed_actions/src/lib.rs             |  25 ++-
37 files changed, 499 insertions(+), 276 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -6817,6 +6817,7 @@ dependencies = [
  "regex",
  "rope",
  "rust-embed",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -9332,6 +9333,7 @@ dependencies = [
  "env_logger 0.11.6",
  "gpui",
  "menu",
+ "schemars",
  "serde",
  "serde_json",
  "ui",
@@ -11278,6 +11280,7 @@ dependencies = [
  "language",
  "menu",
  "project",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -12659,6 +12662,7 @@ dependencies = [
  "menu",
  "picker",
  "project",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -12852,6 +12856,7 @@ dependencies = [
  "language",
  "project",
  "rand 0.8.5",
+ "schemars",
  "search",
  "serde",
  "serde_json",
@@ -13186,6 +13191,7 @@ dependencies = [
  "project",
  "remote",
  "rpc",
+ "schemars",
  "serde",
  "settings",
  "smallvec",

crates/assistant/src/assistant.rs 🔗

@@ -26,7 +26,7 @@ pub use context::*;
 pub use context_store::*;
 use feature_flags::FeatureFlagAppExt;
 use fs::Fs;
-use gpui::impl_actions;
+use gpui::impl_internal_actions;
 use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
 pub(crate) use inline_assistant::*;
 use language_model::{
@@ -74,13 +74,13 @@ actions!(
     ]
 );
 
-#[derive(PartialEq, Clone, Deserialize)]
+#[derive(PartialEq, Clone)]
 pub enum InsertDraggedFiles {
     ProjectPaths(Vec<PathBuf>),
     ExternalFiles(Vec<PathBuf>),
 }
 
-impl_actions!(assistant, [InsertDraggedFiles]);
+impl_internal_actions!(assistant, [InsertDraggedFiles]);
 
 const DEFAULT_CONTEXT_LINES: usize = 50;
 

crates/editor/src/actions.rs 🔗

@@ -1,82 +1,84 @@
 //! This module contains all actions supported by [`Editor`].
 use super::*;
 use gpui::{action_aliases, action_as};
+use schemars::JsonSchema;
 use util::serde::default_true;
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct SelectNext {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct SelectPrevious {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct MoveToBeginningOfLine {
     #[serde(default = "default_true")]
     pub stop_at_soft_wraps: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct SelectToBeginningOfLine {
     #[serde(default)]
     pub(super) stop_at_soft_wraps: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct MovePageUp {
     #[serde(default)]
     pub(super) center_cursor: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct MovePageDown {
     #[serde(default)]
     pub(super) center_cursor: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct MoveToEndOfLine {
     #[serde(default = "default_true")]
     pub stop_at_soft_wraps: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct SelectToEndOfLine {
     #[serde(default)]
     pub(super) stop_at_soft_wraps: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ToggleCodeActions {
     // Display row from which the action was deployed.
     #[serde(default)]
+    #[serde(skip)]
     pub deployed_from_indicator: Option<DisplayRow>,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ConfirmCompletion {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ComposeCompletion {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ConfirmCodeAction {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ToggleComments {
     #[serde(default)]
     pub advance_downwards: bool,
@@ -84,84 +86,87 @@ pub struct ToggleComments {
     pub ignore_indent: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct FoldAt {
+    #[serde(skip)]
     pub buffer_row: MultiBufferRow,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct UnfoldAt {
+    #[serde(skip)]
     pub buffer_row: MultiBufferRow,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct MoveUpByLines {
     #[serde(default)]
     pub(super) lines: u32,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct MoveDownByLines {
     #[serde(default)]
     pub(super) lines: u32,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct SelectUpByLines {
     #[serde(default)]
     pub(super) lines: u32,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct SelectDownByLines {
     #[serde(default)]
     pub(super) lines: u32,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ExpandExcerpts {
     #[serde(default)]
     pub(super) lines: u32,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ExpandExcerptsUp {
     #[serde(default)]
     pub(super) lines: u32,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ExpandExcerptsDown {
     #[serde(default)]
     pub(super) lines: u32,
 }
-#[derive(PartialEq, Clone, Deserialize, Default)]
+
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct ShowCompletions {
     #[serde(default)]
     pub(super) trigger: Option<String>,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct HandleInput(pub String);
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct DeleteToNextWordEnd {
     #[serde(default)]
     pub ignore_newlines: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct DeleteToPreviousWordStart {
     #[serde(default)]
     pub ignore_newlines: bool,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct FoldAtLevel {
     pub level: u32,
 }
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct SpawnNearestTask {
     #[serde(default)]
     pub reveal: task::RevealStrategy,

crates/gpui/src/action.rs 🔗

@@ -63,6 +63,16 @@ pub trait Action: 'static + Send {
     where
         Self: Sized;
 
+    /// Optional JSON schema for the action's input data.
+    fn action_json_schema(
+        _: &mut schemars::gen::SchemaGenerator,
+    ) -> Option<schemars::schema::Schema>
+    where
+        Self: Sized,
+    {
+        None
+    }
+
     /// A list of alternate, deprecated names for this action.
     fn deprecated_aliases() -> &'static [&'static str]
     where
@@ -90,16 +100,16 @@ impl dyn Action {
 type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
 
 pub(crate) struct ActionRegistry {
-    builders_by_name: HashMap<SharedString, ActionBuilder>,
+    by_name: HashMap<SharedString, ActionData>,
     names_by_type_id: HashMap<TypeId, SharedString>,
     all_names: Vec<SharedString>, // So we can return a static slice.
-    deprecations: Vec<(SharedString, SharedString)>,
+    deprecations: HashMap<SharedString, SharedString>,
 }
 
 impl Default for ActionRegistry {
     fn default() -> Self {
         let mut this = ActionRegistry {
-            builders_by_name: Default::default(),
+            by_name: Default::default(),
             names_by_type_id: Default::default(),
             all_names: Default::default(),
             deprecations: Default::default(),
@@ -111,19 +121,25 @@ impl Default for ActionRegistry {
     }
 }
 
+struct ActionData {
+    pub build: ActionBuilder,
+    pub json_schema: fn(&mut schemars::gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
+}
+
 /// This type must be public so that our macros can build it in other crates.
 /// But this is an implementation detail and should not be used directly.
 #[doc(hidden)]
-pub type MacroActionBuilder = fn() -> ActionData;
+pub type MacroActionBuilder = fn() -> MacroActionData;
 
 /// This type must be public so that our macros can build it in other crates.
 /// But this is an implementation detail and should not be used directly.
 #[doc(hidden)]
-pub struct ActionData {
+pub struct MacroActionData {
     pub name: &'static str,
     pub aliases: &'static [&'static str],
     pub type_id: TypeId,
     pub build: ActionBuilder,
+    pub json_schema: fn(&mut schemars::gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
 }
 
 /// This constant must be public to be accessible from other crates.
@@ -143,20 +159,35 @@ impl ActionRegistry {
 
     #[cfg(test)]
     pub(crate) fn load_action<A: Action>(&mut self) {
-        self.insert_action(ActionData {
+        self.insert_action(MacroActionData {
             name: A::debug_name(),
             aliases: A::deprecated_aliases(),
             type_id: TypeId::of::<A>(),
             build: A::build,
+            json_schema: A::action_json_schema,
         });
     }
 
-    fn insert_action(&mut self, action: ActionData) {
+    fn insert_action(&mut self, action: MacroActionData) {
         let name: SharedString = action.name.into();
-        self.builders_by_name.insert(name.clone(), action.build);
+        self.by_name.insert(
+            name.clone(),
+            ActionData {
+                build: action.build,
+                json_schema: action.json_schema,
+            },
+        );
         for &alias in action.aliases {
-            self.builders_by_name.insert(alias.into(), action.build);
-            self.deprecations.push((alias.into(), name.clone()));
+            let alias: SharedString = alias.into();
+            self.by_name.insert(
+                alias.clone(),
+                ActionData {
+                    build: action.build,
+                    json_schema: action.json_schema,
+                },
+            );
+            self.deprecations.insert(alias.clone(), name.clone());
+            self.all_names.push(alias);
         }
         self.names_by_type_id.insert(action.type_id, name.clone());
         self.all_names.push(name);
@@ -180,9 +211,10 @@ impl ActionRegistry {
         params: Option<serde_json::Value>,
     ) -> Result<Box<dyn Action>> {
         let build_action = self
-            .builders_by_name
+            .by_name
             .get(name)
-            .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
+            .ok_or_else(|| anyhow!("No action type registered for {}", name))?
+            .build;
         (build_action)(params.unwrap_or_else(|| json!({})))
             .with_context(|| format!("Attempting to build action {}", name))
     }
@@ -191,12 +223,30 @@ impl ActionRegistry {
         self.all_names.as_slice()
     }
 
-    pub fn action_deprecations(&self) -> &[(SharedString, SharedString)] {
-        self.deprecations.as_slice()
+    pub fn action_schemas(
+        &self,
+        generator: &mut schemars::gen::SchemaGenerator,
+    ) -> Vec<(SharedString, Option<schemars::schema::Schema>)> {
+        // Use the order from all_names so that the resulting schema has sensible order.
+        self.all_names
+            .iter()
+            .map(|name| {
+                let action_data = self
+                    .by_name
+                    .get(name)
+                    .expect("All actions in all_names should be registered");
+                (name.clone(), (action_data.json_schema)(generator))
+            })
+            .collect::<Vec<_>>()
+    }
+
+    pub fn action_deprecations(&self) -> &HashMap<SharedString, SharedString> {
+        &self.deprecations
     }
 }
 
-/// Defines unit structs that can be used as actions.
+/// Defines and registers unit structs that can be used as actions.
+///
 /// To use more complex data types as actions, use `impl_actions!`
 #[macro_export]
 macro_rules! actions {
@@ -211,6 +261,11 @@ macro_rules! actions {
             gpui::__impl_action!($namespace, $name, $name,
                 fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
                     Ok(Box::new(Self))
+                },
+                fn action_json_schema(
+                    _: &mut gpui::private::schemars::gen::SchemaGenerator,
+                ) -> Option<gpui::private::schemars::schema::Schema> {
+                    None
                 }
             );
 
@@ -219,11 +274,10 @@ macro_rules! actions {
     };
 }
 
-/// Defines a unit struct that can be used as an actions, with a name
-/// that differs from it's type name.
+/// Defines and registers a unit struct that can be used as an actions, with a name that differs
+/// from it's type name.
 ///
-/// To use more complex data types as actions, and rename them use
-/// `impl_action_as!`
+/// To use more complex data types as actions, and rename them use `impl_action_as!`
 #[macro_export]
 macro_rules! action_as {
     ($namespace:path, $name:ident as $visual_name:ident) => {
@@ -241,6 +295,11 @@ macro_rules! action_as {
                 _: gpui::private::serde_json::Value,
             ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
                 Ok(Box::new(Self))
+            },
+            fn action_json_schema(
+                generator: &mut gpui::private::schemars::gen::SchemaGenerator,
+            ) -> Option<gpui::private::schemars::schema::Schema> {
+                None
             }
         );
 
@@ -248,7 +307,7 @@ macro_rules! action_as {
     };
 }
 
-/// Defines a unit struct that can be used as an action, with some deprecated aliases.
+/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
 #[macro_export]
 macro_rules! action_aliases {
     ($namespace:path, $name:ident, [$($alias:ident),* $(,)?]) => {
@@ -261,6 +320,7 @@ macro_rules! action_aliases {
             ::std::default::Default,
             ::std::fmt::Debug,
             gpui::private::serde_derive::Deserialize,
+            gpui::private::schemars::JsonSchema,
         )]
         #[serde(crate = "gpui::private::serde")]
         pub struct $name;
@@ -274,6 +334,12 @@ macro_rules! action_aliases {
             ) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
                 Ok(Box::new(Self))
             },
+            fn action_json_schema(
+                generator: &mut gpui::private::schemars::gen::SchemaGenerator,
+            ) -> Option<gpui::private::schemars::schema::Schema> {
+                None
+
+            },
             fn deprecated_aliases() -> &'static [&'static str] {
                 &[
                     $(concat!(stringify!($namespace), "::", stringify!($alias))),*
@@ -285,7 +351,11 @@ macro_rules! action_aliases {
     };
 }
 
-/// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize
+/// Registers the action and implements the Action trait for any struct that implements Clone,
+/// Default, PartialEq, serde_deserialize::Deserialize, and schemars::JsonSchema.
+///
+/// Fields and variants that don't make sense for user configuration should be annotated with
+/// #[serde(skip)].
 #[macro_export]
 macro_rules! impl_actions {
     ($namespace:path, [ $($name:ident),* $(,)? ]) => {
@@ -293,6 +363,13 @@ macro_rules! impl_actions {
             gpui::__impl_action!($namespace, $name, $name,
                 fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
                     Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
+                },
+                fn action_json_schema(
+                    generator: &mut gpui::private::schemars::gen::SchemaGenerator,
+                ) -> Option<gpui::private::schemars::schema::Schema> {
+                    Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
+                        generator,
+                    ))
                 }
             );
 
@@ -301,8 +378,41 @@ macro_rules! impl_actions {
     };
 }
 
-/// Implements the Action trait for a struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize
-/// Allows you to rename the action visually, without changing the struct's name
+/// Implements the Action trait for internal action structs that implement Clone, Default,
+/// PartialEq. The purpose of this is to conveniently define values that can be passed in `dyn
+/// Action`.
+///
+/// These actions are internal and so are not registered and do not support deserialization.
+#[macro_export]
+macro_rules! impl_internal_actions {
+    ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+        $(
+            gpui::__impl_action!($namespace, $name, $name,
+                fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
+                    gpui::Result::Err(gpui::private::anyhow::anyhow!(
+                        concat!(
+                            stringify!($namespace),
+                            "::",
+                            stringify!($visual_name),
+                            " is an internal action, so cannot be built from JSON."
+                        )))
+                },
+                fn action_json_schema(
+                    generator: &mut gpui::private::schemars::gen::SchemaGenerator,
+                ) -> Option<gpui::private::schemars::schema::Schema> {
+                    None
+                }
+            );
+        )*
+    };
+}
+
+/// Implements the Action trait for a struct that implements Clone, Default, PartialEq, and
+/// serde_deserialize::Deserialize. Allows you to rename the action visually, without changing the
+/// struct's name.
+///
+/// Fields and variants that don't make sense for user configuration should be annotated with
+/// #[serde(skip)].
 #[macro_export]
 macro_rules! impl_action_as {
     ($namespace:path, $name:ident as $visual_name:tt ) => {
@@ -316,6 +426,13 @@ macro_rules! impl_action_as {
                 Ok(std::boxed::Box::new(
                     gpui::private::serde_json::from_value::<Self>(value)?,
                 ))
+            },
+            fn action_json_schema(
+                generator: &mut gpui::private::schemars::gen::SchemaGenerator,
+            ) -> Option<gpui::private::schemars::schema::Schema> {
+                Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
+                    generator,
+                ))
             }
         );
 

crates/gpui/src/app.rs 🔗

@@ -23,7 +23,7 @@ use parking_lot::RwLock;
 use slotmap::SlotMap;
 
 pub use async_context::*;
-use collections::{FxHashMap, FxHashSet, VecDeque};
+use collections::{FxHashMap, FxHashSet, HashMap, VecDeque};
 pub use entity_map::*;
 use http_client::HttpClient;
 pub use model_context::*;
@@ -1218,16 +1218,22 @@ impl AppContext {
         self.actions.build_action(name, data)
     }
 
-    /// Get a list of all action names that have been registered.
-    /// in the application. Note that registration only allows for
-    /// actions to be built dynamically, and is unrelated to binding
-    /// actions in the element tree.
+    /// Get all action names that have been registered. Note that registration only allows for
+    /// actions to be built dynamically, and is unrelated to binding actions in the element tree.
     pub fn all_action_names(&self) -> &[SharedString] {
         self.actions.all_action_names()
     }
 
+    /// Get all non-internal actions that have been registered, along with their schemas.
+    pub fn action_schemas(
+        &self,
+        generator: &mut schemars::gen::SchemaGenerator,
+    ) -> Vec<(SharedString, Option<schemars::schema::Schema>)> {
+        self.actions.action_schemas(generator)
+    }
+
     /// Get a list of all deprecated action aliases and their canonical names.
-    pub fn action_deprecations(&self) -> &[(SharedString, SharedString)] {
+    pub fn action_deprecations(&self) -> &HashMap<SharedString, SharedString> {
         self.actions.action_deprecations()
     }
 

crates/gpui/src/gpui.rs 🔗

@@ -102,7 +102,9 @@ mod window;
 /// Do not touch, here be dragons for use by gpui_macros and such.
 #[doc(hidden)]
 pub mod private {
+    pub use anyhow;
     pub use linkme;
+    pub use schemars;
     pub use serde;
     pub use serde_derive;
     pub use serde_json;

crates/gpui/tests/action_macros.rs 🔗

@@ -1,12 +1,13 @@
 use gpui::{actions, impl_actions};
 use gpui_macros::register_action;
+use schemars::JsonSchema;
 use serde_derive::Deserialize;
 
 #[test]
 fn test_action_macros() {
     actions!(test, [TestAction]);
 
-    #[derive(PartialEq, Clone, Deserialize)]
+    #[derive(PartialEq, Clone, Deserialize, JsonSchema)]
     struct AnotherTestAction;
 
     impl_actions!(test, [AnotherTestAction]);

crates/gpui_macros/src/register_action.rs 🔗

@@ -29,12 +29,13 @@ pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
             fn __autogenerated() {
                 /// This is an auto generated function, do not use.
                 #[doc(hidden)]
-                fn #action_builder_fn_name() -> gpui::ActionData {
-                    gpui::ActionData {
+                fn #action_builder_fn_name() -> gpui::MacroActionData {
+                    gpui::MacroActionData {
                         name: <#type_name as gpui::Action>::debug_name(),
                         aliases: <#type_name as gpui::Action>::deprecated_aliases(),
                         type_id: ::std::any::TypeId::of::<#type_name>(),
                         build: <#type_name as gpui::Action>::build,
+                        json_schema: <#type_name as gpui::Action>::action_json_schema,
                     }
                 }
                 #[doc(hidden)]

crates/language/src/language.rs 🔗

@@ -662,8 +662,7 @@ pub struct LanguageConfigOverride {
     pub line_comments: Override<Vec<Arc<str>>>,
     #[serde(default)]
     pub block_comment: Override<(Arc<str>, Arc<str>)>,
-    #[serde(skip_deserializing)]
-    #[schemars(skip)]
+    #[serde(skip)]
     pub disabled_bracket_ixs: Vec<u16>,
     #[serde(default)]
     pub word_characters: Override<HashSet<char>>,
@@ -776,7 +775,7 @@ pub struct BracketPairConfig {
     pub pairs: Vec<BracketPair>,
     /// A list of tree-sitter scopes for which a given bracket should not be active.
     /// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]`
-    #[schemars(skip)]
+    #[serde(skip)]
     pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
 }
 

crates/languages/Cargo.toml 🔗

@@ -56,6 +56,7 @@ project.workspace = true
 regex.workspace = true
 rope.workspace = true
 rust-embed.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/languages/src/json.rs 🔗

@@ -10,6 +10,7 @@ use language::{LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterD
 use lsp::{LanguageServerBinary, LanguageServerName};
 use node_runtime::NodeRuntime;
 use project::{lsp_store::language_server_settings, ContextProviderWithTasks};
+use schemars::gen::SchemaSettings;
 use serde_json::{json, Value};
 use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
 use smol::{
@@ -75,9 +76,6 @@ impl JsonLspAdapter {
     }
 
     fn get_workspace_config(language_names: Vec<String>, cx: &mut AppContext) -> Value {
-        let action_names = cx.all_action_names();
-        let deprecations = cx.action_deprecations();
-
         let font_names = &cx.text_system().all_font_names();
         let settings_schema = cx.global::<SettingsStore>().json_schema(
             &SettingsJsonSchemaParams {
@@ -117,7 +115,7 @@ impl JsonLspAdapter {
                     },
                     {
                         "fileMatch": [schema_file_match(paths::keymap_file())],
-                        "schema": KeymapFile::generate_json_schema(action_names, deprecations),
+                        "schema": Self::generate_keymap_schema(cx),
                     },
                     {
                         "fileMatch": [
@@ -131,6 +129,16 @@ impl JsonLspAdapter {
             }
         })
     }
+
+    fn generate_keymap_schema(cx: &mut AppContext) -> Value {
+        let mut generator = SchemaSettings::draft07()
+            .with(|settings| settings.option_add_null_type = false)
+            .into_generator();
+
+        let action_schemas = cx.action_schemas(&mut generator);
+        let deprecations = cx.action_deprecations();
+        KeymapFile::generate_json_schema(generator, action_schemas, deprecations)
+    }
 }
 
 #[async_trait(?Send)]

crates/picker/Cargo.toml 🔗

@@ -20,6 +20,7 @@ anyhow.workspace = true
 editor.workspace = true
 gpui.workspace = true
 menu.workspace = true
+schemars.workspace = true
 serde.workspace = true
 ui.workspace = true
 workspace.workspace = true

crates/picker/src/picker.rs 🔗

@@ -7,6 +7,7 @@ use gpui::{
     ViewContext, WindowContext,
 };
 use head::Head;
+use schemars::JsonSchema;
 use serde::Deserialize;
 use std::{sync::Arc, time::Duration};
 use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing};
@@ -24,7 +25,7 @@ actions!(picker, [ConfirmCompletion]);
 
 /// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally,
 /// performing some kind of action on it.
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(Clone, PartialEq, Deserialize, JsonSchema, Default)]
 pub struct ConfirmInput {
     pub secondary: bool,
 }

crates/project_panel/src/project_panel.rs 🔗

@@ -1,10 +1,10 @@
 mod project_panel_settings;
 mod utils;
 
+use anyhow::{anyhow, Context as _, Result};
 use client::{ErrorCode, ErrorExt};
-use language::DiagnosticSeverity;
-use settings::{Settings, SettingsStore};
-
+use collections::{hash_map, BTreeSet, HashMap};
+use command_palette_hooks::CommandPaletteFilter;
 use db::kvp::KEY_VALUE_STORE;
 use editor::{
     items::{
@@ -15,10 +15,6 @@ use editor::{
     Editor, EditorEvent, EditorSettings, ShowScrollbar,
 };
 use file_icons::FileIcons;
-
-use anyhow::{anyhow, Context as _, Result};
-use collections::{hash_map, BTreeSet, HashMap};
-use command_palette_hooks::CommandPaletteFilter;
 use git::repository::GitFileStatus;
 use gpui::{
     actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action,
@@ -30,6 +26,7 @@ use gpui::{
     VisualContext as _, WeakView, WindowContext,
 };
 use indexmap::IndexMap;
+use language::DiagnosticSeverity;
 use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
 use project::{
     relativize_path, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree,
@@ -38,7 +35,9 @@ use project::{
 use project_panel_settings::{
     ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides,
 };
+use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
 use smallvec::SmallVec;
 use std::any::TypeId;
 use std::{
@@ -152,13 +151,13 @@ struct EntryDetails {
     canonical_path: Option<Box<Path>>,
 }
 
-#[derive(PartialEq, Clone, Default, Debug, Deserialize)]
+#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
 struct Delete {
     #[serde(default)]
     pub skip_prompt: bool,
 }
 
-#[derive(PartialEq, Clone, Default, Debug, Deserialize)]
+#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
 struct Trash {
     #[serde(default)]
     pub skip_prompt: bool,

crates/search/Cargo.toml 🔗

@@ -31,6 +31,7 @@ gpui.workspace = true
 language.workspace = true
 menu.workspace = true
 project.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/search/src/buffer_search.rs 🔗

@@ -22,6 +22,7 @@ use project::{
     search::SearchQuery,
     search_history::{SearchHistory, SearchHistoryCursor},
 };
+use schemars::JsonSchema;
 use serde::Deserialize;
 use settings::Settings;
 use std::sync::Arc;
@@ -43,7 +44,7 @@ use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
 
 const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
 
-#[derive(PartialEq, Clone, Deserialize)]
+#[derive(PartialEq, Clone, Deserialize, JsonSchema)]
 pub struct Deploy {
     #[serde(default = "util::serde::default_true")]
     pub focus: bool,

crates/settings/src/keymap_file.rs 🔗

@@ -1,11 +1,11 @@
 use crate::{settings_store::parse_json_with_comments, SettingsAssets};
 use anyhow::{anyhow, Context, Result};
-use collections::BTreeMap;
+use collections::{BTreeMap, HashMap};
 use gpui::{Action, AppContext, KeyBinding, SharedString};
 use schemars::{
-    gen::{SchemaGenerator, SchemaSettings},
-    schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
-    JsonSchema, Map,
+    gen::SchemaGenerator,
+    schema::{ArrayValidation, InstanceType, Metadata, Schema, SchemaObject, SubschemaValidation},
+    JsonSchema,
 };
 use serde::Deserialize;
 use serde_json::Value;
@@ -140,55 +140,117 @@ impl KeymapFile {
     }
 
     pub fn generate_json_schema(
-        action_names: &[SharedString],
-        deprecations: &[(SharedString, SharedString)],
+        generator: SchemaGenerator,
+        action_schemas: Vec<(SharedString, Option<Schema>)>,
+        deprecations: &HashMap<SharedString, SharedString>,
     ) -> serde_json::Value {
-        let mut root_schema = SchemaSettings::draft07()
-            .with(|settings| settings.option_add_null_type = false)
-            .into_generator()
-            .into_root_schema_for::<KeymapFile>();
-
-        let mut alternatives = vec![
-            Schema::Object(SchemaObject {
-                instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
-                enum_values: Some(
-                    action_names
-                        .iter()
-                        .map(|name| Value::String(name.to_string()))
-                        .collect(),
-                ),
-                ..Default::default()
-            }),
-            Schema::Object(SchemaObject {
-                instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
-                ..Default::default()
-            }),
-            Schema::Object(SchemaObject {
-                instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
+        fn set<I, O>(input: I) -> Option<O>
+        where
+            I: Into<O>,
+        {
+            Some(input.into())
+        }
+
+        fn add_deprecation_notice(schema_object: &mut SchemaObject, new_name: &SharedString) {
+            schema_object.extensions.insert(
+                // deprecationMessage is not part of the JSON Schema spec,
+                // but json-language-server recognizes it.
+                "deprecationMessage".to_owned(),
+                format!("Deprecated, use {new_name}").into(),
+            );
+        }
+
+        let empty_object: SchemaObject = SchemaObject {
+            instance_type: set(InstanceType::Object),
+            ..Default::default()
+        };
+
+        let mut keymap_action_alternatives = Vec::new();
+        for (name, action_schema) in action_schemas.iter() {
+            let schema = if let Some(Schema::Object(schema)) = action_schema {
+                Some(schema.clone())
+            } else {
+                None
+            };
+
+            // If the type has a description, also apply it to the value. Ideally it would be
+            // removed and applied to the overall array, but `json-language-server` does not show
+            // these descriptions.
+            let description = schema.as_ref().and_then(|schema| {
+                schema
+                    .metadata
+                    .as_ref()
+                    .and_then(|metadata| metadata.description.as_ref())
+            });
+            let mut matches_action_name = SchemaObject {
+                const_value: Some(Value::String(name.to_string())),
                 ..Default::default()
-            }),
-        ];
-        for (old, new) in deprecations {
-            alternatives.push(Schema::Object(SchemaObject {
-                instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
-                const_value: Some(Value::String(old.to_string())),
-                extensions: Map::from_iter([(
-                    // deprecationMessage is not part of the JSON Schema spec,
-                    // but json-language-server recognizes it.
-                    "deprecationMessage".to_owned(),
-                    format!("Deprecated, use {new}").into(),
-                )]),
+            };
+            if let Some(description) = description {
+                matches_action_name.metadata = set(Metadata {
+                    description: Some(description.clone()),
+                    ..Default::default()
+                });
+            }
+
+            // Add an alternative for plain action names.
+            let deprecation = deprecations.get(name);
+            let mut plain_action = SchemaObject {
+                instance_type: set(InstanceType::String),
+                const_value: Some(Value::String(name.to_string())),
                 ..Default::default()
-            }));
+            };
+            if let Some(new_name) = deprecation {
+                add_deprecation_notice(&mut plain_action, new_name);
+            }
+            keymap_action_alternatives.push(plain_action.into());
+
+            // When all fields are skipped or an empty struct is added with impl_actions! /
+            // impl_actions_as! an empty struct is produced. The action should be invoked without
+            // data in this case.
+            if let Some(schema) = schema {
+                if schema != empty_object {
+                    let mut action_with_data = SchemaObject {
+                        instance_type: set(InstanceType::Array),
+                        array: Some(
+                            ArrayValidation {
+                                items: set(vec![matches_action_name.into(), schema.into()]),
+                                min_items: Some(2),
+                                max_items: Some(2),
+                                ..Default::default()
+                            }
+                            .into(),
+                        ),
+                        ..Default::default()
+                    };
+                    if let Some(new_name) = deprecation {
+                        add_deprecation_notice(&mut action_with_data, new_name);
+                    }
+                    keymap_action_alternatives.push(action_with_data.into());
+                }
+            }
         }
-        let action_schema = Schema::Object(SchemaObject {
-            subschemas: Some(Box::new(SubschemaValidation {
-                one_of: Some(alternatives),
+
+        // Placing null first causes json-language-server to default assuming actions should be
+        // null, so place it last.
+        keymap_action_alternatives.push(
+            SchemaObject {
+                instance_type: set(InstanceType::Null),
+                ..Default::default()
+            }
+            .into(),
+        );
+
+        let action_schema = SchemaObject {
+            subschemas: set(SubschemaValidation {
+                one_of: Some(keymap_action_alternatives),
                 ..Default::default()
-            })),
+            }),
             ..Default::default()
-        });
+        }
+        .into();
 
+        let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
         root_schema
             .definitions
             .insert("KeymapAction".to_owned(), action_schema);

crates/tab_switcher/Cargo.toml 🔗

@@ -19,6 +19,7 @@ gpui.workspace = true
 menu.workspace = true
 picker.workspace = true
 project.workspace = true
+schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 ui.workspace = true

crates/tab_switcher/src/tab_switcher.rs 🔗

@@ -10,6 +10,7 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate};
 use project::Project;
+use schemars::JsonSchema;
 use serde::Deserialize;
 use settings::Settings;
 use std::sync::Arc;
@@ -23,7 +24,7 @@ use workspace::{
 
 const PANEL_WIDTH_REMS: f32 = 28.;
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, JsonSchema, Default)]
 pub struct Toggle {
     #[serde(default)]
     pub select_last: bool,

crates/terminal_view/Cargo.toml 🔗

@@ -29,6 +29,7 @@ itertools.workspace = true
 language.workspace = true
 project.workspace = true
 task.workspace = true
+schemars.workspace = true
 search.workspace = true
 serde.workspace = true
 serde_json.workspace = true

crates/terminal_view/src/terminal_view.rs 🔗

@@ -15,6 +15,7 @@ use gpui::{
 use language::Bias;
 use persistence::TERMINAL_DB;
 use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project};
+use schemars::JsonSchema;
 use terminal::{
     alacritty_terminal::{
         index::Point,
@@ -66,14 +67,14 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 
 const GIT_DIFF_PATH_PREFIXES: &[char] = &['a', 'b'];
 
-///Event to transmit the scroll from the element to the view
+/// Event to transmit the scroll from the element to the view
 #[derive(Clone, Debug, PartialEq)]
 pub struct ScrollTerminal(pub i32);
 
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq)]
 pub struct SendText(String);
 
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq)]
 pub struct SendKeystroke(String);
 
 impl_actions!(terminal, [SendText, SendKeystroke]);

crates/title_bar/Cargo.toml 🔗

@@ -36,6 +36,7 @@ notifications.workspace = true
 project.workspace = true
 remote.workspace = true
 rpc.workspace = true
+schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 smallvec.workspace = true

crates/title_bar/src/application_menu.rs 🔗

@@ -1,17 +1,18 @@
 use gpui::{impl_actions, OwnedMenu, OwnedMenuItem, View};
+use schemars::JsonSchema;
 use serde::Deserialize;
 use smallvec::SmallVec;
 use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
 
 impl_actions!(
     app_menu,
-    [OpenApplicationMenu, NavigateApplicationMenuInDirection,]
+    [OpenApplicationMenu, NavigateApplicationMenuInDirection]
 );
 
-#[derive(Clone, Deserialize, PartialEq, Default)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default)]
 pub struct OpenApplicationMenu(String);
 
-#[derive(Clone, Deserialize, PartialEq, Default)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default)]
 pub struct NavigateApplicationMenuInDirection(String);
 
 #[derive(Clone)]

crates/vim/src/command.rs 🔗

@@ -1,11 +1,3 @@
-use std::{
-    iter::Peekable,
-    ops::{Deref, Range},
-    str::Chars,
-    sync::OnceLock,
-    time::Instant,
-};
-
 use anyhow::{anyhow, Result};
 use command_palette_hooks::CommandInterceptResult;
 use editor::{
@@ -13,12 +5,22 @@ use editor::{
     display_map::ToDisplayPoint,
     Bias, Editor, ToPoint,
 };
-use gpui::{actions, impl_actions, Action, AppContext, Global, ViewContext, WindowContext};
+use gpui::{
+    actions, impl_internal_actions, Action, AppContext, Global, ViewContext, WindowContext,
+};
 use language::Point;
 use multi_buffer::MultiBufferRow;
 use regex::Regex;
+use schemars::JsonSchema;
 use search::{BufferSearchBar, SearchOptions};
 use serde::Deserialize;
+use std::{
+    iter::Peekable,
+    ops::{Deref, Range},
+    str::Chars,
+    sync::OnceLock,
+    time::Instant,
+};
 use util::ResultExt;
 use workspace::{notifications::NotifyResultExt, SaveIntent};
 
@@ -33,24 +35,24 @@ use crate::{
     Vim,
 };
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct GoToLine {
     range: CommandRange,
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct YankCommand {
     range: CommandRange,
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct WithRange {
     restore_selection: bool,
     range: CommandRange,
     action: WrappedAction,
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct WithCount {
     count: u32,
     action: WrappedAction,
@@ -60,20 +62,11 @@ pub struct WithCount {
 struct WrappedAction(Box<dyn Action>);
 
 actions!(vim, [VisualCommand, CountCommand]);
-impl_actions!(
+impl_internal_actions!(
     vim,
     [GoToLine, YankCommand, WithRange, WithCount, OnMatchingLines]
 );
 
-impl<'de> Deserialize<'de> for WrappedAction {
-    fn deserialize<D>(_: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        Err(serde::de::Error::custom("Cannot deserialize WrappedAction"))
-    }
-}
-
 impl PartialEq for WrappedAction {
     fn eq(&self, other: &Self) -> bool {
         self.0.partial_eq(&*other.0)
@@ -423,7 +416,7 @@ impl VimCommand {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 enum Position {
     Line { row: u32, offset: i32 },
     Mark { name: char, offset: i32 },
@@ -467,7 +460,7 @@ impl Position {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq)]
 pub(crate) struct CommandRange {
     start: Position,
     end: Option<Position>,
@@ -877,7 +870,7 @@ fn generate_positions(string: &str, query: &str) -> Vec<usize> {
     positions
 }
 
-#[derive(Debug, PartialEq, Deserialize, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub(crate) struct OnMatchingLines {
     range: CommandRange,
     search: String,

crates/vim/src/digraph.rs 🔗

@@ -3,6 +3,7 @@ use std::sync::Arc;
 use collections::HashMap;
 use editor::Editor;
 use gpui::{impl_actions, AppContext, Keystroke, KeystrokeEvent};
+use schemars::JsonSchema;
 use serde::Deserialize;
 use settings::Settings;
 use std::sync::LazyLock;
@@ -12,7 +13,7 @@ use crate::{state::Operator, Vim, VimSettings};
 
 mod default;
 
-#[derive(PartialEq, Clone, Deserialize)]
+#[derive(Debug, Clone, Deserialize, JsonSchema, PartialEq)]
 struct Literal(String, char);
 impl_actions!(vim, [Literal]);
 

crates/vim/src/motion.rs 🔗

@@ -9,6 +9,7 @@ use editor::{
 use gpui::{actions, impl_actions, px, ViewContext};
 use language::{CharKind, Point, Selection, SelectionGoal};
 use multi_buffer::MultiBufferRow;
+use schemars::JsonSchema;
 use serde::Deserialize;
 use std::ops::Range;
 use workspace::searchable::Direction;
@@ -139,105 +140,105 @@ pub enum Motion {
     },
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct NextWordStart {
     #[serde(default)]
     ignore_punctuation: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct NextWordEnd {
     #[serde(default)]
     ignore_punctuation: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct PreviousWordStart {
     #[serde(default)]
     ignore_punctuation: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct PreviousWordEnd {
     #[serde(default)]
     ignore_punctuation: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub(crate) struct NextSubwordStart {
     #[serde(default)]
     pub(crate) ignore_punctuation: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub(crate) struct NextSubwordEnd {
     #[serde(default)]
     pub(crate) ignore_punctuation: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub(crate) struct PreviousSubwordStart {
     #[serde(default)]
     pub(crate) ignore_punctuation: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub(crate) struct PreviousSubwordEnd {
     #[serde(default)]
     pub(crate) ignore_punctuation: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub(crate) struct Up {
     #[serde(default)]
     pub(crate) display_lines: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub(crate) struct Down {
     #[serde(default)]
     pub(crate) display_lines: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct FirstNonWhitespace {
     #[serde(default)]
     display_lines: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct EndOfLine {
     #[serde(default)]
     display_lines: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub struct StartOfLine {
     #[serde(default)]
     pub(crate) display_lines: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct UnmatchedForward {
     #[serde(default)]
     char: char,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct UnmatchedBackward {
     #[serde(default)]

crates/vim/src/normal/increment.rs 🔗

@@ -1,20 +1,20 @@
-use std::ops::Range;
-
 use editor::{scroll::Autoscroll, Editor, MultiBufferSnapshot, ToOffset, ToPoint};
 use gpui::{impl_actions, ViewContext};
 use language::{Bias, Point};
+use schemars::JsonSchema;
 use serde::Deserialize;
+use std::ops::Range;
 
 use crate::{state::Mode, Vim};
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct Increment {
     #[serde(default)]
     step: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct Decrement {
     #[serde(default)]

crates/vim/src/normal/paste.rs 🔗

@@ -1,16 +1,16 @@
-use std::cmp;
-
 use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, DisplayPoint, RowExt};
 use gpui::{impl_actions, ViewContext};
 use language::{Bias, SelectionGoal};
+use schemars::JsonSchema;
 use serde::Deserialize;
+use std::cmp;
 
 use crate::{
     state::{Mode, Register},
     Vim,
 };
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub struct Paste {
     #[serde(default)]

crates/vim/src/normal/search.rs 🔗

@@ -1,10 +1,10 @@
-use std::{iter::Peekable, str::Chars, time::Duration};
-
 use editor::Editor;
-use gpui::{actions, impl_actions, ViewContext};
+use gpui::{actions, impl_actions, impl_internal_actions, ViewContext};
 use language::Point;
+use schemars::JsonSchema;
 use search::{buffer_search, BufferSearchBar, SearchOptions};
 use serde_derive::Deserialize;
+use std::{iter::Peekable, str::Chars, time::Duration};
 use util::serde::default_true;
 use workspace::{notifications::NotifyResultExt, searchable::Direction};
 
@@ -15,7 +15,7 @@ use crate::{
     Vim,
 };
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub(crate) struct MoveToNext {
     #[serde(default = "default_true")]
@@ -26,7 +26,7 @@ pub(crate) struct MoveToNext {
     regex: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 pub(crate) struct MoveToPrev {
     #[serde(default = "default_true")]
@@ -37,7 +37,7 @@ pub(crate) struct MoveToPrev {
     regex: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 pub(crate) struct Search {
     #[serde(default)]
     backwards: bool,
@@ -45,19 +45,19 @@ pub(crate) struct Search {
     regex: bool,
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)]
 pub struct FindCommand {
     pub query: String,
     pub backwards: bool,
 }
 
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct ReplaceCommand {
     pub(crate) range: CommandRange,
     pub(crate) replacement: Replacement,
 }
 
-#[derive(Debug, Default, PartialEq, Deserialize, Clone)]
+#[derive(Clone, Debug, PartialEq)]
 pub(crate) struct Replacement {
     search: String,
     replacement: String,
@@ -66,10 +66,8 @@ pub(crate) struct Replacement {
 }
 
 actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);
-impl_actions!(
-    vim,
-    [FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
-);
+impl_actions!(vim, [FindCommand, Search, MoveToPrev, MoveToNext]);
+impl_internal_actions!(vim, [ReplaceCommand]);
 
 pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
     Vim::action(editor, cx, Vim::move_to_next);

crates/vim/src/object.rs 🔗

@@ -10,15 +10,14 @@ use editor::{
     movement::{self, FindRange},
     Bias, DisplayPoint, Editor,
 };
-
-use itertools::Itertools;
-
 use gpui::{actions, impl_actions, ViewContext};
+use itertools::Itertools;
 use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
 use multi_buffer::MultiBufferRow;
+use schemars::JsonSchema;
 use serde::Deserialize;
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
 pub enum Object {
     Word { ignore_punctuation: bool },
     Sentence,
@@ -40,13 +39,14 @@ pub enum Object {
     Comment,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct Word {
     #[serde(default)]
     ignore_punctuation: bool,
 }
-#[derive(Clone, Deserialize, PartialEq)]
+
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 #[serde(rename_all = "camelCase")]
 struct IndentObj {
     #[serde(default)]

crates/vim/src/state.rs 🔗

@@ -1,6 +1,3 @@
-use std::borrow::BorrowMut;
-use std::{fmt::Display, ops::Range, sync::Arc};
-
 use crate::command::command_interceptor;
 use crate::normal::repeat::Replayer;
 use crate::surrounds::SurroundsType;
@@ -13,12 +10,15 @@ use gpui::{
     Action, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Global, View, WeakView,
 };
 use language::Point;
+use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
+use std::borrow::BorrowMut;
+use std::{fmt::Display, ops::Range, sync::Arc};
 use ui::{SharedString, ViewContext};
 use workspace::searchable::Direction;
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, JsonSchema, Serialize)]
 pub enum Mode {
     Normal,
     Insert,
@@ -59,22 +59,39 @@ impl Default for Mode {
     }
 }
 
-#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
 pub enum Operator {
     Change,
     Delete,
     Yank,
     Replace,
-    Object { around: bool },
-    FindForward { before: bool },
-    FindBackward { after: bool },
-    Sneak { first_char: Option<char> },
-    SneakBackward { first_char: Option<char> },
-    AddSurrounds { target: Option<SurroundsType> },
-    ChangeSurrounds { target: Option<Object> },
+    Object {
+        around: bool,
+    },
+    FindForward {
+        before: bool,
+    },
+    FindBackward {
+        after: bool,
+    },
+    Sneak {
+        first_char: Option<char>,
+    },
+    SneakBackward {
+        first_char: Option<char>,
+    },
+    AddSurrounds {
+        #[serde(skip)]
+        target: Option<SurroundsType>,
+    },
+    ChangeSurrounds {
+        target: Option<Object>,
+    },
     DeleteSurrounds,
     Mark,
-    Jump { line: bool },
+    Jump {
+        line: bool,
+    },
     Indent,
     Outdent,
     AutoIndent,
@@ -82,8 +99,12 @@ pub enum Operator {
     Lowercase,
     Uppercase,
     OppositeCase,
-    Digraph { first_char: Option<char> },
-    Literal { prefix: Option<String> },
+    Digraph {
+        first_char: Option<char>,
+    },
+    Literal {
+        prefix: Option<String>,
+    },
     Register,
     RecordRegister,
     ReplayRegister,

crates/vim/src/surrounds.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
 };
 use editor::{movement, scroll::Autoscroll, Bias};
 use language::BracketPair;
-use serde::Deserialize;
+
 use std::sync::Arc;
 use ui::ViewContext;
 
@@ -17,16 +17,6 @@ pub enum SurroundsType {
     Selection,
 }
 
-// This exists so that we can have Deserialize on Operators, but not on Motions.
-impl<'de> Deserialize<'de> for SurroundsType {
-    fn deserialize<D>(_: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        Err(serde::de::Error::custom("Cannot deserialize SurroundsType"))
-    }
-}
-
 impl Vim {
     pub fn add_surrounds(
         &mut self,

crates/vim/src/vim.rs 🔗

@@ -49,25 +49,25 @@ use workspace::{self, Pane, ResizeIntent, Workspace};
 use crate::state::ReplayableAction;
 
 /// Used to resize the current pane
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 pub struct ResizePane(pub ResizeIntent);
 
 /// An Action to Switch between modes
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 pub struct SwitchMode(pub Mode);
 
 /// PushOperator is used to put vim into a "minor" mode,
 /// where it's waiting for a specific next set of keystrokes.
 /// For example 'd' needs a motion to complete.
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 pub struct PushOperator(pub Operator);
 
 /// Number is used to manage vim's count. Pushing a digit
-/// multiplis the current value by 10 and adds the digit.
-#[derive(Clone, Deserialize, PartialEq)]
+/// multiplies the current value by 10 and adds the digit.
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 struct Number(usize);
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
 struct SelectRegister(String);
 
 actions!(

crates/workspace/src/pane.rs 🔗

@@ -25,6 +25,7 @@ use itertools::Itertools;
 use language::DiagnosticSeverity;
 use parking_lot::Mutex;
 use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
+use schemars::JsonSchema;
 use serde::Deserialize;
 use settings::{Settings, SettingsStore};
 use std::{
@@ -71,7 +72,7 @@ impl DraggedSelection {
     }
 }
 
-#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
+#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)]
 #[serde(rename_all = "camelCase")]
 pub enum SaveIntent {
     /// write all files (even if unchanged)
@@ -92,16 +93,16 @@ pub enum SaveIntent {
     Skip,
 }
 
-#[derive(Clone, Deserialize, PartialEq, Debug)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 pub struct ActivateItem(pub usize);
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseActiveItem {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseInactiveItems {
     pub save_intent: Option<SaveIntent>,
@@ -109,7 +110,7 @@ pub struct CloseInactiveItems {
     pub close_pinned: bool,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseAllItems {
     pub save_intent: Option<SaveIntent>,
@@ -117,34 +118,35 @@ pub struct CloseAllItems {
     pub close_pinned: bool,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseCleanItems {
     #[serde(default)]
     pub close_pinned: bool,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseItemsToTheRight {
     #[serde(default)]
     pub close_pinned: bool,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseItemsToTheLeft {
     #[serde(default)]
     pub close_pinned: bool,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct RevealInProjectPanel {
+    #[serde(skip)]
     pub entry_id: Option<u64>,
 }
 
-#[derive(Default, PartialEq, Clone, Deserialize)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default)]
 pub struct DeploySearch {
     #[serde(default)]
     pub replace_enabled: bool,

crates/workspace/src/pane_group.rs 🔗

@@ -13,6 +13,7 @@ use gpui::{
 };
 use parking_lot::Mutex;
 use project::Project;
+use schemars::JsonSchema;
 use serde::Deserialize;
 use settings::Settings;
 use std::sync::Arc;
@@ -717,7 +718,7 @@ impl PaneAxis {
     }
 }
 
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, JsonSchema)]
 pub enum SplitDirection {
     Up,
     Down,
@@ -800,7 +801,7 @@ impl SplitDirection {
     }
 }
 
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
+#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq)]
 pub enum ResizeIntent {
     Lengthen,
     Shorten,

crates/workspace/src/workspace.rs 🔗

@@ -61,10 +61,9 @@ use persistence::{
     SerializedWindowBounds, DB,
 };
 use postage::stream::Stream;
-use project::{
-    DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
-};
+use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree};
 use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
+use schemars::JsonSchema;
 use serde::Deserialize;
 use session::AppSession;
 use settings::Settings;
@@ -119,9 +118,6 @@ static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
         .and_then(parse_pixel_position_env_var)
 });
 
-#[derive(Clone, PartialEq)]
-pub struct RemoveWorktreeFromProject(pub WorktreeId);
-
 actions!(assistant, [ShowConfiguration]);
 
 actions!(
@@ -165,64 +161,64 @@ pub struct OpenPaths {
     pub paths: Vec<PathBuf>,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 pub struct ActivatePane(pub usize);
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 pub struct ActivatePaneInDirection(pub SplitDirection);
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 pub struct SwapPaneInDirection(pub SplitDirection);
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 pub struct MoveItemToPane {
     pub destination: usize,
     #[serde(default = "default_true")]
     pub focus: bool,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 pub struct MoveItemToPaneInDirection {
     pub direction: SplitDirection,
     #[serde(default = "default_true")]
     pub focus: bool,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct SaveAll {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct Save {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseAllItemsAndPanes {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseInactiveTabsAndPanes {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 pub struct SendKeystrokes(pub String);
 
-#[derive(Clone, Deserialize, PartialEq, Default)]
+#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema)]
 pub struct Reload {
     pub binary_path: Option<PathBuf>,
 }
 
 action_as!(project_symbols, ToggleProjectSymbols as Toggle);
 
-#[derive(Default, PartialEq, Eq, Clone, serde::Deserialize)]
+#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema)]
 pub struct ToggleFileFinder {
     #[serde(default)]
     pub separate_history: bool,
@@ -299,7 +295,7 @@ impl PartialEq for Toast {
     }
 }
 
-#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
 pub struct OpenTerminal {
     pub working_directory: PathBuf,
 }

crates/zed_actions/src/lib.rs 🔗

@@ -11,12 +11,12 @@ use serde::{Deserialize, Serialize};
 // https://github.com/mmastrac/rust-ctor/issues/280
 pub fn init() {}
 
-#[derive(Clone, PartialEq, Deserialize)]
+#[derive(Clone, PartialEq, Deserialize, JsonSchema)]
 pub struct OpenBrowser {
     pub url: String,
 }
 
-#[derive(Clone, PartialEq, Deserialize)]
+#[derive(Clone, PartialEq, Deserialize, JsonSchema)]
 pub struct OpenZedUrl {
     pub url: String,
 }
@@ -65,9 +65,10 @@ pub mod feedback {
 
 pub mod theme_selector {
     use gpui::impl_actions;
+    use schemars::JsonSchema;
     use serde::Deserialize;
 
-    #[derive(PartialEq, Clone, Default, Debug, Deserialize)]
+    #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
     pub struct Toggle {
         /// A list of theme names to filter the theme selector down to.
         pub themes_filter: Option<Vec<String>>,
@@ -76,20 +77,21 @@ pub mod theme_selector {
     impl_actions!(theme_selector, [Toggle]);
 }
 
-#[derive(Clone, Default, Deserialize, PartialEq)]
+#[derive(Clone, Default, Deserialize, PartialEq, JsonSchema)]
 pub struct InlineAssist {
     pub prompt: Option<String>,
 }
 
 impl_actions!(assistant, [InlineAssist]);
 
-#[derive(PartialEq, Clone, Deserialize, Default)]
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct OpenRecent {
     #[serde(default)]
     pub create_new_window: bool,
 }
-gpui::impl_actions!(projects, [OpenRecent]);
-gpui::actions!(projects, [OpenRemote]);
+
+impl_actions!(projects, [OpenRecent]);
+actions!(projects, [OpenRemote]);
 
 /// Where to spawn the task in the UI.
 #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
@@ -102,8 +104,8 @@ pub enum RevealTarget {
     Dock,
 }
 
-/// Spawn a task with name or open tasks modal
-#[derive(Debug, PartialEq, Clone, Deserialize)]
+/// Spawn a task with name or open tasks modal.
+#[derive(Debug, PartialEq, Clone, Deserialize, JsonSchema)]
 #[serde(untagged)]
 pub enum Spawn {
     /// Spawns a task by the name given.
@@ -128,8 +130,8 @@ impl Spawn {
     }
 }
 
-/// Rerun last task
-#[derive(PartialEq, Clone, Deserialize, Default)]
+/// Rerun the last task.
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
 pub struct Rerun {
     /// Controls whether the task context is reevaluated prior to execution of a task.
     /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task
@@ -147,6 +149,7 @@ pub struct Rerun {
     pub use_new_terminal: Option<bool>,
 
     /// If present, rerun the task with this ID, otherwise rerun the last task.
+    #[serde(skip)]
     pub task_id: Option<String>,
 }