From 83449293b67b5b56ba0b02e904649d3eb415ad43 Mon Sep 17 00:00:00 2001 From: Nereuxofficial <37740907+Nereuxofficial@users.noreply.github.com> Date: Sun, 21 Dec 2025 16:29:38 +0100 Subject: [PATCH] Add autocomplete for initialization_options (#43104) Closes #18287 Release Notes: - Added autocomplete for lsp initialization_options ## Description This MR adds the following code-changes: - `initialization_options_schema` to the `LspAdapter` to get JSON Schema's from the language server - Adds a post-processing step to inject schema request paths into the settings schema in `SettingsStore::json_schema` - Adds an implementation for fetching the schema for rust-analyzer which fetches it from the binary it is provided with - Similarly for ruff image ## Open Questions(Would be nice to get some advice here) - Binary Fetching: - I'm pretty sure the binary fetching is suboptimal. The main problem here was getting access to the delegate but i figured that out eventually in a way that i _hope_ should be fine. - The toolchain and binary options can differ from what the user has configured potentially leading to mismatches in the autocomplete values returned(these are probably rarely changed though). I could not really find a way to fetch these in this context so the provided ones are for now just `default` values. - For the trait API it is just provided a binary, since i wanted to use the potentially cached binary from the CachedLspAdapter. Is that fine our should the arguments be passed to the LspAdapter such that it can potentially download the LSP? - As for those LSPs with JSON schema files in their repositories i can add the files to zed manually e.g. in languages/language/initialization_options_schema.json, which could cause mismatches with the actual binary. Is there a preferred approach for Zed here also with regards to updating them? --- Cargo.lock | 1 + crates/editor/src/editor_tests.rs | 8 +- crates/json_schema_store/Cargo.toml | 1 + .../src/json_schema_store.rs | 150 ++++++--- crates/language/src/language.rs | 8 + crates/languages/src/python.rs | 289 ++++++++++++++++++ crates/languages/src/rust.rs | 180 +++++++++++ crates/languages/src/vtsls.rs | 1 + crates/project/src/lsp_store.rs | 4 +- .../src/lsp_store/json_language_server_ext.rs | 10 +- crates/settings/src/settings.rs | 5 +- .../settings/src/settings_content/project.rs | 15 +- crates/settings/src/settings_store.rs | 81 ++++- crates/zed/src/main.rs | 9 +- 14 files changed, 708 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f12d6a3484907a873e0e02d8e7af333c31dd9740..e71461d72b0894c0049a90d2f65edbd68a477648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8645,6 +8645,7 @@ dependencies = [ "extension", "gpui", "language", + "lsp", "paths", "project", "schemars", diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 613850428a8720ed37efa447a1312c262a05571a..6d53c59d90a8df5d74d0879e965f7f2643deaefa 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -18346,7 +18346,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon ); update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( + project_settings.lsp.0.insert( "Some other server name".into(), LspSettings { binary: None, @@ -18367,7 +18367,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon ); update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( + project_settings.lsp.0.insert( language_server_name.into(), LspSettings { binary: None, @@ -18388,7 +18388,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon ); update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( + project_settings.lsp.0.insert( language_server_name.into(), LspSettings { binary: None, @@ -18409,7 +18409,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon ); update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( + project_settings.lsp.0.insert( language_server_name.into(), LspSettings { binary: None, diff --git a/crates/json_schema_store/Cargo.toml b/crates/json_schema_store/Cargo.toml index efb1b36e7978805ec9c5a07baf9339f66a9d2f9f..2225b7c5aa68b43d45dacf404c038248ab2c897f 100644 --- a/crates/json_schema_store/Cargo.toml +++ b/crates/json_schema_store/Cargo.toml @@ -20,6 +20,7 @@ dap.workspace = true extension.workspace = true gpui.workspace = true language.workspace = true +lsp.workspace = true paths.workspace = true project.workspace = true schemars.workspace = true diff --git a/crates/json_schema_store/src/json_schema_store.rs b/crates/json_schema_store/src/json_schema_store.rs index 18041545ccd404eef0035b9b50ff8244d212fa0b..a6d07a6ac368ed47c6ecf215efe5560727dadf05 100644 --- a/crates/json_schema_store/src/json_schema_store.rs +++ b/crates/json_schema_store/src/json_schema_store.rs @@ -2,9 +2,11 @@ use std::{str::FromStr, sync::Arc}; use anyhow::{Context as _, Result}; -use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, WeakEntity}; +use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, Task, WeakEntity}; use language::{LanguageRegistry, language_settings::all_language_settings}; -use project::LspStore; +use lsp::LanguageServerBinaryOptions; +use project::{LspStore, lsp_store::LocalLspAdapterDelegate}; +use settings::LSP_SETTINGS_SCHEMA_URL_PREFIX; use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields}; // Origin: https://github.com/SchemaStore/schemastore @@ -75,23 +77,28 @@ fn handle_schema_request( lsp_store: Entity, uri: String, cx: &mut AsyncApp, -) -> Result { - let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone())?; - let schema = resolve_schema_request(&languages, uri, cx)?; - serde_json::to_string(&schema).context("Failed to serialize schema") +) -> Task> { + let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone()); + cx.spawn(async move |cx| { + let languages = languages?; + let schema = resolve_schema_request(&languages, lsp_store, uri, cx).await?; + serde_json::to_string(&schema).context("Failed to serialize schema") + }) } -pub fn resolve_schema_request( +pub async fn resolve_schema_request( languages: &Arc, + lsp_store: Entity, uri: String, cx: &mut AsyncApp, ) -> Result { let path = uri.strip_prefix("zed://schemas/").context("Invalid URI")?; - resolve_schema_request_inner(languages, path, cx) + resolve_schema_request_inner(languages, lsp_store, path, cx).await } -pub fn resolve_schema_request_inner( +pub async fn resolve_schema_request_inner( languages: &Arc, + lsp_store: Entity, path: &str, cx: &mut AsyncApp, ) -> Result { @@ -99,37 +106,106 @@ pub fn resolve_schema_request_inner( let schema_name = schema_name.unwrap_or(path); let schema = match schema_name { - "settings" => cx.update(|cx| { - let font_names = &cx.text_system().all_font_names(); - let language_names = &languages - .language_names() + "settings" if rest.is_some_and(|r| r.starts_with("lsp/")) => { + let lsp_name = rest + .and_then(|r| { + r.strip_prefix( + LSP_SETTINGS_SCHEMA_URL_PREFIX + .strip_prefix("zed://schemas/settings/") + .unwrap(), + ) + }) + .context("Invalid LSP schema path")?; + + let adapter = languages + .all_lsp_adapters() .into_iter() - .map(|name| name.to_string()) + .find(|adapter| adapter.name().as_ref() as &str == lsp_name) + .with_context(|| format!("LSP adapter not found: {}", lsp_name))?; + + let delegate = cx.update(|inner_cx| { + lsp_store.update(inner_cx, |lsp_store, inner_cx| { + let Some(local) = lsp_store.as_local() else { + return None; + }; + let Some(worktree) = local.worktree_store.read(inner_cx).worktrees().next() else { + return None; + }; + Some(LocalLspAdapterDelegate::from_local_lsp( + local, &worktree, inner_cx, + )) + }) + })?.context("Failed to create adapter delegate - either LSP store is not in local mode or no worktree is available")?; + + let adapter_for_schema = adapter.clone(); + + let binary = adapter + .get_language_server_command( + delegate, + None, + LanguageServerBinaryOptions { + allow_path_lookup: true, + allow_binary_download: false, + pre_release: false, + }, + cx, + ) + .await + .await + .0.with_context(|| format!("Failed to find language server {lsp_name} to generate initialization params schema"))?; + + adapter_for_schema + .adapter + .clone() + .initialization_options_schema(&binary) + .await + .unwrap_or_else(|| { + serde_json::json!({ + "type": "object", + "additionalProperties": true + }) + }) + } + "settings" => { + let lsp_adapter_names = languages + .all_lsp_adapters() + .into_iter() + .map(|adapter| adapter.name().to_string()) .collect::>(); - let mut icon_theme_names = vec![]; - let mut theme_names = vec![]; - if let Some(registry) = theme::ThemeRegistry::try_global(cx) { - icon_theme_names.extend( - registry - .list_icon_themes() - .into_iter() - .map(|icon_theme| icon_theme.name), - ); - theme_names.extend(registry.list_names()); - } - let icon_theme_names = icon_theme_names.as_slice(); - let theme_names = theme_names.as_slice(); - - cx.global::().json_schema( - &settings::SettingsJsonSchemaParams { - language_names, - font_names, - theme_names, - icon_theme_names, - }, - ) - })?, + cx.update(|cx| { + let font_names = &cx.text_system().all_font_names(); + let language_names = &languages + .language_names() + .into_iter() + .map(|name| name.to_string()) + .collect::>(); + + let mut icon_theme_names = vec![]; + let mut theme_names = vec![]; + if let Some(registry) = theme::ThemeRegistry::try_global(cx) { + icon_theme_names.extend( + registry + .list_icon_themes() + .into_iter() + .map(|icon_theme| icon_theme.name), + ); + theme_names.extend(registry.list_names()); + } + let icon_theme_names = icon_theme_names.as_slice(); + let theme_names = theme_names.as_slice(); + + cx.global::().json_schema( + &settings::SettingsJsonSchemaParams { + language_names, + font_names, + theme_names, + icon_theme_names, + lsp_adapter_names: &lsp_adapter_names, + }, + ) + })? + } "keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions)?, "action" => { let normalized_action_name = rest.context("No Action name provided")?; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 290cad4e4497015ef63f79e58a0dacf231168c9f..70ddd01d3a3bc4d76b766a73f46cb7a684ac2f91 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -461,6 +461,14 @@ pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller { Ok(None) } + /// Returns the JSON schema of the initialization_options for the language server. + async fn initialization_options_schema( + self: Arc, + _language_server_binary: &LanguageServerBinary, + ) -> Option { + None + } + async fn workspace_configuration( self: Arc, _: &Arc, diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index a06b1efe649b93ef56a35c40bd0d35cd1bc7ca9c..40825e30cbe79c8e2c48772a40804dda60948894 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -26,6 +26,7 @@ use settings::Settings; use smol::lock::OnceCell; use std::cmp::{Ordering, Reverse}; use std::env::consts; +use std::process::Stdio; use terminal::terminal_settings::TerminalSettings; use util::command::new_smol_command; use util::fs::{make_file_executable, remove_matching}; @@ -2173,6 +2174,119 @@ pub(crate) struct RuffLspAdapter { fs: Arc, } +impl RuffLspAdapter { + fn convert_ruff_schema(raw_schema: &serde_json::Value) -> serde_json::Value { + let Some(schema_object) = raw_schema.as_object() else { + return raw_schema.clone(); + }; + + let mut root_properties = serde_json::Map::new(); + + for (key, value) in schema_object { + let parts: Vec<&str> = key.split('.').collect(); + + if parts.is_empty() { + continue; + } + + let mut current = &mut root_properties; + + for (i, part) in parts.iter().enumerate() { + let is_last = i == parts.len() - 1; + + if is_last { + let mut schema_entry = serde_json::Map::new(); + + if let Some(doc) = value.get("doc").and_then(|d| d.as_str()) { + schema_entry.insert( + "markdownDescription".to_string(), + serde_json::Value::String(doc.to_string()), + ); + } + + if let Some(default_val) = value.get("default") { + schema_entry.insert("default".to_string(), default_val.clone()); + } + + if let Some(value_type) = value.get("value_type").and_then(|v| v.as_str()) { + if value_type.contains('|') { + let enum_values: Vec = value_type + .split('|') + .map(|s| s.trim().trim_matches('"')) + .filter(|s| !s.is_empty()) + .map(|s| serde_json::Value::String(s.to_string())) + .collect(); + + if !enum_values.is_empty() { + schema_entry + .insert("type".to_string(), serde_json::json!("string")); + schema_entry.insert( + "enum".to_string(), + serde_json::Value::Array(enum_values), + ); + } + } else if value_type.starts_with("list[") { + schema_entry.insert("type".to_string(), serde_json::json!("array")); + if let Some(item_type) = value_type + .strip_prefix("list[") + .and_then(|s| s.strip_suffix(']')) + { + let json_type = match item_type { + "str" => "string", + "int" => "integer", + "bool" => "boolean", + _ => "string", + }; + schema_entry.insert( + "items".to_string(), + serde_json::json!({"type": json_type}), + ); + } + } else if value_type.starts_with("dict[") { + schema_entry.insert("type".to_string(), serde_json::json!("object")); + } else { + let json_type = match value_type { + "bool" => "boolean", + "int" | "usize" => "integer", + "str" => "string", + _ => "string", + }; + schema_entry.insert( + "type".to_string(), + serde_json::Value::String(json_type.to_string()), + ); + } + } + + current.insert(part.to_string(), serde_json::Value::Object(schema_entry)); + } else { + let next_current = current + .entry(part.to_string()) + .or_insert_with(|| { + serde_json::json!({ + "type": "object", + "properties": {} + }) + }) + .as_object_mut() + .expect("should be an object") + .entry("properties") + .or_insert_with(|| serde_json::json!({})) + .as_object_mut() + .expect("properties should be an object"); + + current = next_current; + } + } + } + + serde_json::json!({ + "type": "object", + "properties": root_properties + }) + } +} + #[cfg(target_os = "macos")] impl RuffLspAdapter { const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz; @@ -2225,6 +2339,36 @@ impl LspAdapter for RuffLspAdapter { fn name(&self) -> LanguageServerName { Self::SERVER_NAME } + + async fn initialization_options_schema( + self: Arc, + language_server_binary: &LanguageServerBinary, + ) -> Option { + let mut command = util::command::new_smol_command(&language_server_binary.path); + command + .args(&["config", "--output-format", "json"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let cmd = command + .spawn() + .map_err(|e| log::debug!("failed to spawn command {command:?}: {e}")) + .ok()?; + let output = cmd + .output() + .await + .map_err(|e| log::debug!("failed to execute command {command:?}: {e}")) + .ok()?; + if !output.status.success() { + return None; + } + + let raw_schema: serde_json::Value = serde_json::from_slice(output.stdout.as_slice()) + .map_err(|e| log::debug!("failed to parse ruff's JSON schema output: {e}")) + .ok()?; + + let converted_schema = Self::convert_ruff_schema(&raw_schema); + Some(converted_schema) + } } impl LspInstaller for RuffLspAdapter { @@ -2568,4 +2712,149 @@ mod tests { ); } } + + #[test] + fn test_convert_ruff_schema() { + use super::RuffLspAdapter; + + let raw_schema = serde_json::json!({ + "line-length": { + "doc": "The line length to use when enforcing long-lines violations", + "default": "88", + "value_type": "int", + "scope": null, + "example": "line-length = 120", + "deprecated": null + }, + "lint.select": { + "doc": "A list of rule codes or prefixes to enable", + "default": "[\"E4\", \"E7\", \"E9\", \"F\"]", + "value_type": "list[RuleSelector]", + "scope": null, + "example": "select = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]", + "deprecated": null + }, + "lint.isort.case-sensitive": { + "doc": "Sort imports taking into account case sensitivity.", + "default": "false", + "value_type": "bool", + "scope": null, + "example": "case-sensitive = true", + "deprecated": null + }, + "format.quote-style": { + "doc": "Configures the preferred quote character for strings.", + "default": "\"double\"", + "value_type": "\"double\" | \"single\" | \"preserve\"", + "scope": null, + "example": "quote-style = \"single\"", + "deprecated": null + } + }); + + let converted = RuffLspAdapter::convert_ruff_schema(&raw_schema); + + assert!(converted.is_object()); + assert_eq!( + converted.get("type").and_then(|v| v.as_str()), + Some("object") + ); + + let properties = converted + .get("properties") + .expect("should have properties") + .as_object() + .expect("properties should be an object"); + + assert!(properties.contains_key("line-length")); + assert!(properties.contains_key("lint")); + assert!(properties.contains_key("format")); + + let line_length = properties + .get("line-length") + .expect("should have line-length") + .as_object() + .expect("line-length should be an object"); + + assert_eq!( + line_length.get("type").and_then(|v| v.as_str()), + Some("integer") + ); + assert_eq!( + line_length.get("default").and_then(|v| v.as_str()), + Some("88") + ); + + let lint = properties + .get("lint") + .expect("should have lint") + .as_object() + .expect("lint should be an object"); + + let lint_props = lint + .get("properties") + .expect("lint should have properties") + .as_object() + .expect("lint properties should be an object"); + + assert!(lint_props.contains_key("select")); + assert!(lint_props.contains_key("isort")); + + let select = lint_props.get("select").expect("should have select"); + assert_eq!(select.get("type").and_then(|v| v.as_str()), Some("array")); + + let isort = lint_props + .get("isort") + .expect("should have isort") + .as_object() + .expect("isort should be an object"); + + let isort_props = isort + .get("properties") + .expect("isort should have properties") + .as_object() + .expect("isort properties should be an object"); + + let case_sensitive = isort_props + .get("case-sensitive") + .expect("should have case-sensitive"); + + assert_eq!( + case_sensitive.get("type").and_then(|v| v.as_str()), + Some("boolean") + ); + assert!(case_sensitive.get("markdownDescription").is_some()); + + let format = properties + .get("format") + .expect("should have format") + .as_object() + .expect("format should be an object"); + + let format_props = format + .get("properties") + .expect("format should have properties") + .as_object() + .expect("format properties should be an object"); + + let quote_style = format_props + .get("quote-style") + .expect("should have quote-style"); + + assert_eq!( + quote_style.get("type").and_then(|v| v.as_str()), + Some("string") + ); + + let enum_values = quote_style + .get("enum") + .expect("should have enum") + .as_array() + .expect("enum should be an array"); + + assert_eq!(enum_values.len(), 3); + assert!(enum_values.contains(&serde_json::json!("double"))); + assert!(enum_values.contains(&serde_json::json!("single"))); + assert!(enum_values.contains(&serde_json::json!("preserve"))); + } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 80bc48908b0894f251d6631b67cb4a19658454bd..d2b890a1dbe3d4137d9819a55a40ee5e19374add 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -18,6 +18,7 @@ use smol::fs::{self}; use std::cmp::Reverse; use std::fmt::Display; use std::ops::Range; +use std::process::Stdio; use std::{ borrow::Cow, path::{Path, PathBuf}, @@ -66,6 +67,68 @@ enum LibcType { } impl RustLspAdapter { + fn convert_rust_analyzer_schema(raw_schema: &serde_json::Value) -> serde_json::Value { + let Some(schema_array) = raw_schema.as_array() else { + return raw_schema.clone(); + }; + + let mut root_properties = serde_json::Map::new(); + + for item in schema_array { + if let Some(props) = item.get("properties").and_then(|p| p.as_object()) { + for (key, value) in props { + let parts: Vec<&str> = key.split('.').collect(); + + if parts.is_empty() { + continue; + } + + let parts_to_process = if parts.first() == Some(&"rust-analyzer") { + &parts[1..] + } else { + &parts[..] + }; + + if parts_to_process.is_empty() { + continue; + } + + let mut current = &mut root_properties; + + for (i, part) in parts_to_process.iter().enumerate() { + let is_last = i == parts_to_process.len() - 1; + + if is_last { + current.insert(part.to_string(), value.clone()); + } else { + let next_current = current + .entry(part.to_string()) + .or_insert_with(|| { + serde_json::json!({ + "type": "object", + "properties": {} + }) + }) + .as_object_mut() + .expect("should be an object") + .entry("properties") + .or_insert_with(|| serde_json::json!({})) + .as_object_mut() + .expect("properties should be an object"); + + current = next_current; + } + } + } + } + } + + serde_json::json!({ + "type": "object", + "properties": root_properties + }) + } + #[cfg(target_os = "linux")] async fn determine_libc_type() -> LibcType { use futures::pin_mut; @@ -448,6 +511,37 @@ impl LspAdapter for RustLspAdapter { Some(label) } + async fn initialization_options_schema( + self: Arc, + language_server_binary: &LanguageServerBinary, + ) -> Option { + let mut command = util::command::new_smol_command(&language_server_binary.path); + command + .arg("--print-config-schema") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let cmd = command + .spawn() + .map_err(|e| log::debug!("failed to spawn command {command:?}: {e}")) + .ok()?; + let output = cmd + .output() + .await + .map_err(|e| log::debug!("failed to execute command {command:?}: {e}")) + .ok()?; + if !output.status.success() { + return None; + } + + let raw_schema: serde_json::Value = serde_json::from_slice(output.stdout.as_slice()) + .map_err(|e| log::debug!("failed to parse rust-analyzer's JSON schema output: {e}")) + .ok()?; + + // Convert rust-analyzer's array-based schema format to nested JSON Schema + let converted_schema = Self::convert_rust_analyzer_schema(&raw_schema); + Some(converted_schema) + } + async fn label_for_symbol( &self, name: &str, @@ -1912,4 +2006,90 @@ mod tests { ); check([], "/project/src/main.rs", "--"); } + + #[test] + fn test_convert_rust_analyzer_schema() { + let raw_schema = serde_json::json!([ + { + "title": "Assist", + "properties": { + "rust-analyzer.assist.emitMustUse": { + "markdownDescription": "Insert #[must_use] when generating `as_` methods for enum variants.", + "default": false, + "type": "boolean" + } + } + }, + { + "title": "Assist", + "properties": { + "rust-analyzer.assist.expressionFillDefault": { + "markdownDescription": "Placeholder expression to use for missing expressions in assists.", + "default": "todo", + "type": "string" + } + } + }, + { + "title": "Cache Priming", + "properties": { + "rust-analyzer.cachePriming.enable": { + "markdownDescription": "Warm up caches on project load.", + "default": true, + "type": "boolean" + } + } + } + ]); + + let converted = RustLspAdapter::convert_rust_analyzer_schema(&raw_schema); + + assert_eq!( + converted.get("type").and_then(|v| v.as_str()), + Some("object") + ); + + let properties = converted + .pointer("/properties") + .expect("should have properties") + .as_object() + .expect("properties should be object"); + + assert!(properties.contains_key("assist")); + assert!(properties.contains_key("cachePriming")); + assert!(!properties.contains_key("rust-analyzer")); + + let assist_props = properties + .get("assist") + .expect("should have assist") + .pointer("/properties") + .expect("assist should have properties") + .as_object() + .expect("assist properties should be object"); + + assert!(assist_props.contains_key("emitMustUse")); + assert!(assist_props.contains_key("expressionFillDefault")); + + let emit_must_use = assist_props + .get("emitMustUse") + .expect("should have emitMustUse"); + assert_eq!( + emit_must_use.get("type").and_then(|v| v.as_str()), + Some("boolean") + ); + assert_eq!( + emit_must_use.get("default").and_then(|v| v.as_bool()), + Some(false) + ); + + let cache_priming_props = properties + .get("cachePriming") + .expect("should have cachePriming") + .pointer("/properties") + .expect("cachePriming should have properties") + .as_object() + .expect("cachePriming properties should be object"); + + assert!(cache_priming_props.contains_key("enable")); + } } diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 29b21a7cd80f1f0457e7720d68a6fb37954a02c5..7106929c4ad3845d9aca06e0c5206a5d1de9b02c 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -345,6 +345,7 @@ impl LspAdapter for VtslsLspAdapter { let lsp_settings = content .project .lsp + .0 .entry(VTSLS_SERVER_NAME.into()) .or_default(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 5841be02b2db80b2fa15667833b8a3d3eec4ec11..401879ba9d53fb6df58a5346af3784e2b58d31ea 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -257,7 +257,7 @@ struct DynamicRegistrations { pub struct LocalLspStore { weak: WeakEntity, - worktree_store: Entity, + pub worktree_store: Entity, toolchain_store: Entity, http_client: Arc, environment: Entity, @@ -13953,7 +13953,7 @@ impl LocalLspAdapterDelegate { }) } - fn from_local_lsp( + pub fn from_local_lsp( local: &LocalLspStore, worktree: &Entity, cx: &mut App, diff --git a/crates/project/src/lsp_store/json_language_server_ext.rs b/crates/project/src/lsp_store/json_language_server_ext.rs index 78df7132734e9bf71bac8df176f92e15eec21361..8d03dde032303210c9de7ec30b8144c0a3cb8c99 100644 --- a/crates/project/src/lsp_store/json_language_server_ext.rs +++ b/crates/project/src/lsp_store/json_language_server_ext.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use gpui::{App, AsyncApp, Entity, Global, WeakEntity}; +use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity}; use lsp::LanguageServer; use crate::LspStore; @@ -22,7 +22,7 @@ impl lsp::request::Request for SchemaContentRequest { const METHOD: &'static str = "vscode/content"; } -type SchemaRequestHandler = fn(Entity, String, &mut AsyncApp) -> Result; +type SchemaRequestHandler = fn(Entity, String, &mut AsyncApp) -> Task>; pub struct SchemaHandlingImpl(SchemaRequestHandler); impl Global for SchemaHandlingImpl {} @@ -72,9 +72,7 @@ pub fn notify_schema_changed(lsp_store: Entity, uri: String, cx: &App) pub fn register_requests(lsp_store: WeakEntity, language_server: &LanguageServer) { language_server .on_request::(move |params, cx| { - let handler = cx.try_read_global::(|handler, _| { - handler.0 - }); + let handler = cx.try_read_global::(|handler, _| handler.0); let mut cx = cx.clone(); let uri = params.clone().pop(); let lsp_store = lsp_store.clone(); @@ -82,7 +80,7 @@ pub fn register_requests(lsp_store: WeakEntity, language_server: &Lang let lsp_store = lsp_store.upgrade().context("LSP store has been dropped")?; let uri = uri.context("No URI")?; let handle_schema_request = handler.context("No schema handler registered")?; - handle_schema_request(lsp_store, uri, &mut cx) + handle_schema_request(lsp_store, uri, &mut cx).await }; async move { zlog::trace!(LOGGER => "Handling schema request for {:?}", ¶ms); diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 5f07ebe52f8997a91653063b5c20ca8e7432acc5..3cf33ec40f442ae02b69a8798efe02607da670c4 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -33,8 +33,9 @@ pub use serde_helper::*; pub use settings_file::*; pub use settings_json::*; pub use settings_store::{ - InvalidSettingsError, LocalSettingsKind, MigrationStatus, ParseStatus, Settings, SettingsFile, - SettingsJsonSchemaParams, SettingsKey, SettingsLocation, SettingsParseResult, SettingsStore, + InvalidSettingsError, LSP_SETTINGS_SCHEMA_URL_PREFIX, LocalSettingsKind, MigrationStatus, + ParseStatus, Settings, SettingsFile, SettingsJsonSchemaParams, SettingsKey, SettingsLocation, + SettingsParseResult, SettingsStore, }; pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource}; diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index 8e2d864149c9ecb6ca38ca73ef58205f588dc07b..4855d9835bbbfaf3c383ac09fb70066afcf1bcd7 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -11,6 +11,19 @@ use crate::{ SlashCommandSettings, }; +#[with_fallible_options] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] +pub struct LspSettingsMap(pub HashMap, LspSettings>); + +impl IntoIterator for LspSettingsMap { + type Item = (Arc, LspSettings); + type IntoIter = std::collections::hash_map::IntoIter, LspSettings>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + #[with_fallible_options] #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct ProjectSettingsContent { @@ -29,7 +42,7 @@ pub struct ProjectSettingsContent { /// name to the lsp value. /// Default: null #[serde(default)] - pub lsp: HashMap, LspSettings>, + pub lsp: LspSettingsMap, pub terminal: Option, diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index abd45a141647f6ba13708c549188a22988c78069..a9130a5d14de0c07578a8e70ec3299930689dd0f 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -32,7 +32,8 @@ pub type EditorconfigProperties = ec4rs::Properties; use crate::{ ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent, - LanguageToSettingsMap, ThemeName, VsCodeSettings, WorktreeId, fallible_options, + LanguageToSettingsMap, LspSettings, LspSettingsMap, ThemeName, VsCodeSettings, WorktreeId, + fallible_options, merge_from::MergeFrom, settings_content::{ ExtensionsSettingsContent, ProjectSettingsContent, SettingsContent, UserSettingsContent, @@ -41,6 +42,8 @@ use crate::{ use settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text}; +pub const LSP_SETTINGS_SCHEMA_URL_PREFIX: &str = "zed://schemas/settings/lsp/"; + pub trait SettingsKey: 'static + Send + Sync { /// The name of a key within the JSON file from which this setting should /// be deserialized. If this is `None`, then the setting will be deserialized @@ -256,6 +259,7 @@ pub struct SettingsJsonSchemaParams<'a> { pub font_names: &'a [String], pub theme_names: &'a [SharedString], pub icon_theme_names: &'a [SharedString], + pub lsp_adapter_names: &'a [String], } impl SettingsStore { @@ -1025,6 +1029,14 @@ impl SettingsStore { .subschema_for::() .to_value(); + generator.subschema_for::(); + + let lsp_settings_def = generator + .definitions() + .get("LspSettings") + .expect("LspSettings should be defined") + .clone(); + replace_subschema::(&mut generator, || { json_schema!({ "type": "object", @@ -1063,6 +1075,38 @@ impl SettingsStore { }) }); + replace_subschema::(&mut generator, || { + let mut lsp_properties = serde_json::Map::new(); + + for adapter_name in params.lsp_adapter_names { + let mut base_lsp_settings = lsp_settings_def + .as_object() + .expect("LspSettings should be an object") + .clone(); + + if let Some(properties) = base_lsp_settings.get_mut("properties") { + if let Some(props_obj) = properties.as_object_mut() { + props_obj.insert( + "initialization_options".to_string(), + serde_json::json!({ + "$ref": format!("{LSP_SETTINGS_SCHEMA_URL_PREFIX}{adapter_name}") + }), + ); + } + } + + lsp_properties.insert( + adapter_name.clone(), + serde_json::Value::Object(base_lsp_settings), + ); + } + + json_schema!({ + "type": "object", + "properties": lsp_properties, + }) + }); + generator .root_schema_for::() .to_value() @@ -2304,4 +2348,39 @@ mod tests { ] ) } + + #[gpui::test] + fn test_lsp_settings_schema_generation(cx: &mut App) { + let store = SettingsStore::test(cx); + + let schema = store.json_schema(&SettingsJsonSchemaParams { + language_names: &["Rust".to_string(), "TypeScript".to_string()], + font_names: &["Zed Mono".to_string()], + theme_names: &["One Dark".into()], + icon_theme_names: &["Zed Icons".into()], + lsp_adapter_names: &[ + "rust-analyzer".to_string(), + "typescript-language-server".to_string(), + ], + }); + + let properties = schema + .pointer("/$defs/LspSettingsMap/properties") + .expect("LspSettingsMap should have properties") + .as_object() + .unwrap(); + + assert!(properties.contains_key("rust-analyzer")); + assert!(properties.contains_key("typescript-language-server")); + + let init_options_ref = properties + .get("rust-analyzer") + .unwrap() + .pointer("/properties/initialization_options/$ref") + .expect("initialization_options should have a $ref") + .as_str() + .unwrap(); + + assert_eq!(init_options_ref, "zed://schemas/settings/lsp/rust-analyzer"); + } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index bdf3bf3f950a329e7c1b49f5ce27560b00807a5f..8c6575780bbc418b4717af8329c51a057eb608d3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -833,12 +833,19 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut cx.spawn_in(window, async move |workspace, cx| { let res = async move { let json = app_state.languages.language_for_name("JSONC").await.ok(); + let lsp_store = workspace.update(cx, |workspace, cx| { + workspace + .project() + .update(cx, |project, _| project.lsp_store()) + })?; let json_schema_content = json_schema_store::resolve_schema_request_inner( &app_state.languages, + lsp_store, &schema_path, cx, - )?; + ) + .await?; let json_schema_content = serde_json::to_string_pretty(&json_schema_content) .context("Failed to serialize JSON Schema as JSON")?;