keymap_ui: Auto complete action arguments (#34785)

Supersedes: #34242

Creates an `ActionArgumentsEditor` that implements the required logic to
have a JSON language server run when editing keybinds so that there is
auto-complete for action arguments.

This is the first time action argument schemas are required by
themselves rather than inlined in the keymap schema. Rather than add all
action schemas to the configuration options we send to the JSON LSP on
startup, this PR implements support for the
`vscode-json-language-server` extension to the LSP whereby the server
will request the client (Zed) to resolve URLs with URI schemes it does
not recognize, in our case `zed://`. This limits the impact on the size
of the configuration options to ~1KB as we send URLs for the language
server to resolve on demand rather than the schema itself. My
understanding is that this is how VSCode handles JSON schemas as well. I
plan to investigate converting the rest of our schema generation logic
to this method in a follow up PR.

Co-Authored-By: Cole <cole@zed.dev>

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle 2025-07-20 15:24:17 -05:00 committed by GitHub
parent ff79b29f38
commit 7c1040bc93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 453 additions and 109 deletions

View file

@ -1,4 +1,5 @@
pub mod clangd_ext;
pub mod json_language_server_ext;
pub mod lsp_ext_command;
pub mod rust_analyzer_ext;
@ -1034,6 +1035,7 @@ impl LocalLspStore {
})
.detach();
json_language_server_ext::register_requests(this.clone(), language_server);
rust_analyzer_ext::register_notifications(this.clone(), language_server);
clangd_ext::register_notifications(this, language_server, adapter);
}

View file

@ -0,0 +1,101 @@
use anyhow::Context as _;
use collections::HashMap;
use gpui::WeakEntity;
use lsp::LanguageServer;
use crate::LspStore;
/// https://github.com/Microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#schema-content-request
///
/// Represents a "JSON language server-specific, non-standardized, extension to the LSP" with which the vscode-json-language-server
/// can request the contents of a schema that is associated with a uri scheme it does not support.
/// In our case, we provide the uris for actions on server startup under the `zed://schemas/action/{normalize_action_name}` scheme.
/// We can then respond to this request with the schema content on demand, thereby greatly reducing the total size of the JSON we send to the server on startup
struct SchemaContentRequest {}
impl lsp::request::Request for SchemaContentRequest {
type Params = Vec<String>;
type Result = String;
const METHOD: &'static str = "vscode/content";
}
pub fn register_requests(_lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
language_server
.on_request::<SchemaContentRequest, _, _>(|params, cx| {
// PERF: Use a cache (`OnceLock`?) to avoid recomputing the action schemas
let mut generator = settings::KeymapFile::action_schema_generator();
let all_schemas = cx.update(|cx| HashMap::from_iter(cx.action_schemas(&mut generator)));
async move {
let all_schemas = all_schemas?;
let Some(uri) = params.get(0) else {
anyhow::bail!("No URI");
};
let normalized_action_name = uri
.strip_prefix("zed://schemas/action/")
.context("Invalid URI")?;
let action_name = denormalize_action_name(normalized_action_name);
let schema = root_schema_from_action_schema(
all_schemas
.get(action_name.as_str())
.and_then(Option::as_ref),
&mut generator,
)
.to_value();
serde_json::to_string(&schema).context("Failed to serialize schema")
}
})
.detach();
}
pub fn normalize_action_name(action_name: &str) -> String {
action_name.replace("::", "__")
}
pub fn denormalize_action_name(action_name: &str) -> String {
action_name.replace("__", "::")
}
pub fn normalized_action_file_name(action_name: &str) -> String {
normalized_action_name_to_file_name(normalize_action_name(action_name))
}
pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String {
normalized_action_name.push_str(".json");
normalized_action_name
}
pub fn url_schema_for_action(action_name: &str) -> serde_json::Value {
let normalized_name = normalize_action_name(action_name);
let file_name = normalized_action_name_to_file_name(normalized_name.clone());
serde_json::json!({
"fileMatch": [file_name],
"url": format!("zed://schemas/action/{}", normalized_name)
})
}
fn root_schema_from_action_schema(
action_schema: Option<&schemars::Schema>,
generator: &mut schemars::SchemaGenerator,
) -> schemars::Schema {
let Some(action_schema) = action_schema else {
return schemars::json_schema!(false);
};
let meta_schema = generator
.settings()
.meta_schema
.as_ref()
.expect("meta_schema should be present in schemars settings")
.to_string();
let defs = generator.definitions();
let mut schema = schemars::json_schema!({
"$schema": meta_schema,
"allowTrailingCommas": true,
"$defs": defs,
});
schema
.ensure_object()
.extend(std::mem::take(action_schema.clone().ensure_object()));
schema
}