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
<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?
This commit is contained in:
Nereuxofficial
2025-12-21 16:29:38 +01:00
committed by GitHub
parent 213cb30445
commit 83449293b6
14 changed files with 707 additions and 53 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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.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<_>>();
cx.global::<settings::SettingsStore>().json_schema(
&settings::SettingsJsonSchemaParams {
language_names,
font_names,
theme_names,
icon_theme_names,
},
)
})?,
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")?;

View File

@@ -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>,

View File

@@ -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")));
}
}

View File

@@ -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"));
}
}

View File

@@ -345,6 +345,7 @@ impl LspAdapter for VtslsLspAdapter {
let lsp_settings = content
.project
.lsp
.0
.entry(VTSLS_SERVER_NAME.into())
.or_default();

View File

@@ -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,

View File

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

View File

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

View File

@@ -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>,

View File

@@ -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");
}
}

View File

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