Detailed changes
@@ -442,7 +442,7 @@ impl PickerDelegate for CommandPaletteDelegate {
) -> gpui::Task<()> {
let settings = WorkspaceSettings::get_global(cx);
if let Some(alias) = settings.command_aliases.get(&query) {
- query = alias.to_string();
+ query = alias.as_ref().to_owned();
}
let workspace = self.workspace.clone();
@@ -3,7 +3,7 @@ use mdbook::BookItem;
use mdbook::book::{Book, Chapter};
use mdbook::preprocess::CmdPreprocessor;
use regex::Regex;
-use settings::{KeymapFile, SettingsStore};
+use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::io::{self, Read};
@@ -369,7 +369,18 @@ fn find_binding_with_overlay(
}
fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
- let settings_schema = SettingsStore::json_schema(&Default::default());
+ let params = SettingsJsonSchemaParams {
+ language_names: &[],
+ font_names: &[],
+ theme_names: &[],
+ icon_theme_names: &[],
+ lsp_adapter_names: &[],
+ action_names: &[],
+ action_documentation: &HashMap::default(),
+ deprecations: &HashMap::default(),
+ deprecation_messages: &HashMap::default(),
+ };
+ let settings_schema = SettingsStore::json_schema(¶ms);
let settings_validator = jsonschema::validator_for(&settings_schema)
.expect("failed to compile settings JSON schema");
@@ -352,6 +352,11 @@ async fn resolve_dynamic_schema(
let icon_theme_names = icon_theme_names.as_slice();
let theme_names = theme_names.as_slice();
+ let action_names = cx.all_action_names();
+ let action_documentation = cx.action_documentation();
+ let deprecations = cx.deprecated_actions_to_preferred_actions();
+ let deprecation_messages = cx.action_deprecation_messages();
+
let mut schema =
settings::SettingsStore::json_schema(&settings::SettingsJsonSchemaParams {
language_names,
@@ -359,6 +364,10 @@ async fn resolve_dynamic_schema(
theme_names,
icon_theme_names,
lsp_adapter_names: &lsp_adapter_names,
+ action_names,
+ action_documentation,
+ deprecations,
+ deprecation_messages,
});
inject_feature_flags_schema(&mut schema);
schema
@@ -387,6 +396,10 @@ async fn resolve_dynamic_schema(
font_names: &[],
theme_names: &[],
icon_theme_names: &[],
+ action_names: &[],
+ action_documentation: &HashMap::default(),
+ deprecations: &HashMap::default(),
+ deprecation_messages: &HashMap::default(),
});
inject_feature_flags_schema(&mut schema);
schema
@@ -19,6 +19,7 @@ use util::{
};
use crate::SettingsAssets;
+use settings_content::{ActionName, ActionWithArguments};
use settings_json::{
append_top_level_array_value_in_json_text, parse_json_with_comments,
replace_top_level_array_value_in_json_text,
@@ -698,10 +699,17 @@ impl KeymapFile {
"minItems": 2,
"maxItems": 2
});
- let mut keymap_action_alternatives = vec![
- empty_action_name.clone(),
- empty_action_name_with_input.clone(),
- ];
+
+ let mut keymap_deprecations = deprecations.clone();
+ keymap_deprecations.insert(NoAction.name(), "null");
+ let action_name_schema = ActionName::build_schema(
+ action_schemas.iter().map(|(name, _)| *name),
+ action_documentation,
+ &keymap_deprecations,
+ deprecation_messages,
+ );
+
+ let mut action_with_arguments_alternatives = vec![empty_action_name_with_input.clone()];
let mut unbind_target_action_alternatives =
vec![empty_action_name, empty_action_name_with_input];
@@ -731,7 +739,6 @@ impl KeymapFile {
if let Some(description) = &description {
add_description(&mut plain_action, description);
}
- keymap_action_alternatives.push(plain_action.clone());
if include_in_unbind_target_schema {
unbind_target_action_alternatives.push(plain_action);
}
@@ -760,7 +767,7 @@ impl KeymapFile {
"minItems": 2,
"maxItems": 2
});
- keymap_action_alternatives.push(action_with_input.clone());
+ action_with_arguments_alternatives.push(action_with_input.clone());
if include_in_unbind_target_schema {
unbind_target_action_alternatives.push(action_with_input);
}
@@ -789,7 +796,7 @@ impl KeymapFile {
"This action does not take input - just the action name string should be used."
.to_string(),
);
- keymap_action_alternatives.push(actions_with_empty_input);
+ action_with_arguments_alternatives.push(actions_with_empty_input);
}
if !empty_schema_unbind_target_action_names.is_empty() {
@@ -812,17 +819,22 @@ impl KeymapFile {
unbind_target_action_alternatives.push(actions_with_empty_input);
}
- // Placing null first causes json-language-server to default assuming actions should be
- // null, so place it last.
- keymap_action_alternatives.push(json_schema!({
- "type": "null"
- }));
+ generator.definitions_mut().insert(
+ ActionName::schema_name().to_string(),
+ action_name_schema.to_value(),
+ );
+ generator.definitions_mut().insert(
+ ActionWithArguments::schema_name().to_string(),
+ json!({ "anyOf": action_with_arguments_alternatives }),
+ );
generator.definitions_mut().insert(
KeymapAction::schema_name().to_string(),
- json!({
- "anyOf": keymap_action_alternatives
- }),
+ json!({ "anyOf": [
+ { "$ref": format!("#/$defs/{}", ActionName::schema_name().to_string()) },
+ { "$ref": format!("#/$defs/{}", ActionWithArguments::schema_name().to_string()) },
+ { "type": "null" }
+ ] }),
);
generator.definitions_mut().insert(
UnbindTargetAction::schema_name().to_string(),
@@ -13,7 +13,7 @@ use gpui::{
use paths::{local_settings_file_relative_path, task_file_name};
use schemars::{JsonSchema, json_schema};
use serde_json::Value;
-use settings_content::ParseStatus;
+use settings_content::{ActionName, ParseStatus};
use std::{
any::{Any, TypeId, type_name},
fmt::Debug,
@@ -272,13 +272,16 @@ pub trait AnySettingValue: 'static + Send + Sync {
}
/// Parameters that are used when generating some JSON schemas at runtime.
-#[derive(Default)]
pub struct SettingsJsonSchemaParams<'a> {
pub language_names: &'a [String],
pub font_names: &'a [String],
pub theme_names: &'a [SharedString],
pub icon_theme_names: &'a [SharedString],
pub lsp_adapter_names: &'a [String],
+ pub action_names: &'a [&'a str],
+ pub action_documentation: &'a HashMap<&'a str, &'a str>,
+ pub deprecations: &'a HashMap<&'a str, &'a str>,
+ pub deprecation_messages: &'a HashMap<&'a str, &'a str>,
}
impl SettingsStore {
@@ -1263,6 +1266,17 @@ impl SettingsStore {
});
}
+ if !params.action_names.is_empty() {
+ replace_subschema::<ActionName>(&mut generator, || {
+ ActionName::build_schema(
+ params.action_names.iter().copied(),
+ params.action_documentation,
+ params.deprecations,
+ params.deprecation_messages,
+ )
+ });
+ }
+
generator
.root_schema_for::<UserSettingsContent>()
.to_value()
@@ -2738,6 +2752,10 @@ mod tests {
"rust-analyzer".to_string(),
"typescript-language-server".to_string(),
],
+ action_names: &[],
+ action_documentation: &HashMap::default(),
+ deprecations: &HashMap::default(),
+ deprecation_messages: &HashMap::default(),
});
let properties = schema
@@ -2789,6 +2807,10 @@ mod tests {
"rust-analyzer".to_string(),
"typescript-language-server".to_string(),
],
+ action_names: &[],
+ action_documentation: &HashMap::default(),
+ deprecations: &HashMap::default(),
+ deprecation_messages: &HashMap::default(),
});
let properties = schema
@@ -2837,6 +2859,10 @@ mod tests {
theme_names: &["One Dark".into()],
icon_theme_names: &["Zed Icons".into()],
lsp_adapter_names: &["rust-analyzer".to_string()],
+ action_names: &[],
+ action_documentation: &HashMap::default(),
+ deprecations: &HashMap::default(),
+ deprecation_messages: &HashMap::default(),
};
let user_schema = SettingsStore::json_schema(¶ms);
@@ -0,0 +1,202 @@
+use std::borrow::Cow;
+use std::fmt::{Display, Formatter, Result};
+
+use collections::HashMap;
+use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use settings_macros::MergeFrom;
+
+/// The name of a registered GPUI action, serialized as a plain JSON string, for
+/// example, "editor::Cancel"` or `"workspace::CloseActiveItem"`.
+///
+/// This newtype exists so that settings fields like `command_aliases`, or the
+/// keymap file bindings, can request JSON-schema auto completion over the set
+/// of actions known at runtime.
+#[derive(Serialize, Deserialize, Default, MergeFrom, Clone, Debug, PartialEq)]
+#[serde(transparent)]
+pub struct ActionName(String);
+
+/// Small helper function to populate the schema's `deprecationMessage` field with the
+/// provided deprecation message.
+fn add_deprecation(schema: &mut Schema, message: String) {
+ schema.insert("deprecationMessage".into(), Value::String(message));
+}
+
+/// Small helper function to populate the schema's `description` field with the
+/// provided description.
+fn add_description(schema: &mut Schema, description: &str) {
+ schema.insert("description".into(), Value::String(description.to_string()));
+}
+
+impl ActionName {
+ pub fn new(name: impl Into<String>) -> Self {
+ Self(name.into())
+ }
+
+ /// Build the JSON schema to be used for `$defs/ActionName`, basically an
+ /// `anyOf` of all of the available actions with per-action documentation
+ /// and deprecation metadata attached.
+ pub fn build_schema<'a>(
+ action_names: impl IntoIterator<Item = &'a str>,
+ action_documentation: &HashMap<&str, &str>,
+ deprecations: &HashMap<&str, &str>,
+ deprecation_messages: &HashMap<&str, &str>,
+ ) -> Schema {
+ let mut alternatives = Vec::new();
+
+ for action_name in action_names {
+ let mut entry = json_schema!({
+ "type": "string",
+ "const": action_name
+ });
+
+ if let Some(message) = deprecation_messages.get(action_name) {
+ add_deprecation(&mut entry, message.to_string());
+ } else if let Some(new_name) = deprecations.get(action_name) {
+ add_deprecation(&mut entry, format!("Deprecated, use {new_name}"));
+ }
+
+ if let Some(description) = action_documentation.get(action_name) {
+ add_description(&mut entry, description);
+ }
+
+ alternatives.push(entry);
+ }
+
+ json_schema!({ "anyOf": alternatives })
+ }
+}
+
+impl Display for ActionName {
+ fn fmt(&self, formatter: &mut Formatter<'_>) -> Result {
+ write!(formatter, "{}", self.0)
+ }
+}
+
+impl AsRef<str> for ActionName {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl JsonSchema for ActionName {
+ /// The name under which this type should be stored in a generator's `$defs`
+ /// map when schemars encounters it during schema generation.
+ /// Keeping it stable as `"ActionName"` lets consumers reference it by
+ /// `#/$defs/ActionName` and lets [`util::schemars::replace_subschema`] look
+ /// it up at runtime to swap in the real schema.
+ fn schema_name() -> Cow<'static, str> {
+ "ActionName".into()
+ }
+
+ /// Returns `true` as a placeholder.
+ ///
+ /// The real schema, an `anyOf` of every registered action name with action
+ /// documentation and deprecation metadata, cannot be produced here because
+ /// `JsonSchema::json_schema` receives no runtime context. It is instead
+ /// built by call sites that do have access to the GPUI action registry
+ /// using [`ActionName::build_schema`].
+ fn json_schema(_: &mut SchemaGenerator) -> Schema {
+ json_schema!(true)
+ }
+}
+
+/// A GPUI action together with its input data, serialized as a two-element JSON
+/// array of the form `["namespace::Name", { ... }]`, for example,
+/// `["pane::ActivateItem", { "index": 0 }]`.
+#[derive(Deserialize, Default)]
+#[serde(transparent)]
+pub struct ActionWithArguments(pub Value);
+
+impl JsonSchema for ActionWithArguments {
+ /// The name under which this type should be stored in a generator's `$defs`
+ /// map when schemars encounters it during schema generation.
+ /// Keeping it stable as `"ActionWithArguments"` lets consumers reference it
+ /// by `#/$defs/ActionWithArguments` and lets
+ /// [`util::schemars::replace_subschema`] look it up at runtime to swap in
+ /// the real schema.
+ fn schema_name() -> Cow<'static, str> {
+ "ActionWithArguments".into()
+ }
+
+ /// Returns `true` as a placeholder.
+ ///
+ /// The real schema, an `anyOf` of every registered action name that
+ /// supports arguments, with action documentation and deprecation metadata,
+ /// cannot be produced here because `JsonSchema::json_schema` receives no
+ /// runtime context. At the time of writing, it is instead built by
+ /// [`KeymapFile::generate_json_schema`], where all of the runtime
+ /// information is available.
+ fn json_schema(_: &mut SchemaGenerator) -> Schema {
+ json_schema!(true)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn build_schema_produces_anyof_of_consts_per_name() {
+ let mut action_documentation = HashMap::default();
+ let mut deprecations = HashMap::default();
+ let mut deprecation_messages = HashMap::default();
+ action_documentation.insert("editor::Cancel", "Cancel the current operation.");
+ deprecations.insert("workspace::CloseCurrentItem", "workspace::CloseActiveItem");
+ deprecation_messages.insert("editor::Explode", "DO NOT USE!");
+
+ let schema = ActionName::build_schema(
+ [
+ "editor::Cancel",
+ "editor::Explode",
+ "workspace::CloseCurrentItem",
+ "workspace::CloseActiveItem",
+ ],
+ &action_documentation,
+ &deprecations,
+ &deprecation_messages,
+ );
+
+ let value = schema.to_value();
+ let values = value
+ .pointer("/anyOf")
+ .and_then(|v| v.as_array())
+ .expect("anyOf should be present");
+ assert_eq!(values.len(), 4);
+
+ let (name, schema_type, description) = (
+ values[0].get("const").and_then(Value::as_str),
+ values[0].get("type").and_then(Value::as_str),
+ values[0].get("description").and_then(Value::as_str),
+ );
+ assert_eq!(name, Some("editor::Cancel"));
+ assert_eq!(schema_type, Some("string"));
+ assert_eq!(description, Some("Cancel the current operation."));
+
+ let (name, schema_type, message) = (
+ values[1].get("const").and_then(Value::as_str),
+ values[1].get("type").and_then(Value::as_str),
+ values[1].get("deprecationMessage").and_then(Value::as_str),
+ );
+ assert_eq!(name, Some("editor::Explode"));
+ assert_eq!(schema_type, Some("string"));
+ assert_eq!(message, Some("DO NOT USE!"));
+
+ let (name, schema_type, message) = (
+ values[2].get("const").and_then(Value::as_str),
+ values[2].get("type").and_then(Value::as_str),
+ values[2].get("deprecationMessage").and_then(Value::as_str),
+ );
+ assert_eq!(name, Some("workspace::CloseCurrentItem"));
+ assert_eq!(schema_type, Some("string"));
+ assert_eq!(message, Some("Deprecated, use workspace::CloseActiveItem"));
+
+ let (name, schema_type) = (
+ values[3].get("const").and_then(Value::as_str),
+ values[3].get("type").and_then(Value::as_str),
+ );
+ assert_eq!(name, Some("workspace::CloseActiveItem"));
+ assert_eq!(schema_type, Some("string"));
+ }
+}
@@ -1,3 +1,4 @@
+mod action;
mod agent;
mod editor;
mod extension;
@@ -12,6 +13,7 @@ mod theme;
mod title_bar;
mod workspace;
+pub use action::{ActionName, ActionWithArguments};
pub use agent::*;
pub use editor::*;
pub use extension::*;
@@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
use settings_macros::{MergeFrom, with_fallible_options};
use crate::{
- CenteredPaddingSettings, DelayMs, DockPosition, DockSide, InactiveOpacity, ShowIndentGuides,
- ShowScrollbar, serialize_optional_f32_with_two_decimal_places,
+ ActionName, CenteredPaddingSettings, DelayMs, DockPosition, DockSide, InactiveOpacity,
+ ShowIndentGuides, ShowScrollbar, serialize_optional_f32_with_two_decimal_places,
};
#[with_fallible_options]
@@ -88,9 +88,9 @@ pub struct WorkspaceSettingsContent {
/// Aliases for the command palette. When you type a key in this map,
/// it will be assumed to equal the value.
///
- /// Default: true
+ /// Default: {}
#[serde(default)]
- pub command_aliases: HashMap<String, String>,
+ pub command_aliases: HashMap<String, ActionName>,
/// Maximum open tabs in a pane. Will not close an unsaved
/// tab. Set to `None` for unlimited tabs.
///
@@ -18,7 +18,7 @@ use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext, px};
use itertools::Itertools;
use language::{CursorShape, Language, LanguageConfig, Point};
pub use neovim_backed_test_context::*;
-use settings::SettingsStore;
+use settings::{ActionName, SettingsStore};
use ui::Pixels;
use util::{path, test::marked_text_ranges};
pub use vim_test_context::*;
@@ -1926,7 +1926,7 @@ async fn test_command_alias(cx: &mut gpui::TestAppContext) {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings(cx, |s| {
let mut aliases = HashMap::default();
- aliases.insert("Q".to_string(), "upper".to_string());
+ aliases.insert("Q".to_string(), ActionName::new("upper"));
s.workspace.command_aliases = aliases
});
});
@@ -4,7 +4,7 @@ use crate::DockPosition;
use collections::HashMap;
use serde::Deserialize;
pub use settings::{
- AutosaveSetting, BottomDockLayout, EncodingDisplayOptions, InactiveOpacity,
+ ActionName, AutosaveSetting, BottomDockLayout, EncodingDisplayOptions, InactiveOpacity,
PaneSplitDirectionHorizontal, PaneSplitDirectionVertical, RegisterSetting,
RestoreOnStartupBehavior, Settings,
};
@@ -25,7 +25,7 @@ pub struct WorkspaceSettings {
pub drop_target_size: f32,
pub use_system_path_prompts: bool,
pub use_system_prompts: bool,
- pub command_aliases: HashMap<String, String>,
+ pub command_aliases: HashMap<String, ActionName>,
pub max_tabs: Option<NonZeroUsize>,
pub when_closing_with_no_tabs: settings::CloseWindowWhenNoItems,
pub on_last_window_closed: settings::OnLastWindowClosed,