action.rs

  1use std::borrow::Cow;
  2use std::fmt::{Display, Formatter, Result};
  3
  4use collections::HashMap;
  5use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
  6use serde::{Deserialize, Serialize};
  7use serde_json::Value;
  8use settings_macros::MergeFrom;
  9
 10/// The name of a registered GPUI action, serialized as a plain JSON string, for
 11/// example, "editor::Cancel"` or `"workspace::CloseActiveItem"`.
 12///
 13/// This newtype exists so that settings fields like `command_aliases`, or the
 14/// keymap file bindings, can request JSON-schema auto completion over the set
 15/// of actions known at runtime.
 16#[derive(Serialize, Deserialize, Default, MergeFrom, Clone, Debug, PartialEq)]
 17#[serde(transparent)]
 18pub struct ActionName(String);
 19
 20/// Small helper function to populate the schema's `deprecationMessage` field with the
 21/// provided deprecation message.
 22fn add_deprecation(schema: &mut Schema, message: String) {
 23    schema.insert("deprecationMessage".into(), Value::String(message));
 24}
 25
 26/// Small helper function to populate the schema's `description` field with the
 27/// provided description.
 28fn add_description(schema: &mut Schema, description: &str) {
 29    schema.insert("description".into(), Value::String(description.to_string()));
 30}
 31
 32impl ActionName {
 33    pub fn new(name: impl Into<String>) -> Self {
 34        Self(name.into())
 35    }
 36
 37    /// Build the JSON schema to be used for `$defs/ActionName`, basically an
 38    /// `anyOf` of all of the available actions with per-action documentation
 39    /// and deprecation metadata attached.
 40    pub fn build_schema<'a>(
 41        action_names: impl IntoIterator<Item = &'a str>,
 42        action_documentation: &HashMap<&str, &str>,
 43        deprecations: &HashMap<&str, &str>,
 44        deprecation_messages: &HashMap<&str, &str>,
 45    ) -> Schema {
 46        let mut alternatives = Vec::new();
 47
 48        for action_name in action_names {
 49            let mut entry = json_schema!({
 50                "type": "string",
 51                "const": action_name
 52            });
 53
 54            if let Some(message) = deprecation_messages.get(action_name) {
 55                add_deprecation(&mut entry, message.to_string());
 56            } else if let Some(new_name) = deprecations.get(action_name) {
 57                add_deprecation(&mut entry, format!("Deprecated, use {new_name}"));
 58            }
 59
 60            if let Some(description) = action_documentation.get(action_name) {
 61                add_description(&mut entry, description);
 62            }
 63
 64            alternatives.push(entry);
 65        }
 66
 67        json_schema!({ "anyOf": alternatives })
 68    }
 69}
 70
 71impl Display for ActionName {
 72    fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
 73        write!(formatter, "{}", self.0)
 74    }
 75}
 76
 77impl AsRef<str> for ActionName {
 78    fn as_ref(&self) -> &str {
 79        &self.0
 80    }
 81}
 82
 83impl JsonSchema for ActionName {
 84    /// The name under which this type should be stored in a generator's `$defs`
 85    /// map when schemars encounters it during schema generation.
 86    /// Keeping it stable as `"ActionName"` lets consumers reference it by
 87    /// `#/$defs/ActionName` and lets [`util::schemars::replace_subschema`] look
 88    /// it up at runtime to swap in the real schema.
 89    fn schema_name() -> Cow<'static, str> {
 90        "ActionName".into()
 91    }
 92
 93    /// Returns `true` as a placeholder.
 94    ///
 95    /// The real schema, an `anyOf` of every registered action name with action
 96    /// documentation and deprecation metadata, cannot be produced here because
 97    /// `JsonSchema::json_schema` receives no runtime context. It is instead
 98    /// built by call sites that do have access to the GPUI action registry
 99    /// using [`ActionName::build_schema`].
