Add autocomplete for initialization_options (#43104)

Nereuxofficial created

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
<img width="857" height="836" alt="image"
src="https://github.com/user-attachments/assets/3cc10883-364f-4f04-b3b9-3c3881f64252"
/>


## 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?

Change summary

Cargo.lock                                               |   1 
crates/editor/src/editor_tests.rs                        |   8 
crates/json_schema_store/Cargo.toml                      |   1 
crates/json_schema_store/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 
crates/project/src/lsp_store/json_language_server_ext.rs |  10 
crates/settings/src/settings.rs                          |   5 
crates/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(-)

Detailed changes

Cargo.lock 🔗

@@ -8645,6 +8645,7 @@ dependencies = [
  "extension",
  "gpui",
  "language",
+ "lsp",
  "paths",
  "project",
  "schemars",

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,

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

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<LspStore>,
     uri: String,
     cx: &mut AsyncApp,
-) -> Result<String> {
-    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<Result<String>> {
+    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<LanguageRegistry>,
+    lsp_store: Entity<LspStore>,
     uri: String,
     cx: &mut AsyncApp,
 ) -> Result<serde_json::Value> {
     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<LanguageRegistry>,
+    lsp_store: Entity<LspStore>,
     path: &str,
     cx: &mut AsyncApp,
 ) -> Result<serde_json::Value> {
@@ -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::<Vec<_>>();
 
-            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::<settings::SettingsStore>().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::<Vec<_>>();
+
+                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::<settings::SettingsStore>().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")?;

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<Self>,
+        _language_server_binary: &LanguageServerBinary,
+    ) -> Option<serde_json::Value> {
+        None
+    }
+
     async fn workspace_configuration(
         self: Arc<Self>,
         _: &Arc<dyn LspAdapterDelegate>,

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<dyn Fs>,
 }
 
+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<serde_json::Value> = 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<Self>,
+        language_server_binary: &LanguageServerBinary,
+    ) -> Option<serde_json::Value> {
+        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")));
+    }
 }

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<Self>,
+        language_server_binary: &LanguageServerBinary,
+    ) -> Option<serde_json::Value> {
+        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"));
+    }
 }

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();
 

crates/project/src/lsp_store.rs 🔗

@@ -257,7 +257,7 @@ struct DynamicRegistrations {
 
 pub struct LocalLspStore {
     weak: WeakEntity<LspStore>,
-    worktree_store: Entity<WorktreeStore>,
+    pub worktree_store: Entity<WorktreeStore>,
     toolchain_store: Entity<LocalToolchainStore>,
     http_client: Arc<dyn HttpClient>,
     environment: Entity<ProjectEnvironment>,
@@ -13953,7 +13953,7 @@ impl LocalLspAdapterDelegate {
         })
     }
 
-    fn from_local_lsp(
+    pub fn from_local_lsp(
         local: &LocalLspStore,
         worktree: &Entity<Worktree>,
         cx: &mut App,

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<LspStore>, String, &mut AsyncApp) -> Result<String>;
+type SchemaRequestHandler = fn(Entity<LspStore>, String, &mut AsyncApp) -> Task<Result<String>>;
 pub struct SchemaHandlingImpl(SchemaRequestHandler);
 
 impl Global for SchemaHandlingImpl {}
@@ -72,9 +72,7 @@ pub fn notify_schema_changed(lsp_store: Entity<LspStore>, uri: String, cx: &App)
 pub fn register_requests(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
     language_server
         .on_request::<SchemaContentRequest, _, _>(move |params, cx| {
-            let handler = cx.try_read_global::<SchemaHandlingImpl, _>(|handler, _| {
-                handler.0
-            });
+            let handler = cx.try_read_global::<SchemaHandlingImpl, _>(|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<LspStore>, 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 {:?}", &params);

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};

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<Arc<str>, LspSettings>);
+
+impl IntoIterator for LspSettingsMap {
+    type Item = (Arc<str>, LspSettings);
+    type IntoIter = std::collections::hash_map::IntoIter<Arc<str>, 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<Arc<str>, LspSettings>,
+    pub lsp: LspSettingsMap,
 
     pub terminal: Option<ProjectTerminalSettingsContent>,
 

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::<LanguageSettingsContent>()
             .to_value();
 
+        generator.subschema_for::<LspSettings>();
+
+        let lsp_settings_def = generator
+            .definitions()
+            .get("LspSettings")
+            .expect("LspSettings should be defined")
+            .clone();
+
         replace_subschema::<LanguageToSettingsMap>(&mut generator, || {
             json_schema!({
                 "type": "object",
@@ -1063,6 +1075,38 @@ impl SettingsStore {
             })
         });
 
+        replace_subschema::<LspSettingsMap>(&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::<UserSettingsContent>()
             .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");
+    }
 }

crates/zed/src/main.rs 🔗

@@ -833,12 +833,19 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, 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")?;