From 8f1023360db7162db2579047f2d40658ef541a10 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 25 Mar 2025 18:23:59 +0530 Subject: [PATCH] extension: Add support for `additional_workspace_configuration` and `additional_initialization_options` (#27407) Closes #22410 With this PR extensions can provide additional workspace configuration for other LSP Adapters. This allows extensions like Astro, Svelte, Vue, etc to provide plugins for vtsls typescript server, fixing issues like: https://github.com/zed-industries/zed/issues/4577, https://github.com/zed-industries/zed/issues/21697, https://github.com/zed-industries/zed/issues/26901#issuecomment-2737485096 Todo: - [x] Test case when extension is installed, does vtsls workspace config refreshes? Before: image After: image Release Notes: - N/A --- crates/extension/src/extension.rs | 14 ++ crates/extension_api/src/extension_api.rs | 52 ++++++++ .../wit/since_v0.4.0/extension.wit | 6 + crates/extension_host/src/wasm_host.rs | 50 +++++++ crates/extension_host/src/wasm_host/wit.rs | 52 ++++++++ crates/language/src/language.rs | 21 +++ crates/language/src/language_registry.rs | 9 ++ .../src/extension_lsp_adapter.rs | 52 ++++++++ crates/project/src/lsp_store.rs | 123 +++++++++++++++--- 9 files changed, 360 insertions(+), 19 deletions(-) diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index e222d49718..95c2049194 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -76,6 +76,20 @@ pub trait Extension: Send + Sync + 'static { worktree: Arc, ) -> Result>; + async fn language_server_additional_initialization_options( + &self, + language_server_id: LanguageServerName, + target_language_server_id: LanguageServerName, + worktree: Arc, + ) -> Result>; + + async fn language_server_additional_workspace_configuration( + &self, + language_server_id: LanguageServerName, + target_language_server_id: LanguageServerName, + worktree: Arc, + ) -> Result>; + async fn labels_for_completions( &self, language_server_id: LanguageServerName, diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index f84c98af0c..ca0772e6f2 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -93,6 +93,26 @@ pub trait Extension: Send + Sync { Ok(None) } + /// Returns the initialization options to pass to the other language server. + fn language_server_additional_initialization_options( + &mut self, + _language_server_id: &LanguageServerId, + _target_language_server_id: &LanguageServerId, + _worktree: &Worktree, + ) -> Result> { + Ok(None) + } + + /// Returns the workspace configuration options to pass to the other language server. + fn language_server_additional_workspace_configuration( + &mut self, + _language_server_id: &LanguageServerId, + _target_language_server_id: &LanguageServerId, + _worktree: &Worktree, + ) -> Result> { + Ok(None) + } + /// Returns the label for the given completion. fn label_for_completion( &self, @@ -235,6 +255,38 @@ impl wit::Guest for Component { .and_then(|value| serde_json::to_string(&value).ok())) } + fn language_server_additional_initialization_options( + language_server_id: String, + target_language_server_id: String, + worktree: &Worktree, + ) -> Result, String> { + let language_server_id = LanguageServerId(language_server_id); + let target_language_server_id = LanguageServerId(target_language_server_id); + Ok(extension() + .language_server_additional_initialization_options( + &language_server_id, + &target_language_server_id, + worktree, + )? + .and_then(|value| serde_json::to_string(&value).ok())) + } + + fn language_server_additional_workspace_configuration( + language_server_id: String, + target_language_server_id: String, + worktree: &Worktree, + ) -> Result, String> { + let language_server_id = LanguageServerId(language_server_id); + let target_language_server_id = LanguageServerId(target_language_server_id); + Ok(extension() + .language_server_additional_workspace_configuration( + &language_server_id, + &target_language_server_id, + worktree, + )? + .and_then(|value| serde_json::to_string(&value).ok())) + } + fn labels_for_completions( language_server_id: String, completions: Vec, diff --git a/crates/extension_api/wit/since_v0.4.0/extension.wit b/crates/extension_api/wit/since_v0.4.0/extension.wit index 95aaec5469..3caf8b60b7 100644 --- a/crates/extension_api/wit/since_v0.4.0/extension.wit +++ b/crates/extension_api/wit/since_v0.4.0/extension.wit @@ -95,6 +95,12 @@ world extension { /// Returns the workspace configuration options to pass to the language server. export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow) -> result, string>; + /// Returns the initialization options to pass to the other language server. + export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; + + /// Returns the workspace configuration options to pass to the other language server. + export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow) -> result, string>; + /// A label containing some code. record code-label { /// The source code to parse with Tree-sitter. diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 0d9c2d053f..a01662f63f 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -140,6 +140,56 @@ impl extension::Extension for WasmExtension { .await } + async fn language_server_additional_initialization_options( + &self, + language_server_id: LanguageServerName, + target_language_server_id: LanguageServerName, + worktree: Arc, + ) -> Result> { + self.call(|extension, store| { + async move { + let resource = store.data_mut().table().push(worktree)?; + let options = extension + .call_language_server_additional_initialization_options( + store, + &language_server_id, + &target_language_server_id, + resource, + ) + .await? + .map_err(|err| anyhow!("{err}"))?; + anyhow::Ok(options) + } + .boxed() + }) + .await + } + + async fn language_server_additional_workspace_configuration( + &self, + language_server_id: LanguageServerName, + target_language_server_id: LanguageServerName, + worktree: Arc, + ) -> Result> { + self.call(|extension, store| { + async move { + let resource = store.data_mut().table().push(worktree)?; + let options = extension + .call_language_server_additional_workspace_configuration( + store, + &language_server_id, + &target_language_server_id, + resource, + ) + .await? + .map_err(|err| anyhow!("{err}"))?; + anyhow::Ok(options) + } + .boxed() + }) + .await + } + async fn labels_for_completions( &self, language_server_id: LanguageServerName, diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 823e714d8a..6c07a181da 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -365,6 +365,58 @@ impl Extension { } } + pub async fn call_language_server_additional_initialization_options( + &self, + store: &mut Store, + language_server_id: &LanguageServerName, + target_language_server_id: &LanguageServerName, + resource: Resource>, + ) -> Result, String>> { + match self { + Extension::V040(ext) => { + ext.call_language_server_additional_initialization_options( + store, + &language_server_id.0, + &target_language_server_id.0, + resource, + ) + .await + } + Extension::V030(_) + | Extension::V020(_) + | Extension::V010(_) + | Extension::V006(_) + | Extension::V004(_) + | Extension::V001(_) => Ok(Ok(None)), + } + } + + pub async fn call_language_server_additional_workspace_configuration( + &self, + store: &mut Store, + language_server_id: &LanguageServerName, + target_language_server_id: &LanguageServerName, + resource: Resource>, + ) -> Result, String>> { + match self { + Extension::V040(ext) => { + ext.call_language_server_additional_workspace_configuration( + store, + &language_server_id.0, + &target_language_server_id.0, + resource, + ) + .await + } + Extension::V030(_) + | Extension::V020(_) + | Extension::V010(_) + | Extension::V006(_) + | Extension::V004(_) + | Extension::V001(_) => Ok(Ok(None)), + } + } + pub async fn call_labels_for_completions( &self, store: &mut Store, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index bda5db426e..9938cf911e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -305,6 +305,7 @@ pub trait LspAdapterDelegate: Send + Sync { fn worktree_root_path(&self) -> &Path; fn exists(&self, path: &Path, is_dir: Option) -> bool; fn update_status(&self, language: LanguageServerName, status: BinaryStatus); + fn registered_lsp_adapters(&self) -> Vec>; async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option>; async fn npm_package_installed_version( @@ -515,6 +516,26 @@ pub trait LspAdapter: 'static + Send + Sync { Ok(serde_json::json!({})) } + async fn additional_initialization_options( + self: Arc, + _target_language_server_id: LanguageServerName, + _: &dyn Fs, + _: &Arc, + ) -> Result> { + Ok(None) + } + + async fn additional_workspace_configuration( + self: Arc, + _target_language_server_id: LanguageServerName, + _: &dyn Fs, + _: &Arc, + _: Arc, + _cx: &mut AsyncApp, + ) -> Result> { + Ok(None) + } + /// Returns a list of code actions supported by a given LspAdapter fn code_action_kinds(&self) -> Option> { Some(vec![ diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index d4deca5944..f99c927c69 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -912,6 +912,15 @@ impl LanguageRegistry { .unwrap_or_default() } + pub fn all_lsp_adapters(&self) -> Vec> { + self.state + .read() + .all_lsp_adapters + .values() + .cloned() + .collect() + } + pub fn adapter_for_name(&self, name: &LanguageServerName) -> Option> { self.state.read().all_lsp_adapters.get(name).cloned() } diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index 3ccefb1f56..148e5b5902 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -267,6 +267,58 @@ impl LspAdapter for ExtensionLspAdapter { }) } + async fn additional_initialization_options( + self: Arc, + target_language_server_id: LanguageServerName, + _: &dyn Fs, + delegate: &Arc, + ) -> Result> { + let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; + let json_options: Option = self + .extension + .language_server_additional_initialization_options( + self.language_server_id.clone(), + target_language_server_id.clone(), + delegate, + ) + .await?; + Ok(if let Some(json_options) = json_options { + serde_json::from_str(&json_options).with_context(|| { + format!( + "failed to parse additional_initialization_options from extension: {json_options}" + ) + })? + } else { + None + }) + } + + async fn additional_workspace_configuration( + self: Arc, + target_language_server_id: LanguageServerName, + _: &dyn Fs, + delegate: &Arc, + _: Arc, + _cx: &mut AsyncApp, + ) -> Result> { + let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; + let json_options: Option = self + .extension + .language_server_additional_workspace_configuration( + self.language_server_id.clone(), + target_language_server_id.clone(), + delegate, + ) + .await?; + Ok(if let Some(json_options) = json_options { + serde_json::from_str(&json_options).with_context(|| { + format!("failed to parse additional_workspace_configuration from extension: {json_options}") + })? + } else { + None + }) + } + async fn labels_for_completions( self: Arc, completions: &[lsp::CompletionItem], diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 3361ac1662..0f088aa46d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -251,17 +251,21 @@ impl LocalLspStore { let toolchains = this.update(cx, |this, cx| this.toolchain_store(cx))?; let language_server = pending_server.await?; - let workspace_config = adapter - .adapter - .clone() - .workspace_configuration(fs.as_ref(), &delegate, toolchains.clone(), cx) - .await?; + let workspace_config = Self::workspace_configuration_for_adapter( + adapter.adapter.clone(), + fs.as_ref(), + &delegate, + toolchains.clone(), + cx, + ) + .await?; - let mut initialization_options = adapter - .adapter - .clone() - .initialization_options(fs.as_ref(), &(delegate)) - .await?; + let mut initialization_options = Self::initialization_options_for_adapter( + adapter.adapter.clone(), + fs.as_ref(), + &delegate, + ) + .await?; match (&mut initialization_options, override_options) { (Some(initialization_options), Some(override_options)) => { @@ -478,9 +482,16 @@ impl LocalLspStore { async move { let toolchains = this.update(&mut cx, |this, cx| this.toolchain_store(cx))?; - let workspace_config = adapter - .workspace_configuration(fs.as_ref(), &delegate, toolchains, &mut cx) - .await?; + + let workspace_config = Self::workspace_configuration_for_adapter( + adapter.clone(), + fs.as_ref(), + &delegate, + toolchains.clone(), + &mut cx, + ) + .await?; + Ok(params .items .into_iter() @@ -3225,6 +3236,67 @@ impl LocalLspStore { self.rebuild_watched_paths(language_server_id, cx); } + + async fn initialization_options_for_adapter( + adapter: Arc, + fs: &dyn Fs, + delegate: &Arc, + ) -> Result> { + let Some(mut initialization_config) = + adapter.clone().initialization_options(fs, delegate).await? + else { + return Ok(None); + }; + + for other_adapter in delegate.registered_lsp_adapters() { + if other_adapter.name() == adapter.name() { + continue; + } + if let Ok(Some(target_config)) = other_adapter + .clone() + .additional_initialization_options(adapter.name(), fs, delegate) + .await + { + merge_json_value_into(target_config.clone(), &mut initialization_config); + } + } + + Ok(Some(initialization_config)) + } + + async fn workspace_configuration_for_adapter( + adapter: Arc, + fs: &dyn Fs, + delegate: &Arc, + toolchains: Arc, + cx: &mut AsyncApp, + ) -> Result { + let mut workspace_config = adapter + .clone() + .workspace_configuration(fs, delegate, toolchains.clone(), cx) + .await?; + + for other_adapter in delegate.registered_lsp_adapters() { + if other_adapter.name() == adapter.name() { + continue; + } + if let Ok(Some(target_config)) = other_adapter + .clone() + .additional_workspace_configuration( + adapter.name(), + fs, + delegate, + toolchains.clone(), + cx, + ) + .await + { + merge_json_value_into(target_config.clone(), &mut workspace_config); + } + } + + Ok(workspace_config) + } } #[derive(Debug)] @@ -3764,8 +3836,8 @@ impl LspStore { for (adapter, server, delegate) in servers { adapter.clear_zed_json_schema_cache().await; - let Some(json_workspace_config) = adapter - .workspace_configuration( + let Some(json_workspace_config) = LocalLspStore::workspace_configuration_for_adapter( + adapter, fs.as_ref(), &delegate, toolchain_store.clone(), @@ -6065,10 +6137,15 @@ impl LspStore { let toolchain_store = this.update(cx, |this, cx| this.toolchain_store(cx)).ok()?; for (adapter, server, delegate) in servers { - let settings = adapter - .workspace_configuration(fs.as_ref(), &delegate, toolchain_store.clone(), cx) - .await - .ok()?; + let settings = LocalLspStore::workspace_configuration_for_adapter( + adapter, + fs.as_ref(), + &delegate, + toolchain_store.clone(), + cx, + ) + .await + .ok()?; server .notify::( @@ -9811,6 +9888,14 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { .update_lsp_status(server_name, status); } + fn registered_lsp_adapters(&self) -> Vec> { + self.language_registry + .all_lsp_adapters() + .into_iter() + .map(|adapter| adapter.adapter.clone() as Arc) + .collect() + } + async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option> { let dir = self.language_registry.language_server_download_dir(name)?;