100    fn json_schema(_: &mut SchemaGenerator) -> Schema {
101        json_schema!(true)
102    }
103}
104
105/// A GPUI action together with its input data, serialized as a two-element JSON
106/// array of the form `["namespace::Name", { ... }]`, for example,
107/// `["pane::ActivateItem", { "index": 0 }]`.
108#[derive(Deserialize, Default)]
109#[serde(transparent)]
110pub struct ActionWithArguments(pub Value);
111
112impl JsonSchema for ActionWithArguments {
113    /// The name under which this type should be stored in a generator's `$defs`
114    /// map when schemars encounters it during schema generation.
115    /// Keeping it stable as `"ActionWithArguments"` lets consumers reference it
116    /// by `#/$defs/ActionWithArguments` and lets
117    /// [`util::schemars::replace_subschema`] look it up at runtime to swap in
118    /// the real schema.
119    fn schema_name() -> Cow<'static, str> {
120        "ActionWithArguments".into()
121    }
122
123    /// Returns `true` as a placeholder.
124    ///
125    /// The real schema, an `anyOf` of every registered action name that
126    /// supports arguments, with action documentation and deprecation metadata,
127    /// cannot be produced here because `JsonSchema::json_schema` receives no
128    /// runtime context. At the time of writing, it is instead built by
129    /// [`KeymapFile::generate_json_schema`], where all of the runtime
130    /// information is available.
131    fn json_schema(_: &mut SchemaGenerator) -> Schema {
132        json_schema!(true)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn build_schema_produces_anyof_of_consts_per_name() {
142        let mut action_documentation = HashMap::default();
143        let mut deprecations = HashMap::default();
144        let mut deprecation_messages = HashMap::default();
145        action_documentation.insert("editor::Cancel", "Cancel the current operation.");
146        deprecations.insert("workspace::CloseCurrentItem", "workspace::CloseActiveItem");
147        deprecation_messages.insert("editor::Explode", "DO NOT USE!");
148
149        let schema = ActionName::build_schema(
150            [
151                "editor::Cancel",
152                "editor::Explode",
153                "workspace::CloseCurrentItem",
154                "workspace::CloseActiveItem",
155            ],
156            &action_documentation,
157            &deprecations,
158            &deprecation_messages,
159        );
160
161        let value = schema.to_value();
162        let values = value
163            .pointer("/anyOf")
164            .and_then(|v| v.as_array())
165            .expect("anyOf should be present");
166        assert_eq!(values.len(), 4);
167
168        let (name, schema_type, description) = (
169            values[0].get("const").and_then(Value::as_str),
170            values[0].get("type").and_then(Value::as_str),
171            values[0].get("description").and_then(Value::as_str),
172        );
173        assert_eq!(name, Some("editor::Cancel"));
174        assert_eq!(schema_type, Some("string"));
175        assert_eq!(description, Some("Cancel the current operation."));
176
177        let (name, schema_type, message) = (
178            values[1].get("const").and_then(Value::as_str),
179            values[1].get("type").and_then(Value::as_str),
180            values[1].get("deprecationMessage").and_then(Value::as_str),
181        );
182        assert_eq!(name, Some("editor::Explode"));
183        assert_eq!(schema_type, Some("string"));
184        assert_eq!(message, Some("DO NOT USE!"));
185
186        let (name, schema_type, message) = (
187            values[2].get("const").and_then(Value::as_str),
188            values[2].get("type").and_then(Value::as_str),
189            values[2].get("deprecationMessage").and_then(Value::as_str),
190        );
191        assert_eq!(name, Some("workspace::CloseCurrentItem"));
192        assert_eq!(schema_type, Some("string"));
193        assert_eq!(message, Some("Deprecated, use workspace::CloseActiveItem"));
194
195        let (name, schema_type) = (
196            values[3].get("const").and_then(Value::as_str),
197            values[3].get("type").and_then(Value::as_str),
198        );
199        assert_eq!(name, Some("workspace::CloseActiveItem"));
200        assert_eq!(schema_type, Some("string"));
201    }
202}