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:
parent
4c50201036
commit
6aba3950d2
37 changed files with 506 additions and 283 deletions
|
@ -1,11 +1,11 @@
|
|||
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::BTreeMap;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use gpui::{Action, AppContext, KeyBinding, SharedString};
|
||||
use schemars::{
|
||||
gen::{SchemaGenerator, SchemaSettings},
|
||||
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
|
||||
JsonSchema, Map,
|
||||
gen::SchemaGenerator,
|
||||
schema::{ArrayValidation, InstanceType, Metadata, Schema, SchemaObject, SubschemaValidation},
|
||||
JsonSchema,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
@ -140,55 +140,117 @@ impl KeymapFile {
|
|||
}
|
||||
|
||||
pub fn generate_json_schema(
|
||||
action_names: &[SharedString],
|
||||
deprecations: &[(SharedString, SharedString)],
|
||||
generator: SchemaGenerator,
|
||||
action_schemas: Vec<(SharedString, Option<Schema>)>,
|
||||
deprecations: &HashMap<SharedString, SharedString>,
|
||||
) -> serde_json::Value {
|
||||
let mut root_schema = SchemaSettings::draft07()
|
||||
.with(|settings| settings.option_add_null_type = false)
|
||||
.into_generator()
|
||||
.into_root_schema_for::<KeymapFile>();
|
||||
|
||||
let mut alternatives = vec![
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||
enum_values: Some(
|
||||
action_names
|
||||
.iter()
|
||||
.map(|name| Value::String(name.to_string()))
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
|
||||
..Default::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
|
||||
..Default::default()
|
||||
}),
|
||||
];
|
||||
for (old, new) in deprecations {
|
||||
alternatives.push(Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||
const_value: Some(Value::String(old.to_string())),
|
||||
extensions: Map::from_iter([(
|
||||
// deprecationMessage is not part of the JSON Schema spec,
|
||||
// but json-language-server recognizes it.
|
||||
"deprecationMessage".to_owned(),
|
||||
format!("Deprecated, use {new}").into(),
|
||||
)]),
|
||||
..Default::default()
|
||||
}));
|
||||
fn set<I, O>(input: I) -> Option<O>
|
||||
where
|
||||
I: Into<O>,
|
||||
{
|
||||
Some(input.into())
|
||||
}
|
||||
let action_schema = Schema::Object(SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
one_of: Some(alternatives),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
fn add_deprecation_notice(schema_object: &mut SchemaObject, new_name: &SharedString) {
|
||||
schema_object.extensions.insert(
|
||||
// deprecationMessage is not part of the JSON Schema spec,
|
||||
// but json-language-server recognizes it.
|
||||
"deprecationMessage".to_owned(),
|
||||
format!("Deprecated, use {new_name}").into(),
|
||||
);
|
||||
}
|
||||
|
||||
let empty_object: SchemaObject = SchemaObject {
|
||||
instance_type: set(InstanceType::Object),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut keymap_action_alternatives = Vec::new();
|
||||
for (name, action_schema) in action_schemas.iter() {
|
||||
let schema = if let Some(Schema::Object(schema)) = action_schema {
|
||||
Some(schema.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// If the type has a description, also apply it to the value. Ideally it would be
|
||||
// removed and applied to the overall array, but `json-language-server` does not show
|
||||
// these descriptions.
|
||||
let description = schema.as_ref().and_then(|schema| {
|
||||
schema
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.description.as_ref())
|
||||
});
|
||||
let mut matches_action_name = SchemaObject {
|
||||
const_value: Some(Value::String(name.to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(description) = description {
|
||||
matches_action_name.metadata = set(Metadata {
|
||||
description: Some(description.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
// Add an alternative for plain action names.
|
||||
let deprecation = deprecations.get(name);
|
||||
let mut plain_action = SchemaObject {
|
||||
instance_type: set(InstanceType::String),
|
||||
const_value: Some(Value::String(name.to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(new_name) = deprecation {
|
||||
add_deprecation_notice(&mut plain_action, new_name);
|
||||
}
|
||||
keymap_action_alternatives.push(plain_action.into());
|
||||
|
||||
// When all fields are skipped or an empty struct is added with impl_actions! /
|
||||
// impl_actions_as! an empty struct is produced. The action should be invoked without
|
||||
// data in this case.
|
||||
if let Some(schema) = schema {
|
||||
if schema != empty_object {
|
||||
let mut action_with_data = SchemaObject {
|
||||
instance_type: set(InstanceType::Array),
|
||||
array: Some(
|
||||
ArrayValidation {
|
||||
items: set(vec![matches_action_name.into(), schema.into()]),
|
||||
min_items: Some(2),
|
||||
max_items: Some(2),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(new_name) = deprecation {
|
||||
add_deprecation_notice(&mut action_with_data, new_name);
|
||||
}
|
||||
keymap_action_alternatives.push(action_with_data.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Placing null first causes json-language-server to default assuming actions should be
|
||||
// null, so place it last.
|
||||
keymap_action_alternatives.push(
|
||||
SchemaObject {
|
||||
instance_type: set(InstanceType::Null),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
let action_schema = SchemaObject {
|
||||
subschemas: set(SubschemaValidation {
|
||||
one_of: Some(keymap_action_alternatives),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into();
|
||||
|
||||
let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
|
||||
root_schema
|
||||
.definitions
|
||||
.insert("KeymapAction".to_owned(), action_schema);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue