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 e222d49718085868686d678177d30a08c7dc2ee6..95c20491944dfb0ca67baac2d9f293a49c8479e2 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 f84c98af0ccc2408d035781d953106d07975c8e8..ca0772e6f285d3c0d0a73c1f6b153978cabfa4e3 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 95aaec5469018cd5dea1615d76b23a97b2aacd6e..3caf8b60b73105c55df0900acf5012791c6e6760 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 0d9c2d053f98c20c32bdd1fe88c824bbda2ff1ca..a01662f63f72e322eaa2c25f6543bb3ec959f936 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 823e714d8a6cb4490dadd4d0f9b50587baaf3dd3..6c07a181dada303c4934daebf3710bd78b4829b8 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 bda5db426e0c319a73eb52e12185dd95f990c3c5..9938cf911eb0ef0fec68849495decd6a5e76ee76 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 d4deca5944c15548cebd621307daa75957ece35e..f99c927c692e2bfa77741f80fa0b26c02788b2e5 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 3ccefb1f56296e58c0bb89613ae198d50374d483..148e5b59025ae2911a4e51266557379592ce6628 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 3361ac1662634a186142b89c018426a5cc9db4b6..0f088aa46d7140e7c43d9ff112a2cf13f17b04e9 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)?;