diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 21dc144c64..b8c9cff21f 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,10 +1,10 @@ use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use anyhow::{anyhow, Context, Result}; use collections::{BTreeMap, HashMap}; -use gpui::{Action, AppContext, KeyBinding, SharedString}; +use gpui::{Action, AppContext, KeyBinding, NoAction, SharedString}; use schemars::{ gen::{SchemaGenerator, SchemaSettings}, - schema::{ArrayValidation, InstanceType, Metadata, Schema, SchemaObject, SubschemaValidation}, + schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation}, JsonSchema, }; use serde::Deserialize; @@ -161,21 +161,68 @@ impl KeymapFile { Some(input.into()) } - fn add_deprecation_notice(schema_object: &mut SchemaObject, new_name: &SharedString) { + fn add_deprecation(schema_object: &mut SchemaObject, message: String) { 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(), + Value::String(message), ); } + fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) { + add_deprecation(schema_object, format!("Deprecated, use {new_name}")); + } + + fn add_description(schema_object: &mut SchemaObject, description: String) { + schema_object + .metadata + .get_or_insert(Default::default()) + .description = Some(description); + } + let empty_object: SchemaObject = SchemaObject { instance_type: set(InstanceType::Object), ..Default::default() }; - let mut keymap_action_alternatives = Vec::new(); + // This is a workaround for a json-language-server issue where it matches the first + // alternative that matches the value's shape and uses that for documentation. + // + // In the case of the array validations, it would even provide an error saying that the name + // must match the name of the first alternative. + let mut plain_action = SchemaObject { + instance_type: set(InstanceType::String), + const_value: Some(Value::String("".to_owned())), + ..Default::default() + }; + let no_action_message = "No action named this."; + add_description(&mut plain_action, no_action_message.to_owned()); + add_deprecation(&mut plain_action, no_action_message.to_owned()); + let mut matches_action_name = SchemaObject { + const_value: Some(Value::String("".to_owned())), + ..Default::default() + }; + let no_action_message = "No action named this that takes input."; + add_description(&mut matches_action_name, no_action_message.to_owned()); + add_deprecation(&mut matches_action_name, no_action_message.to_owned()); + let action_with_input = SchemaObject { + instance_type: set(InstanceType::Array), + array: set(ArrayValidation { + items: set(vec![ + matches_action_name.into(), + // Accept any value, as we want this to be the preferred match when there is a + // typo in the name. + Schema::Bool(true), + ]), + min_items: Some(2), + max_items: Some(2), + ..Default::default() + }), + ..Default::default() + }; + let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()]; + for (name, action_schema) in action_schemas.iter() { let schema = if let Some(Schema::Object(schema)) = action_schema { Some(schema.clone()) @@ -183,60 +230,61 @@ impl KeymapFile { 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()) + .and_then(|metadata| metadata.description.clone()) }); - let mut matches_action_name = SchemaObject { - const_value: Some(Value::String(name.to_string())), - ..Default::default() + + let deprecation = if name == NoAction.name() { + Some("null") + } else { + deprecations.get(name).map(|new_name| new_name.as_ref()) }; - 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); + add_deprecation_preferred_name(&mut plain_action, new_name); + } + if let Some(description) = description.clone() { + add_description(&mut plain_action, description); } 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. + // Add an alternative for actions with data specified as a [name, data] array. + // + // When a struct with no deserializable fields is added with impl_actions! / + // impl_actions_as! an empty object schema 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(), - ), + let mut matches_action_name = SchemaObject { + const_value: Some(Value::String(name.to_string())), ..Default::default() }; - if let Some(new_name) = deprecation { - add_deprecation_notice(&mut action_with_data, new_name); + if let Some(description) = description.clone() { + add_description(&mut matches_action_name, description.to_string()); } - keymap_action_alternatives.push(action_with_data.into()); + if let Some(new_name) = deprecation { + add_deprecation_preferred_name(&mut matches_action_name, new_name); + } + let action_with_input = SchemaObject { + instance_type: set(InstanceType::Array), + array: set(ArrayValidation { + items: set(vec![matches_action_name.into(), schema.into()]), + min_items: Some(2), + max_items: Some(2), + ..Default::default() + }), + ..Default::default() + }; + keymap_action_alternatives.push(action_with_input.into()); } } }