Improve keymap json schema (#23044)

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.
This commit is contained in:
Michael Sloan 2025-01-12 19:34:35 -07:00 committed by GitHub
parent 4c50201036
commit 6aba3950d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 506 additions and 283 deletions

View file

@ -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 {
}
}
/// 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;
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 struct 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 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,
))
}
);

View file

@ -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()
}

View file

@ -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;