Skip to repository content291 lines · 10.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:43:34.715Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
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 value of an entry in `command_aliases`, either a registered action name
11/// or an arbitrary string, for example, a vim command like `"w"` or `"wq"`.
12///
13/// This is a separate type from `ActionName` on purpose, as `ActionName` is
14/// also used by keymap bindings, which only accept registered action names, and
15/// we don't want to weaken those guarantees and accept any arbitrary string.
16#[derive(Serialize, Deserialize, Default, MergeFrom, Clone, Debug, PartialEq)]
17#[serde(transparent)]
18pub struct CommandAliasTarget(String);
19
20impl CommandAliasTarget {
21 pub fn new(name: impl Into<String>) -> Self {
22 Self(name.into())
23 }
24
25 /// Build the runtime schema, an `anyOf` whose entries are the same as the
26 /// ones produced by [`ActionName::build_schema`] plus a final fallback for
27 /// arbitrary strings that aren't registered actions.
28 pub fn build_schema(
29 action_names: &[&str],
30 action_documentation: &HashMap<&str, &str>,
31 deprecations: &HashMap<&str, &str>,
32 deprecation_messages: &HashMap<&str, &str>,
33 ) -> Schema {
34 let action_schema = ActionName::build_schema(
35 action_names.iter().copied(),
36 action_documentation,
37 deprecations,
38 deprecation_messages,
39 );
40
41 let mut alternatives = action_schema
42 .to_value()
43 .get("anyOf")
44 .and_then(Value::as_array)
45 .cloned()
46 .unwrap_or_default();
47
48 let mut exclude = action_names
49 .iter()
50 .map(|name| serde_json::json!({ "const": *name }))
51 .collect::<Vec<Value>>();
52
53 // Since we accept any arbitrary string, besides the list of registered
54 // action names, we'll add any string that uses a `::` pattern to the
55 // list of exclusions, ensuring we display a warning in that case,
56 // otherwise the user might be led into thinking they've typed a valid
57 // action name when that's not the case.
58 exclude.push(serde_json::json!({ "pattern": "::" }));
59
60 alternatives.push(serde_json::json!({
61 "type": "string",
62 "not": { "anyOf": exclude }
63 }));
64
65 json_schema!({ "anyOf": alternatives })
66 }
67}
68
69impl Display for CommandAliasTarget {
70 fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
71 write!(formatter, "{}", self.0)
72 }
73}
74
75impl AsRef<str> for CommandAliasTarget {
76 fn as_ref(&self) -> &str {
77 &self.0
78 }
79}
80
81impl JsonSchema for CommandAliasTarget {
82 fn schema_name() -> Cow<'static, str> {
83 "CommandAliasTarget".into()
84 }
85
86 /// Returns `true` as a placeholder.
87 ///
88 /// The real schema, an `anyOf` of every registered action name with action
89 /// documentation and deprecation metadata, as well as any arbitrary string,
90 /// cannot be produced here because `JsonSchema::json_schema` receives no
91 /// runtime context. It is instead built by call sites that do have access
92 /// to the GPUI action registry using [`CommandAliasTarget::build_schema`].
93 fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
94 json_schema!(true)
95 }
96}
97
98/// The name of a registered GPUI action, serialized as a plain JSON string, for
99/// example, "editor::Cancel"` or `"workspace::CloseActiveItem"`.
100///
101/// This newtype exists so that settings fields like `command_aliases`, or the
102/// keymap file bindings, can request JSON-schema auto completion over the set
103/// of actions known at runtime.
104#[derive(Serialize, Deserialize, Default, MergeFrom, Clone, Debug, PartialEq)]
105#[serde(transparent)]
106pub struct ActionName(String);
107
108/// Small helper function to populate the schema's `deprecationMessage` field with the
109/// provided deprecation message.
110fn add_deprecation(schema: &mut Schema, message: String) {
111 schema.insert("deprecationMessage".into(), Value::String(message));
112}
113
114/// Small helper function to populate the schema's `description` field with the
115/// provided description.
116fn add_description(schema: &mut Schema, description: &str) {
117 schema.insert("description".into(), Value::String(description.to_string()));
118}
119
120impl ActionName {
121 pub fn new(name: impl Into<String>) -> Self {
122 Self(name.into())
123 }
124
125 /// Build the JSON schema to be used for `$defs/ActionName`, basically an
126 /// `anyOf` of all of the available actions with per-action documentation
127 /// and deprecation metadata attached.
128 pub fn build_schema<'a>(
129 action_names: impl IntoIterator<Item = &'a str>,
130 action_documentation: &HashMap<&str, &str>,
131 deprecations: &HashMap<&str, &str>,
132 deprecation_messages: &HashMap<&str, &str>,
133 ) -> Schema {
134 let mut alternatives = Vec::new();
135
136 for action_name in action_names {
137 let mut entry = json_schema!({
138 "type": "string",
139 "const": action_name
140 });
141
142 if let Some(message) = deprecation_messages.get(action_name) {
143 add_deprecation(&mut entry, message.to_string());
144 } else if let Some(new_name) = deprecations.get(action_name) {
145 add_deprecation(&mut entry, format!("Deprecated, use {new_name}"));
146 }
147
148 if let Some(description) = action_documentation.get(action_name) {
149 add_description(&mut entry, description);
150 }
151
152 alternatives.push(entry);
153 }
154
155 json_schema!({ "anyOf": alternatives })
156 }
157}
158
159impl Display for ActionName {
160 fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
161 write!(formatter, "{}", self.0)
162 }
163}
164
165impl AsRef<str> for ActionName {
166 fn as_ref(&self) -> &str {
167 &self.0
168 }
169}
170
171impl JsonSchema for ActionName {
172 /// The name under which this type should be stored in a generator's `$defs`
173 /// map when schemars encounters it during schema generation.
174 /// Keeping it stable as `"ActionName"` lets consumers reference it by
175 /// `#/$defs/ActionName` and lets [`util::schemars::replace_subschema`] look
176 /// it up at runtime to swap in the real schema.
177 fn schema_name() -> Cow<'static, str> {
178 "ActionName".into()
179 }
180
181 /// Returns `true` as a placeholder.
182 ///
183 /// The real schema, an `anyOf` of every registered action name with action
184 /// documentation and deprecation metadata, cannot be produced here because
185 /// `JsonSchema::json_schema` receives no runtime context. It is instead
186 /// built by call sites that do have access to the GPUI action registry
187 /// using [`ActionName::build_schema`].
188 fn json_schema(_: &mut SchemaGenerator) -> Schema {
189 json_schema!(true)
190 }
191}
192
193/// A GPUI action together with its input data, serialized as a two-element JSON
194/// array of the form `["namespace::Name", { ... }]`, for example,
195/// `["pane::ActivateItem", { "index": 0 }]`.
196#[derive(Deserialize, Default)]
197#[serde(transparent)]
198pub struct ActionWithArguments(pub Value);
199
200impl JsonSchema for ActionWithArguments {
201 /// The name under which this type should be stored in a generator's `$defs`
202 /// map when schemars encounters it during schema generation.
203 /// Keeping it stable as `"ActionWithArguments"` lets consumers reference it
204 /// by `#/$defs/ActionWithArguments` and lets
205 /// [`util::schemars::replace_subschema`] look it up at runtime to swap in
206 /// the real schema.
207 fn schema_name() -> Cow<'static, str> {
208 "ActionWithArguments".into()
209 }
210
211 /// Returns `true` as a placeholder.
212 ///
213 /// The real schema, an `anyOf` of every registered action name that
214 /// supports arguments, with action documentation and deprecation metadata,
215 /// cannot be produced here because `JsonSchema::json_schema` receives no
216 /// runtime context. At the time of writing, it is instead built by
217 /// [`KeymapFile::generate_json_schema`], where all of the runtime
218 /// information is available.
219 fn json_schema(_: &mut SchemaGenerator) -> Schema {
220 json_schema!(true)
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn build_schema_produces_anyof_of_consts_per_name() {
230 let mut action_documentation = HashMap::default();
231 let mut deprecations = HashMap::default();
232 let mut deprecation_messages = HashMap::default();
233 action_documentation.insert("editor::Cancel", "Cancel the current operation.");
234 deprecations.insert("workspace::CloseCurrentItem", "workspace::CloseActiveItem");
235 deprecation_messages.insert("editor::Explode", "DO NOT USE!");
236
237 let schema = ActionName::build_schema(
238 [
239 "editor::Cancel",
240 "editor::Explode",
241 "workspace::CloseCurrentItem",
242 "workspace::CloseActiveItem",
243 ],
244 &action_documentation,
245 &deprecations,
246 &deprecation_messages,
247 );
248
249 let value = schema.to_value();
250 let values = value
251 .pointer("/anyOf")
252 .and_then(|v| v.as_array())
253 .expect("anyOf should be present");
254 assert_eq!(values.len(), 4);
255
256 let (name, schema_type, description) = (
257 values[0].get("const").and_then(Value::as_str),
258 values[0].get("type").and_then(Value::as_str),
259 values[0].get("description").and_then(Value::as_str),
260 );
261 assert_eq!(name, Some("editor::Cancel"));
262 assert_eq!(schema_type, Some("string"));
263 assert_eq!(description, Some("Cancel the current operation."));
264
265 let (name, schema_type, message) = (
266 values[1].get("const").and_then(Value::as_str),
267 values[1].get("type").and_then(Value::as_str),
268 values[1].get("deprecationMessage").and_then(Value::as_str),
269 );
270 assert_eq!(name, Some("editor::Explode"));
271 assert_eq!(schema_type, Some("string"));
272 assert_eq!(message, Some("DO NOT USE!"));
273
274 let (name, schema_type, message) = (
275 values[2].get("const").and_then(Value::as_str),
276 values[2].get("type").and_then(Value::as_str),
277 values[2].get("deprecationMessage").and_then(Value::as_str),
278 );
279 assert_eq!(name, Some("workspace::CloseCurrentItem"));
280 assert_eq!(schema_type, Some("string"));
281 assert_eq!(message, Some("Deprecated, use workspace::CloseActiveItem"));
282
283 let (name, schema_type) = (
284 values[3].get("const").and_then(Value::as_str),
285 values[3].get("type").and_then(Value::as_str),
286 );
287 assert_eq!(name, Some("workspace::CloseActiveItem"));
288 assert_eq!(schema_type, Some("string"));
289 }
290}
291