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}