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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,6 +345,7 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
let lsp_settings = content
|
||||
.project
|
||||
.lsp
|
||||
.0
|
||||
.entry(VTSLS_SERVER_NAME.into())
|
||||
.or_default();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {:?}", ¶ms);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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>,
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
Reference in New Issue
Block a user