290 lines · 10.7 KB · Rust
RawDownload
use std::borrow::Cow;use std::fmt::{Display, Formatter, Result}; use collections::HashMap;use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};use serde::{Deserialize, Serialize};use serde_json::Value;use settings_macros::MergeFrom; /// The value of an entry in `command_aliases`, either a registered action name/// or an arbitrary string, for example, a vim command like `"w"` or `"wq"`.////// This is a separate type from `ActionName` on purpose, as `ActionName` is/// also used by keymap bindings, which only accept registered action names, and/// we don't want to weaken those guarantees and accept any arbitrary string.#[derive(Serialize, Deserialize, Default, MergeFrom, Clone, Debug, PartialEq)]#[serde(transparent)]pub struct CommandAliasTarget(String); impl CommandAliasTarget {    pub fn new(name: impl Into<String>) -> Self {        Self(name.into())    }     /// Build the runtime schema, an `anyOf` whose entries are the same as the    /// ones produced by [`ActionName::build_schema`] plus a final fallback for    /// arbitrary strings that aren't registered actions.    pub fn build_schema(        action_names: &[&str],        action_documentation: &HashMap<&str, &str>,        deprecations: &HashMap<&str, &str>,        deprecation_messages: &HashMap<&str, &str>,    ) -> Schema {        let action_schema = ActionName::build_schema(            action_names.iter().copied(),            action_documentation,            deprecations,            deprecation_messages,        );         let mut alternatives = action_schema            .to_value()            .get("anyOf")            .and_then(Value::as_array)            .cloned()            .unwrap_or_default();         let mut exclude = action_names            .iter()            .map(|name| serde_json::json!({ "const": *name }))            .collect::<Vec<Value>>();         // Since we accept any arbitrary string, besides the list of registered        // action names, we'll add any string that uses a `::` pattern to the        // list of exclusions, ensuring we display a warning in that case,        // otherwise the user might be led into thinking they've typed a valid        // action name when that's not the case.        exclude.push(serde_json::json!({ "pattern": "::" }));         alternatives.push(serde_json::json!({            "type": "string",            "not": { "anyOf": exclude }        }));         json_schema!({ "anyOf": alternatives })    }} impl Display for CommandAliasTarget {    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {        write!(formatter, "{}", self.0)    }} impl AsRef<str> for CommandAliasTarget {    fn as_ref(&self) -> &str {        &self.0    }} impl JsonSchema for CommandAliasTarget {    fn schema_name() -> Cow<'static, str> {        "CommandAliasTarget".into()    }     /// Returns `true` as a placeholder.    ///    /// The real schema, an `anyOf` of every registered action name with action    /// documentation and deprecation metadata, as well as any arbitrary string,    /// cannot be produced here because `JsonSchema::json_schema` receives no    /// runtime context. It is instead built by call sites that do have access    /// to the GPUI action registry using [`CommandAliasTarget::build_schema`].    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {        json_schema!(true)    }} /// The name of a registered GPUI action, serialized as a plain JSON string, for/// example, "editor::Cancel"` or `"workspace::CloseActiveItem"`.////// This newtype exists so that settings fields like `command_aliases`, or the/// keymap file bindings, can request JSON-schema auto completion over the set/// of actions known at runtime.#[derive(Serialize, Deserialize, Default, MergeFrom, Clone, Debug, PartialEq)]#[serde(transparent)]pub struct ActionName(String); /// Small helper function to populate the schema's `deprecationMessage` field with the/// provided deprecation message.fn add_deprecation(schema: &mut Schema, message: String) {    schema.insert("deprecationMessage".into(), Value::String(message));} /// Small helper function to populate the schema's `description` field with the/// provided description.fn add_description(schema: &mut Schema, description: &str) {    schema.insert("description".into(), Value::String(description.to_string()));} impl ActionName {    pub fn new(name: impl Into<String>) -> Self {        Self(name.into())    }     /// Build the JSON schema to be used for `$defs/ActionName`, basically an    /// `anyOf` of all of the available actions with per-action documentation    /// and deprecation metadata attached.    pub fn build_schema<'a>(        action_names: impl IntoIterator<Item = &'a str>,        action_documentation: &HashMap<&str, &str>,        deprecations: &HashMap<&str, &str>,        deprecation_messages: &HashMap<&str, &str>,    ) -> Schema {        let mut alternatives = Vec::new();         for action_name in action_names {            let mut entry = json_schema!({                "type": "string",                "const": action_name            });             if let Some(message) = deprecation_messages.get(action_name) {                add_deprecation(&mut entry, message.to_string());            } else if let Some(new_name) = deprecations.get(action_name) {                add_deprecation(&mut entry, format!("Deprecated, use {new_name}"));            }             if let Some(description) = action_documentation.get(action_name) {                add_description(&mut entry, description);            }             alternatives.push(entry);        }         json_schema!({ "anyOf": alternatives })    }} impl Display for ActionName {    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {        write!(formatter, "{}", self.0)    }} impl AsRef<str> for ActionName {    fn as_ref(&self) -> &str {        &self.0    }} impl JsonSchema for ActionName {    /// The name under which this type should be stored in a generator's `$defs`    /// map when schemars encounters it during schema generation.    /// Keeping it stable as `"ActionName"` lets consumers reference it by    /// `#/$defs/ActionName` and lets [`util::schemars::replace_subschema`] look    /// it up at runtime to swap in the real schema.    fn schema_name() -> Cow<'static, str> {        "ActionName".into()    }     /// Returns `true` as a placeholder.    ///    /// The real schema, an `anyOf` of every registered action name with action    /// documentation and deprecation metadata, cannot be produced here because    /// `JsonSchema::json_schema` receives no runtime context. It is instead    /// built by call sites that do have access to the GPUI action registry    /// using [`ActionName::build_schema`].    fn json_schema(_: &mut SchemaGenerator) -> Schema {        json_schema!(true)    }} /// A GPUI action together with its input data, serialized as a two-element JSON/// array of the form `["namespace::Name", { ... }]`, for example,/// `["pane::ActivateItem", { "index": 0 }]`.#[derive(Deserialize, Default)]#[serde(transparent)]pub struct ActionWithArguments(pub Value); impl JsonSchema for ActionWithArguments {    /// The name under which this type should be stored in a generator's `$defs`    /// map when schemars encounters it during schema generation.    /// Keeping it stable as `"ActionWithArguments"` lets consumers reference it    /// by `#/$defs/ActionWithArguments` and lets    /// [`util::schemars::replace_subschema`] look it up at runtime to swap in    /// the real schema.    fn schema_name() -> Cow<'static, str> {        "ActionWithArguments".into()    }     /// Returns `true` as a placeholder.    ///    /// The real schema, an `anyOf` of every registered action name that    /// supports arguments, with action documentation and deprecation metadata,    /// cannot be produced here because `JsonSchema::json_schema` receives no    /// runtime context. At the time of writing, it is instead built by    /// [`KeymapFile::generate_json_schema`], where all of the runtime    /// information is available.    fn json_schema(_: &mut SchemaGenerator) -> Schema {        json_schema!(true)    }} #[cfg(test)]mod tests {    use super::*;     #[test]    fn build_schema_produces_anyof_of_consts_per_name() {        let mut action_documentation = HashMap::default();        let mut deprecations = HashMap::default();        let mut deprecation_messages = HashMap::default();        action_documentation.insert("editor::Cancel", "Cancel the current operation.");        deprecations.insert("workspace::CloseCurrentItem", "workspace::CloseActiveItem");        deprecation_messages.insert("editor::Explode", "DO NOT USE!");         let schema = ActionName::build_schema(            [                "editor::Cancel",                "editor::Explode",                "workspace::CloseCurrentItem",                "workspace::CloseActiveItem",            ],            &action_documentation,            &deprecations,            &deprecation_messages,        );         let value = schema.to_value();        let values = value            .pointer("/anyOf")            .and_then(|v| v.as_array())            .expect("anyOf should be present");        assert_eq!(values.len(), 4);         let (name, schema_type, description) = (            values[0].get("const").and_then(Value::as_str),            values[0].get("type").and_then(Value::as_str),            values[0].get("description").and_then(Value::as_str),        );        assert_eq!(name, Some("editor::Cancel"));        assert_eq!(schema_type, Some("string"));        assert_eq!(description, Some("Cancel the current operation."));         let (name, schema_type, message) = (            values[1].get("const").and_then(Value::as_str),            values[1].get("type").and_then(Value::as_str),            values[1].get("deprecationMessage").and_then(Value::as_str),        );        assert_eq!(name, Some("editor::Explode"));        assert_eq!(schema_type, Some("string"));        assert_eq!(message, Some("DO NOT USE!"));         let (name, schema_type, message) = (            values[2].get("const").and_then(Value::as_str),            values[2].get("type").and_then(Value::as_str),            values[2].get("deprecationMessage").and_then(Value::as_str),        );        assert_eq!(name, Some("workspace::CloseCurrentItem"));        assert_eq!(schema_type, Some("string"));        assert_eq!(message, Some("Deprecated, use workspace::CloseActiveItem"));         let (name, schema_type) = (            values[3].get("const").and_then(Value::as_str),            values[3].get("type").and_then(Value::as_str),        );        assert_eq!(name, Some("workspace::CloseActiveItem"));        assert_eq!(schema_type, Some("string"));    }}