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
## 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")?;