From aae59a2842f910e32a0331128962b70bffcb744d Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 6 Aug 2025 11:48:05 +0200 Subject: [PATCH 1/2] Split Lsp installation methods from LspAdapter --- crates/language/src/language.rs | 361 +++++++++--------- .../src/extension_lsp_adapter.rs | 37 +- crates/languages/src/c.rs | 52 +-- crates/languages/src/css.rs | 38 +- crates/languages/src/go.rs | 39 +- crates/languages/src/json.rs | 62 +-- crates/languages/src/python.rs | 156 ++++---- crates/languages/src/rust.rs | 304 +++++++-------- crates/languages/src/tailwind.rs | 38 +- crates/languages/src/typescript.rs | 210 +++++----- crates/languages/src/vtsls.rs | 28 +- crates/languages/src/yaml.rs | 38 +- crates/project/src/lsp_store.rs | 73 ++-- 13 files changed, 719 insertions(+), 717 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7ae77c9141..332022a2bd 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -46,7 +46,6 @@ use settings::WorktreeId; use smol::future::FutureExt as _; use std::num::NonZeroU32; use std::{ - any::Any, ffi::OsStr, fmt::Debug, hash::Hash, @@ -306,127 +305,9 @@ pub trait LspAdapterDelegate: Send + Sync { } #[async_trait(?Send)] -pub trait LspAdapter: 'static + Send + Sync { +pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller { fn name(&self) -> LanguageServerName; - fn get_language_server_command<'a>( - self: Arc, - delegate: Arc, - toolchains: Option, - binary_options: LanguageServerBinaryOptions, - mut cached_binary: futures::lock::MutexGuard<'a, Option>, - cx: &'a mut AsyncApp, - ) -> Pin>>> { - async move { - // First we check whether the adapter can give us a user-installed binary. - // If so, we do *not* want to cache that, because each worktree might give us a different - // binary: - // - // worktree 1: user-installed at `.bin/gopls` - // worktree 2: user-installed at `~/bin/gopls` - // worktree 3: no gopls found in PATH -> fallback to Zed installation - // - // We only want to cache when we fall back to the global one, - // because we don't want to download and overwrite our global one - // for each worktree we might have open. - if binary_options.allow_path_lookup - && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { - log::debug!( - "found user-installed language server for {}. path: {:?}, arguments: {:?}", - self.name().0, - binary.path, - binary.arguments - ); - return Ok(binary); - } - - anyhow::ensure!(binary_options.allow_binary_download, "downloading language servers disabled"); - - if let Some(cached_binary) = cached_binary.as_ref() { - return Ok(cached_binary.clone()); - } - - let Some(container_dir) = delegate.language_server_download_dir(&self.name()).await else { - anyhow::bail!("no language server download dir defined") - }; - - let mut binary = try_fetch_server_binary(self.as_ref(), &delegate, container_dir.to_path_buf(), cx).await; - - if let Err(error) = binary.as_ref() { - if let Some(prev_downloaded_binary) = self - .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) - .await - { - log::info!( - "failed to fetch newest version of language server {:?}. error: {:?}, falling back to using {:?}", - self.name(), - error, - prev_downloaded_binary.path - ); - binary = Ok(prev_downloaded_binary); - } else { - delegate.update_status( - self.name(), - BinaryStatus::Failed { - error: format!("{error:?}"), - }, - ); - } - } - - if let Ok(binary) = &binary { - *cached_binary = Some(binary.clone()); - } - - binary - } - .boxed_local() - } - - async fn check_if_user_installed( - &self, - _: &dyn LspAdapterDelegate, - _: Option, - _: &AsyncApp, - ) -> Option { - None - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result>; - - fn will_fetch_server( - &self, - _: &Arc, - _: &mut AsyncApp, - ) -> Option>> { - None - } - - async fn check_if_version_installed( - &self, - _version: &(dyn 'static + Send + Any), - _container_dir: &PathBuf, - _delegate: &dyn LspAdapterDelegate, - ) -> Option { - None - } - - async fn fetch_server_binary( - &self, - latest_version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result; - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Option; - fn process_diagnostics( &self, _: &mut lsp::PublishDiagnosticsParams, @@ -590,40 +471,182 @@ pub trait LspAdapter: 'static + Send + Sync { } } -async fn try_fetch_server_binary( - adapter: &L, - delegate: &Arc, - container_dir: PathBuf, - cx: &mut AsyncApp, -) -> Result { - if let Some(task) = adapter.will_fetch_server(delegate, cx) { - task.await?; +pub trait LspInstaller { + type BinaryVersion; + fn check_if_user_installed( + &self, + _: &dyn LspAdapterDelegate, + _: Option, + _: &AsyncApp, + ) -> impl Future> { + async { None } } - let name = adapter.name(); - log::debug!("fetching latest version of language server {:?}", name.0); - delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate); + fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> impl Future>; - let latest_version = adapter - .fetch_latest_server_version(delegate.as_ref()) - .await?; + fn will_fetch_server( + &self, + _: &Arc, + _: &mut AsyncApp, + ) -> Option>> { + None + } - if let Some(binary) = adapter - .check_if_version_installed(latest_version.as_ref(), &container_dir, delegate.as_ref()) - .await - { - log::debug!("language server {:?} is already installed", name.0); - delegate.update_status(name.clone(), BinaryStatus::None); - Ok(binary) - } else { - log::info!("downloading language server {:?}", name.0); - delegate.update_status(adapter.name(), BinaryStatus::Downloading); - let binary = adapter - .fetch_server_binary(latest_version, container_dir, delegate.as_ref()) - .await; + fn check_if_version_installed( + &self, + _version: &Self::BinaryVersion, + _container_dir: &PathBuf, + _delegate: &dyn LspAdapterDelegate, + ) -> impl Future> { + async { None } + } - delegate.update_status(name.clone(), BinaryStatus::None); - binary + fn fetch_server_binary( + &self, + latest_version: Self::BinaryVersion, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> impl Future>; + + fn cached_server_binary( + &self, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> impl Future>; +} +#[async_trait(?Send)] +pub trait DynLspInstaller { + async fn try_fetch_server_binary( + &self, + delegate: &Arc, + container_dir: PathBuf, + cx: &mut AsyncApp, + ) -> Result; + fn get_language_server_command<'a>( + self: Arc, + delegate: Arc, + toolchains: Option, + binary_options: LanguageServerBinaryOptions, + cached_binary: futures::lock::MutexGuard<'a, Option>, + cx: &'a mut AsyncApp, + ) -> Pin>>>; +} +#[async_trait(?Send)] +impl DynLspInstaller for LI +where + LI: LspInstaller + LspAdapter, +{ + async fn try_fetch_server_binary( + &self, + delegate: &Arc, + container_dir: PathBuf, + cx: &mut AsyncApp, + ) -> Result { + if let Some(task) = self.will_fetch_server(delegate, cx) { + task.await?; + } + + let name = self.name(); + + log::debug!("fetching latest version of language server {:?}", name.0); + delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate); + + let latest_version = self.fetch_latest_server_version(delegate.as_ref()).await?; + + if let Some(binary) = self + .check_if_version_installed(&latest_version, &container_dir, delegate.as_ref()) + .await + { + log::debug!("language server {:?} is already installed", name.0); + delegate.update_status(name.clone(), BinaryStatus::None); + Ok(binary) + } else { + log::debug!("downloading language server {:?}", name.0); + delegate.update_status(name.clone(), BinaryStatus::Downloading); + let binary = self + .fetch_server_binary(latest_version, container_dir, delegate.as_ref()) + .await; + + delegate.update_status(name.clone(), BinaryStatus::None); + binary + } + } + fn get_language_server_command<'a>( + self: Arc, + delegate: Arc, + toolchain: Option, + binary_options: LanguageServerBinaryOptions, + mut cached_binary: futures::lock::MutexGuard<'a, Option>, + cx: &'a mut AsyncApp, + ) -> Pin>>> { + async move { + // First we check whether the adapter can give us a user-installed binary. + // If so, we do *not* want to cache that, because each worktree might give us a different + // binary: + // + // worktree 1: user-installed at `.bin/gopls` + // worktree 2: user-installed at `~/bin/gopls` + // worktree 3: no gopls found in PATH -> fallback to Zed installation + // + // We only want to cache when we fall back to the global one, + // because we don't want to download and overwrite our global one + // for each worktree we might have open. + if binary_options.allow_path_lookup + && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchain, cx).await { + log::info!( + "found user-installed language server for {}. path: {:?}, arguments: {:?}", + self.name().0, + binary.path, + binary.arguments + ); + return Ok(binary); + + } + + anyhow::ensure!(binary_options.allow_binary_download, "downloading language servers disabled"); + + if let Some(cached_binary) = cached_binary.as_ref() { + return Ok(cached_binary.clone()); + } + + let Some(container_dir) = delegate.language_server_download_dir(&self.name()).await else { + anyhow::bail!("no language server download dir defined") + }; + + let mut binary = self.try_fetch_server_binary( &delegate, container_dir.to_path_buf(), cx).await; + + if let Err(error) = binary.as_ref() { + if let Some(prev_downloaded_binary) = self + .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) + .await + { + log::info!( + "failed to fetch newest version of language server {:?}. error: {:?}, falling back to using {:?}", + self.name(), + error, + prev_downloaded_binary.path + ); + binary = Ok(prev_downloaded_binary); + } else { + delegate.update_status( + self.name(), + BinaryStatus::Failed { + error: format!("{error:?}"), + }, + ); + } + } + + if let Ok(binary) = &binary { + *cached_binary = Some(binary.clone()); + } + + binary + } + .boxed_local() } } @@ -2176,10 +2199,14 @@ impl Default for FakeLspAdapter { } #[cfg(any(test, feature = "test-support"))] -#[async_trait(?Send)] -impl LspAdapter for FakeLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName(self.name.into()) +impl LspInstaller for FakeLspAdapter { + type BinaryVersion = (); + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result { + unreachable!() } async fn check_if_user_installed( @@ -2191,27 +2218,9 @@ impl LspAdapter for FakeLspAdapter { Some(self.language_server_binary.clone()) } - fn get_language_server_command<'a>( - self: Arc, - _: Arc, - _: Option, - _: LanguageServerBinaryOptions, - _: futures::lock::MutexGuard<'a, Option>, - _: &'a mut AsyncApp, - ) -> Pin>>> { - async move { Ok(self.language_server_binary.clone()) }.boxed_local() - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - unreachable!(); - } - async fn fetch_server_binary( &self, - _: Box, + _: (), _: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { @@ -2225,6 +2234,14 @@ impl LspAdapter for FakeLspAdapter { ) -> Option { unreachable!(); } +} + +#[cfg(any(test, feature = "test-support"))] +#[async_trait(?Send)] +impl LspAdapter for FakeLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName(self.name.into()) + } fn disk_based_diagnostic_sources(&self) -> Vec { self.disk_based_diagnostics_sources.clone() diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index e465a8dd0a..1df0b24dce 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -1,4 +1,3 @@ -use std::any::Any; use std::ops::Range; use std::path::PathBuf; use std::pin::Pin; @@ -12,8 +11,8 @@ use fs::Fs; use futures::{Future, FutureExt, future::join_all}; use gpui::{App, AppContext, AsyncApp, Task}; use language::{ - BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LspAdapter, LspAdapterDelegate, - Toolchain, + BinaryStatus, CodeLabel, DynLspInstaller, HighlightId, Language, LanguageName, LspAdapter, + LspAdapterDelegate, Toolchain, }; use lsp::{ CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName, @@ -151,11 +150,7 @@ impl ExtensionLspAdapter { } #[async_trait(?Send)] -impl LspAdapter for ExtensionLspAdapter { - fn name(&self) -> LanguageServerName { - self.language_server_id.clone() - } - +impl DynLspInstaller for ExtensionLspAdapter { fn get_language_server_command<'a>( self: Arc, delegate: Arc, @@ -201,28 +196,20 @@ impl LspAdapter for ExtensionLspAdapter { .boxed_local() } - async fn fetch_latest_server_version( + async fn try_fetch_server_binary( &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - unreachable!("get_language_server_command is overridden") - } - - async fn fetch_server_binary( - &self, - _: Box, + _: &Arc, _: PathBuf, - _: &dyn LspAdapterDelegate, + _: &mut AsyncApp, ) -> Result { - unreachable!("get_language_server_command is overridden") + unreachable!("get_language_server_command is overriden") } +} - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - unreachable!("get_language_server_command is overridden") +#[async_trait(?Send)] +impl LspAdapter for ExtensionLspAdapter { + fn name(&self) -> LanguageServerName { + self.language_server_id.clone() } fn code_action_kinds(&self) -> Option> { diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 2820f55a49..e7977eef58 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -8,7 +8,7 @@ use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName}; use project::lsp_store::clangd_ext; use serde_json::json; use smol::fs; -use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; +use std::{env::consts, path::PathBuf, sync::Arc}; use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into}; use crate::github_download::{GithubBinaryMetadata, download_server_binary}; @@ -19,30 +19,13 @@ impl CLspAdapter { const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd"); } -#[async_trait(?Send)] -impl super::LspAdapter for CLspAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME - } - - async fn check_if_user_installed( - &self, - delegate: &dyn LspAdapterDelegate, - _: Option, - _: &AsyncApp, - ) -> Option { - let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; - Some(LanguageServerBinary { - path, - arguments: Vec::new(), - env: None, - }) - } +impl LspInstaller for CLspAdapter { + type BinaryVersion = GitHubLspBinaryVersion; async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, - ) -> Result> { + ) -> Result { let release = latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?; let os_suffix = match consts::OS { @@ -62,12 +45,26 @@ impl super::LspAdapter for CLspAdapter { url: asset.browser_download_url.clone(), digest: asset.digest.clone(), }; - Ok(Box::new(version) as Box<_>) + Ok(version) + } + + async fn check_if_user_installed( + &self, + delegate: &dyn LspAdapterDelegate, + _: Option, + _: &AsyncApp, + ) -> Option { + let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; + Some(LanguageServerBinary { + path, + arguments: Vec::new(), + env: None, + }) } async fn fetch_server_binary( &self, - version: Box, + version: GitHubLspBinaryVersion, container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { @@ -75,7 +72,7 @@ impl super::LspAdapter for CLspAdapter { name, url, digest: expected_digest, - } = *version.downcast::().unwrap(); + } = version; let version_dir = container_dir.join(format!("clangd_{name}")); let binary_path = version_dir.join("bin/clangd"); @@ -146,6 +143,13 @@ impl super::LspAdapter for CLspAdapter { ) -> Option { get_cached_server_binary(container_dir).await } +} + +#[async_trait(?Send)] +impl super::LspAdapter for CLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } async fn label_for_completion( &self, diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 2480d40268..761539e222 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -2,14 +2,13 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncApp; -use language::{LspAdapter, LspAdapterDelegate, Toolchain}; +use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::json; use smol::fs; use std::{ - any::Any, ffi::OsString, path::{Path, PathBuf}, sync::Arc, @@ -34,10 +33,13 @@ impl CssLspAdapter { } } -#[async_trait(?Send)] -impl LspAdapter for CssLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("vscode-css-language-server".into()) +impl LspInstaller for CssLspAdapter { + type BinaryVersion = String; + + async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result { + self.node + .npm_package_latest_version("vscode-langservers-extracted") + .await } async fn check_if_user_installed( @@ -58,24 +60,12 @@ impl LspAdapter for CssLspAdapter { }) } - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new( - self.node - .npm_package_latest_version("vscode-langservers-extracted") - .await?, - ) as Box<_>) - } - async fn fetch_server_binary( &self, - latest_version: Box, + latest_version: String, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); self.node @@ -94,11 +84,10 @@ impl LspAdapter for CssLspAdapter { async fn check_if_version_installed( &self, - version: &(dyn 'static + Send + Any), + version: &String, container_dir: &PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - let version = version.downcast_ref::().unwrap(); let server_path = container_dir.join(SERVER_PATH); let should_install_language_server = self @@ -129,6 +118,13 @@ impl LspAdapter for CssLspAdapter { ) -> Option { get_cached_server_binary(container_dir, &self.node).await } +} + +#[async_trait(?Send)] +impl LspAdapter for CssLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("vscode-css-language-server".into()) + } async fn initialization_options( self: Arc, diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 24e2ca2f56..8257678c3f 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -5,17 +5,17 @@ use futures::StreamExt; use gpui::{App, AsyncApp, Task}; use http_client::github::latest_github_release; pub use language::*; +use language::{LanguageToolchainStore, LspAdapterDelegate, LspInstaller}; use lsp::{LanguageServerBinary, LanguageServerName}; use project::Fs; use regex::Regex; use serde_json::json; use smol::fs; use std::{ - any::Any, borrow::Cow, ffi::{OsStr, OsString}, ops::Range, - path::PathBuf, + path::{Path, PathBuf}, process::Output, str, sync::{ @@ -50,16 +50,13 @@ const BINARY: &str = if cfg!(target_os = "windows") { "gopls" }; -#[async_trait(?Send)] -impl super::LspAdapter for GoLspAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME - } +impl LspInstaller for GoLspAdapter { + type BinaryVersion = Option; async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, - ) -> Result> { + ) -> Result> { let release = latest_github_release("golang/tools", false, false, delegate.http_client()).await?; let version: Option = release.tag_name.strip_prefix("gopls/v").map(str::to_string); @@ -69,7 +66,7 @@ impl super::LspAdapter for GoLspAdapter { release.tag_name ); } - Ok(Box::new(version) as Box<_>) + Ok(version) } async fn check_if_user_installed( @@ -115,7 +112,7 @@ impl super::LspAdapter for GoLspAdapter { async fn fetch_server_binary( &self, - version: Box, + version: Option, container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { @@ -126,10 +123,8 @@ impl super::LspAdapter for GoLspAdapter { .await .context("failed to get go version via `go version` command`")?; let go_version = parse_version_output(&go_version_output)?; - let version = version.downcast::>().unwrap(); - let this = *self; - if let Some(version) = *version { + if let Some(version) = version { let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}")); if let Ok(metadata) = fs::metadata(&binary_path).await && metadata.is_file() @@ -145,10 +140,7 @@ impl super::LspAdapter for GoLspAdapter { env: None, }); } - } else if let Some(path) = this - .cached_server_binary(container_dir.clone(), delegate) - .await - { + } else if let Some(path) = get_cached_server_binary(&container_dir).await { return Ok(path); } @@ -194,7 +186,14 @@ impl super::LspAdapter for GoLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir).await + get_cached_server_binary(&container_dir).await + } +} + +#[async_trait(?Send)] +impl LspAdapter for GoLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME } async fn initialization_options( @@ -442,10 +441,10 @@ fn parse_version_output(output: &Output) -> Result<&str> { Ok(version) } -async fn get_cached_server_binary(container_dir: PathBuf) -> Option { +async fn get_cached_server_binary(container_dir: &Path) -> Option { maybe!(async { let mut last_binary_path = None; - let mut entries = fs::read_dir(&container_dir).await?; + let mut entries = fs::read_dir(container_dir).await?; while let Some(entry) = entries.next().await { let entry = entry?; if entry.file_type().await?.is_file() diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 4fcf865568..673151cfdd 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -9,7 +9,7 @@ use gpui::{App, AsyncApp, Task}; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use language::{ ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter, - LspAdapterDelegate, Toolchain, + LspAdapterDelegate, LspInstaller, Toolchain, }; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -22,7 +22,6 @@ use smol::{ lock::RwLock, }; use std::{ - any::Any, env::consts, ffi::OsString, path::{Path, PathBuf}, @@ -294,10 +293,13 @@ fn generate_inspector_style_schema() -> serde_json_lenient::Value { serde_json_lenient::to_value(schema).unwrap() } -#[async_trait(?Send)] -impl LspAdapter for JsonLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("json-language-server".into()) +impl LspInstaller for JsonLspAdapter { + type BinaryVersion = String; + + async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result { + self.node + .npm_package_latest_version(Self::PACKAGE_NAME) + .await } async fn check_if_user_installed( @@ -318,24 +320,12 @@ impl LspAdapter for JsonLspAdapter { }) } - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new( - self.node - .npm_package_latest_version(Self::PACKAGE_NAME) - .await?, - ) as Box<_>) - } - async fn check_if_version_installed( &self, - version: &(dyn 'static + Send + Any), + version: &String, container_dir: &PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - let version = version.downcast_ref::().unwrap(); let server_path = container_dir.join(SERVER_PATH); let should_install_language_server = self @@ -361,11 +351,10 @@ impl LspAdapter for JsonLspAdapter { async fn fetch_server_binary( &self, - latest_version: Box, + latest_version: String, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); self.node @@ -389,6 +378,13 @@ impl LspAdapter for JsonLspAdapter { ) -> Option { get_cached_server_binary(container_dir, &self.node).await } +} + +#[async_trait(?Send)] +impl LspAdapter for JsonLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("json-language-server".into()) + } async fn initialization_options( self: Arc, @@ -485,16 +481,13 @@ impl NodeVersionAdapter { LanguageServerName::new_static("package-version-server"); } -#[async_trait(?Send)] -impl LspAdapter for NodeVersionAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME - } +impl LspInstaller for NodeVersionAdapter { + type BinaryVersion = GitHubLspBinaryVersion; async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, - ) -> Result> { + ) -> Result { let release = latest_github_release( "zed-industries/package-version-server", true, @@ -519,11 +512,11 @@ impl LspAdapter for NodeVersionAdapter { .iter() .find(|asset| asset.name == asset_name) .with_context(|| format!("no asset found matching `{asset_name:?}`"))?; - Ok(Box::new(GitHubLspBinaryVersion { + Ok(GitHubLspBinaryVersion { name: release.tag_name, url: asset.browser_download_url.clone(), digest: asset.digest.clone(), - })) + }) } async fn check_if_user_installed( @@ -542,11 +535,11 @@ impl LspAdapter for NodeVersionAdapter { async fn fetch_server_binary( &self, - latest_version: Box, + latest_version: GitHubLspBinaryVersion, container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { - let version = latest_version.downcast::().unwrap(); + let version = &latest_version; let destination_path = container_dir.join(format!( "{}-{}{}", Self::SERVER_NAME, @@ -596,6 +589,13 @@ impl LspAdapter for NodeVersionAdapter { } } +#[async_trait(?Send)] +impl LspAdapter for NodeVersionAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } +} + async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option { maybe!(async { let mut last = None; diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index d21b5dabd3..4f43ef9281 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -9,7 +9,7 @@ use language::ToolchainList; use language::ToolchainLister; use language::language_settings::language_settings; use language::{ContextLocation, LanguageToolchainStore}; -use language::{ContextProvider, LspAdapter, LspAdapterDelegate}; +use language::{ContextProvider, LspAdapter, LspAdapterDelegate, LspInstaller}; use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; use lsp::LanguageServerBinary; use lsp::LanguageServerName; @@ -26,7 +26,6 @@ use std::cmp::Ordering; use parking_lot::Mutex; use std::str::FromStr; use std::{ - any::Any, borrow::Cow, ffi::OsString, fmt::Write, @@ -100,28 +99,13 @@ impl PythonLspAdapter { } } -#[async_trait(?Send)] -impl LspAdapter for PythonLspAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME - } +impl LspInstaller for PythonLspAdapter { + type BinaryVersion = String; - async fn initialization_options( - self: Arc, - _: &dyn Fs, - _: &Arc, - ) -> Result> { - // Provide minimal initialization options - // Virtual environment configuration will be handled through workspace configuration - Ok(Some(json!({ - "python": { - "analysis": { - "autoSearchPaths": true, - "useLibraryCodeForTypes": true, - "autoImportCompletions": true - } - } - }))) + async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result { + self.node + .npm_package_latest_version(Self::SERVER_NAME.as_ref()) + .await } async fn check_if_user_installed( @@ -155,24 +139,12 @@ impl LspAdapter for PythonLspAdapter { } } - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new( - self.node - .npm_package_latest_version(Self::SERVER_NAME.as_ref()) - .await?, - ) as Box<_>) - } - async fn fetch_server_binary( &self, - latest_version: Box, + latest_version: String, container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { - let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); self.node @@ -192,11 +164,10 @@ impl LspAdapter for PythonLspAdapter { async fn check_if_version_installed( &self, - version: &(dyn 'static + Send + Any), + version: &String, container_dir: &PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Option { - let version = version.downcast_ref::().unwrap(); let server_path = container_dir.join(SERVER_PATH); let should_install_language_server = self @@ -230,6 +201,31 @@ impl LspAdapter for PythonLspAdapter { binary.env = Some(delegate.shell_env().await); Some(binary) } +} + +#[async_trait(?Send)] +impl LspAdapter for PythonLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } + + async fn initialization_options( + self: Arc, + _: &dyn Fs, + _: &Arc, + ) -> Result> { + // Provide minimal initialization options + // Virtual environment configuration will be handled through workspace configuration + Ok(Some(json!({ + "python": { + "analysis": { + "autoSearchPaths": true, + "useLibraryCodeForTypes": true, + "autoImportCompletions": true + } + } + }))) + } async fn process_completions(&self, items: &mut [lsp::CompletionItem]) { // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. @@ -1023,10 +1019,11 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") { "bin" }; -#[async_trait(?Send)] -impl LspAdapter for PyLspAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME +impl LspInstaller for PyLspAdapter { + type BinaryVersion = (); + + async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<()> { + Ok(()) } async fn check_if_user_installed( @@ -1053,16 +1050,9 @@ impl LspAdapter for PyLspAdapter { } } - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(()) as Box<_>) - } - async fn fetch_server_binary( &self, - _: Box, + _: (), _: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { @@ -1122,6 +1112,13 @@ impl LspAdapter for PyLspAdapter { arguments: vec![], }) } +} + +#[async_trait(?Send)] +impl LspAdapter for PyLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {} @@ -1315,28 +1312,11 @@ impl BasedPyrightLspAdapter { } } -#[async_trait(?Send)] -impl LspAdapter for BasedPyrightLspAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME - } +impl LspInstaller for BasedPyrightLspAdapter { + type BinaryVersion = (); - async fn initialization_options( - self: Arc, - _: &dyn Fs, - _: &Arc, - ) -> Result> { - // Provide minimal initialization options - // Virtual environment configuration will be handled through workspace configuration - Ok(Some(json!({ - "python": { - "analysis": { - "autoSearchPaths": true, - "useLibraryCodeForTypes": true, - "autoImportCompletions": true - } - } - }))) + async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<()> { + Ok(()) } async fn check_if_user_installed( @@ -1364,16 +1344,9 @@ impl LspAdapter for BasedPyrightLspAdapter { } } - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(()) as Box<_>) - } - async fn fetch_server_binary( &self, - _latest_version: Box, + _latest_version: (), _container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { @@ -1411,6 +1384,31 @@ impl LspAdapter for BasedPyrightLspAdapter { arguments: vec!["--stdio".into()], }) } +} + +#[async_trait(?Send)] +impl LspAdapter for BasedPyrightLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } + + async fn initialization_options( + self: Arc, + _: &dyn Fs, + _: &Arc, + ) -> Result> { + // Provide minimal initialization options + // Virtual environment configuration will be handled through workspace configuration + Ok(Some(json!({ + "python": { + "analysis": { + "autoSearchPaths": true, + "useLibraryCodeForTypes": true, + "autoImportCompletions": true + } + } + }))) + } async fn process_completions(&self, items: &mut [lsp::CompletionItem]) { // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index c6c7357148..1af41ddec2 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -17,7 +17,6 @@ use smol::fs::{self}; use std::fmt::Display; use std::ops::Range; use std::{ - any::Any, borrow::Cow, path::{Path, PathBuf}, sync::{Arc, LazyLock}, @@ -109,156 +108,6 @@ impl LspAdapter for RustLspAdapter { SERVER_NAME } - async fn check_if_user_installed( - &self, - delegate: &dyn LspAdapterDelegate, - _: Option, - _: &AsyncApp, - ) -> Option { - let path = delegate.which("rust-analyzer".as_ref()).await?; - let env = delegate.shell_env().await; - - // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to - // /usr/bin/rust-analyzer that fails when you run it; so we need to test it. - log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`"); - let result = delegate - .try_exec(LanguageServerBinary { - path: path.clone(), - arguments: vec!["--help".into()], - env: Some(env.clone()), - }) - .await; - if let Err(err) = result { - log::debug!( - "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}", - path, - err - ); - return None; - } - - Some(LanguageServerBinary { - path, - env: Some(env), - arguments: vec![], - }) - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - let release = latest_github_release( - "rust-lang/rust-analyzer", - true, - false, - delegate.http_client(), - ) - .await?; - let asset_name = Self::build_asset_name(); - let asset = release - .assets - .into_iter() - .find(|asset| asset.name == asset_name) - .with_context(|| format!("no asset found matching `{asset_name:?}`"))?; - Ok(Box::new(GitHubLspBinaryVersion { - name: release.tag_name, - url: asset.browser_download_url, - digest: asset.digest, - })) - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - let GitHubLspBinaryVersion { - name, - url, - digest: expected_digest, - } = *version.downcast::().unwrap(); - let destination_path = container_dir.join(format!("rust-analyzer-{name}")); - let server_path = match Self::GITHUB_ASSET_KIND { - AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place. - AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe - }; - - let binary = LanguageServerBinary { - path: server_path.clone(), - env: None, - arguments: Default::default(), - }; - - let metadata_path = destination_path.with_extension("metadata"); - let metadata = GithubBinaryMetadata::read_from_file(&metadata_path) - .await - .ok(); - if let Some(metadata) = metadata { - let validity_check = async || { - delegate - .try_exec(LanguageServerBinary { - path: server_path.clone(), - arguments: vec!["--version".into()], - env: None, - }) - .await - .inspect_err(|err| { - log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",) - }) - }; - if let (Some(actual_digest), Some(expected_digest)) = - (&metadata.digest, &expected_digest) - { - if actual_digest == expected_digest { - if validity_check().await.is_ok() { - return Ok(binary); - } - } else { - log::info!( - "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}" - ); - } - } else if validity_check().await.is_ok() { - return Ok(binary); - } - } - - download_server_binary( - delegate, - &url, - expected_digest.as_deref(), - &destination_path, - Self::GITHUB_ASSET_KIND, - ) - .await?; - make_file_executable(&server_path).await?; - remove_matching(&container_dir, |path| path != destination_path).await; - GithubBinaryMetadata::write_to_file( - &GithubBinaryMetadata { - metadata_version: 1, - digest: expected_digest, - }, - &metadata_path, - ) - .await?; - - Ok(LanguageServerBinary { - path: server_path, - env: None, - arguments: Default::default(), - }) - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - get_cached_server_binary(container_dir).await - } - fn disk_based_diagnostic_sources(&self) -> Vec { vec![CARGO_DIAGNOSTICS_SOURCE_NAME.to_owned()] } @@ -528,6 +377,159 @@ impl LspAdapter for RustLspAdapter { } } +impl LspInstaller for RustLspAdapter { + type BinaryVersion = GitHubLspBinaryVersion; + async fn check_if_user_installed( + &self, + delegate: &dyn LspAdapterDelegate, + _: Option, + _: &AsyncApp, + ) -> Option { + let path = delegate.which("rust-analyzer".as_ref()).await?; + let env = delegate.shell_env().await; + + // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to + // /usr/bin/rust-analyzer that fails when you run it; so we need to test it. + log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`"); + let result = delegate + .try_exec(LanguageServerBinary { + path: path.clone(), + arguments: vec!["--help".into()], + env: Some(env.clone()), + }) + .await; + if let Err(err) = result { + log::debug!( + "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}", + path, + err + ); + return None; + } + + Some(LanguageServerBinary { + path, + env: Some(env), + arguments: vec![], + }) + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let release = latest_github_release( + "rust-lang/rust-analyzer", + true, + false, + delegate.http_client(), + ) + .await?; + let asset_name = Self::build_asset_name(); + let asset = release + .assets + .into_iter() + .find(|asset| asset.name == asset_name) + .with_context(|| format!("no asset found matching `{asset_name:?}`"))?; + Ok(GitHubLspBinaryVersion { + name: release.tag_name, + url: asset.browser_download_url, + digest: asset.digest, + }) + } + + async fn fetch_server_binary( + &self, + version: GitHubLspBinaryVersion, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let GitHubLspBinaryVersion { + name, + url, + digest: expected_digest, + } = version; + let destination_path = container_dir.join(format!("rust-analyzer-{name}")); + let server_path = match Self::GITHUB_ASSET_KIND { + AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place. + AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe + }; + + let binary = LanguageServerBinary { + path: server_path.clone(), + env: None, + arguments: Default::default(), + }; + + let metadata_path = destination_path.with_extension("metadata"); + let metadata = GithubBinaryMetadata::read_from_file(&metadata_path) + .await + .ok(); + if let Some(metadata) = metadata { + let validity_check = async || { + delegate + .try_exec(LanguageServerBinary { + path: server_path.clone(), + arguments: vec!["--version".into()], + env: None, + }) + .await + .inspect_err(|err| { + log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",) + }) + }; + if let (Some(actual_digest), Some(expected_digest)) = + (&metadata.digest, &expected_digest) + { + if actual_digest == expected_digest { + if validity_check().await.is_ok() { + return Ok(binary); + } + } else { + log::info!( + "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}" + ); + } + } else if validity_check().await.is_ok() { + return Ok(binary); + } + } + + download_server_binary( + delegate, + &url, + expected_digest.as_deref(), + &destination_path, + Self::GITHUB_ASSET_KIND, + ) + .await?; + make_file_executable(&server_path).await?; + remove_matching(&container_dir, |path| path != destination_path).await; + GithubBinaryMetadata::write_to_file( + &GithubBinaryMetadata { + metadata_version: 1, + digest: expected_digest, + }, + &metadata_path, + ) + .await?; + + Ok(LanguageServerBinary { + path: server_path, + env: None, + arguments: Default::default(), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } +} + pub(crate) struct RustContextProvider; const RUST_PACKAGE_TASK_VARIABLE: VariableName = diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 47eb254053..dd41a7e83a 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -3,14 +3,13 @@ use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; use gpui::AsyncApp; -use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain}; +use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; use smol::fs; use std::{ - any::Any, ffi::OsString, path::{Path, PathBuf}, sync::Arc, @@ -41,10 +40,13 @@ impl TailwindLspAdapter { } } -#[async_trait(?Send)] -impl LspAdapter for TailwindLspAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME +impl LspInstaller for TailwindLspAdapter { + type BinaryVersion = String; + + async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result { + self.node + .npm_package_latest_version(Self::PACKAGE_NAME) + .await } async fn check_if_user_installed( @@ -63,24 +65,12 @@ impl LspAdapter for TailwindLspAdapter { }) } - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new( - self.node - .npm_package_latest_version(Self::PACKAGE_NAME) - .await?, - ) as Box<_>) - } - async fn fetch_server_binary( &self, - latest_version: Box, + latest_version: String, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); self.node @@ -99,11 +89,10 @@ impl LspAdapter for TailwindLspAdapter { async fn check_if_version_installed( &self, - version: &(dyn 'static + Send + Any), + version: &String, container_dir: &PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - let version = version.downcast_ref::().unwrap(); let server_path = container_dir.join(SERVER_PATH); let should_install_language_server = self @@ -134,6 +123,13 @@ impl LspAdapter for TailwindLspAdapter { ) -> Option { get_cached_server_binary(container_dir, &self.node).await } +} + +#[async_trait(?Send)] +impl LspAdapter for TailwindLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } async fn initialization_options( self: Arc, diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 77cf1a64f1..c04603ff14 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -7,7 +7,7 @@ use gpui::{App, AppContext, AsyncApp, Task}; use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url}; use language::{ ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter, - LspAdapterDelegate, Toolchain, + LspAdapterDelegate, LspInstaller, Toolchain, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -15,7 +15,6 @@ use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; use smol::{fs, lock::RwLock, stream::StreamExt}; use std::{ - any::Any, borrow::Cow, ffi::OsString, path::{Path, PathBuf}, @@ -549,37 +548,33 @@ impl TypeScriptLspAdapter { } } -struct TypeScriptVersions { +pub struct TypeScriptVersions { typescript_version: String, server_version: String, } -#[async_trait(?Send)] -impl LspAdapter for TypeScriptLspAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME - } +impl LspInstaller for TypeScriptLspAdapter { + type BinaryVersion = TypeScriptVersions; async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(TypeScriptVersions { + ) -> Result { + Ok(TypeScriptVersions { typescript_version: self.node.npm_package_latest_version("typescript").await?, server_version: self .node .npm_package_latest_version("typescript-language-server") .await?, - }) as Box<_>) + }) } async fn check_if_version_installed( &self, - version: &(dyn 'static + Send + Any), + version: &TypeScriptVersions, container_dir: &PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - let version = version.downcast_ref::().unwrap(); let server_path = container_dir.join(Self::NEW_SERVER_PATH); let should_install_language_server = self @@ -605,11 +600,10 @@ impl LspAdapter for TypeScriptLspAdapter { async fn fetch_server_binary( &self, - latest_version: Box, + latest_version: TypeScriptVersions, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(Self::NEW_SERVER_PATH); self.node @@ -642,6 +636,13 @@ impl LspAdapter for TypeScriptLspAdapter { ) -> Option { get_cached_ts_server_binary(container_dir, &self.node).await } +} + +#[async_trait(?Send)] +impl LspAdapter for TypeScriptLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } fn code_action_kinds(&self) -> Option> { Some(vec![ @@ -809,6 +810,97 @@ impl EsLintLspAdapter { } } +impl LspInstaller for EsLintLspAdapter { + type BinaryVersion = GitHubLspBinaryVersion; + + async fn fetch_latest_server_version( + &self, + _delegate: &dyn LspAdapterDelegate, + ) -> Result { + let url = build_asset_url( + "zed-industries/vscode-eslint", + Self::CURRENT_VERSION_TAG_NAME, + Self::GITHUB_ASSET_KIND, + )?; + + Ok(GitHubLspBinaryVersion { + name: Self::CURRENT_VERSION.into(), + digest: None, + url, + }) + } + + async fn fetch_server_binary( + &self, + version: GitHubLspBinaryVersion, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let destination_path = Self::build_destination_path(&container_dir); + let server_path = destination_path.join(Self::SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + remove_matching(&container_dir, |_| true).await; + + download_server_binary( + delegate, + &version.url, + None, + &destination_path, + Self::GITHUB_ASSET_KIND, + ) + .await?; + + let mut dir = fs::read_dir(&destination_path).await?; + let first = dir.next().await.context("missing first file")??; + let repo_root = destination_path.join("vscode-eslint"); + fs::rename(first.path(), &repo_root).await?; + + #[cfg(target_os = "windows")] + { + handle_symlink( + repo_root.join("$shared"), + repo_root.join("client").join("src").join("shared"), + ) + .await?; + handle_symlink( + repo_root.join("$shared"), + repo_root.join("server").join("src").join("shared"), + ) + .await?; + } + + self.node + .run_npm_subcommand(&repo_root, "install", &[]) + .await?; + + self.node + .run_npm_subcommand(&repo_root, "run-script", &["compile"]) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + env: None, + arguments: eslint_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let server_path = + Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH); + Some(LanguageServerBinary { + path: self.node.binary_path().await.ok()?, + env: None, + arguments: eslint_server_binary_arguments(&server_path), + }) + } +} + #[async_trait(?Send)] impl LspAdapter for EsLintLspAdapter { fn code_action_kinds(&self) -> Option> { @@ -881,94 +973,6 @@ impl LspAdapter for EsLintLspAdapter { fn name(&self) -> LanguageServerName { Self::SERVER_NAME } - - async fn fetch_latest_server_version( - &self, - _delegate: &dyn LspAdapterDelegate, - ) -> Result> { - let url = build_asset_url( - "zed-industries/vscode-eslint", - Self::CURRENT_VERSION_TAG_NAME, - Self::GITHUB_ASSET_KIND, - )?; - - Ok(Box::new(GitHubLspBinaryVersion { - name: Self::CURRENT_VERSION.into(), - digest: None, - url, - })) - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - let version = version.downcast::().unwrap(); - let destination_path = Self::build_destination_path(&container_dir); - let server_path = destination_path.join(Self::SERVER_PATH); - - if fs::metadata(&server_path).await.is_err() { - remove_matching(&container_dir, |_| true).await; - - download_server_binary( - delegate, - &version.url, - None, - &destination_path, - Self::GITHUB_ASSET_KIND, - ) - .await?; - - let mut dir = fs::read_dir(&destination_path).await?; - let first = dir.next().await.context("missing first file")??; - let repo_root = destination_path.join("vscode-eslint"); - fs::rename(first.path(), &repo_root).await?; - - #[cfg(target_os = "windows")] - { - handle_symlink( - repo_root.join("$shared"), - repo_root.join("client").join("src").join("shared"), - ) - .await?; - handle_symlink( - repo_root.join("$shared"), - repo_root.join("server").join("src").join("shared"), - ) - .await?; - } - - self.node - .run_npm_subcommand(&repo_root, "install", &[]) - .await?; - - self.node - .run_npm_subcommand(&repo_root, "run-script", &["compile"]) - .await?; - } - - Ok(LanguageServerBinary { - path: self.node.binary_path().await?, - env: None, - arguments: eslint_server_binary_arguments(&server_path), - }) - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - let server_path = - Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH); - Some(LanguageServerBinary { - path: self.node.binary_path().await.ok()?, - env: None, - arguments: eslint_server_binary_arguments(&server_path), - }) - } } #[cfg(target_os = "windows")] diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index f7152b0b5d..1f62bf4c65 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -2,13 +2,12 @@ use anyhow::Result; use async_trait::async_trait; use collections::HashMap; use gpui::AsyncApp; -use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain}; +use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain}; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; use serde_json::Value; use std::{ - any::Any, ffi::OsString, path::{Path, PathBuf}, sync::Arc, @@ -57,30 +56,27 @@ impl VtslsLspAdapter { } } -struct TypeScriptVersions { +pub struct TypeScriptVersions { typescript_version: String, server_version: String, } const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls"); -#[async_trait(?Send)] -impl LspAdapter for VtslsLspAdapter { - fn name(&self) -> LanguageServerName { - SERVER_NAME - } +impl LspInstaller for VtslsLspAdapter { + type BinaryVersion = TypeScriptVersions; async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(TypeScriptVersions { + ) -> Result { + Ok(TypeScriptVersions { typescript_version: self.node.npm_package_latest_version("typescript").await?, server_version: self .node .npm_package_latest_version("@vtsls/language-server") .await?, - }) as Box<_>) + }) } async fn check_if_user_installed( @@ -100,11 +96,10 @@ impl LspAdapter for VtslsLspAdapter { async fn fetch_server_binary( &self, - latest_version: Box, + latest_version: TypeScriptVersions, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(Self::SERVER_PATH); let mut packages_to_install = Vec::new(); @@ -156,6 +151,13 @@ impl LspAdapter for VtslsLspAdapter { ) -> Option { get_cached_ts_server_binary(container_dir, &self.node).await } +} + +#[async_trait(?Send)] +impl LspAdapter for VtslsLspAdapter { + fn name(&self) -> LanguageServerName { + SERVER_NAME + } fn code_action_kinds(&self) -> Option> { Some(vec![ diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index b9197b12ae..474c572fcb 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -2,7 +2,9 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncApp; -use language::{LspAdapter, LspAdapterDelegate, Toolchain, language_settings::AllLanguageSettings}; +use language::{ + LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain, language_settings::AllLanguageSettings, +}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; @@ -10,7 +12,6 @@ use serde_json::Value; use settings::{Settings, SettingsLocation}; use smol::fs; use std::{ - any::Any, ffi::OsString, path::{Path, PathBuf}, sync::Arc, @@ -35,21 +36,13 @@ impl YamlLspAdapter { } } -#[async_trait(?Send)] -impl LspAdapter for YamlLspAdapter { - fn name(&self) -> LanguageServerName { - Self::SERVER_NAME - } +impl LspInstaller for YamlLspAdapter { + type BinaryVersion = String; - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new( - self.node - .npm_package_latest_version("yaml-language-server") - .await?, - ) as Box<_>) + async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result { + self.node + .npm_package_latest_version("yaml-language-server") + .await } async fn check_if_user_installed( @@ -70,11 +63,10 @@ impl LspAdapter for YamlLspAdapter { async fn fetch_server_binary( &self, - latest_version: Box, + latest_version: String, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let latest_version = latest_version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); self.node @@ -93,11 +85,10 @@ impl LspAdapter for YamlLspAdapter { async fn check_if_version_installed( &self, - version: &(dyn 'static + Send + Any), + version: &String, container_dir: &PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - let version = version.downcast_ref::().unwrap(); let server_path = container_dir.join(SERVER_PATH); let should_install_language_server = self @@ -128,6 +119,13 @@ impl LspAdapter for YamlLspAdapter { ) -> Option { get_cached_server_binary(container_dir, &self.node).await } +} + +#[async_trait(?Send)] +impl LspAdapter for YamlLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME + } async fn workspace_configuration( self: Arc, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 1b46117897..0a7bf43e26 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -55,9 +55,9 @@ use itertools::Itertools as _; use language::{ Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, - LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, ManifestDelegate, ManifestName, - Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, Transaction, - Unclipped, + LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller, ManifestDelegate, + ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, + Transaction, Unclipped, language_settings::{ FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, @@ -92,7 +92,6 @@ use sha2::{Digest, Sha256}; use smol::channel::Sender; use snippet::Snippet; use std::{ - any::Any, borrow::Cow, cell::RefCell, cmp::{Ordering, Reverse}, @@ -12773,6 +12772,39 @@ impl SshLspAdapter { } } +impl LspInstaller for SshLspAdapter { + type BinaryVersion = (); + async fn check_if_user_installed( + &self, + _: &dyn LspAdapterDelegate, + _: Option, + _: &AsyncApp, + ) -> Option { + Some(self.binary.clone()) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + None + } + + async fn fetch_latest_server_version(&self, _: &dyn LspAdapterDelegate) -> Result<()> { + anyhow::bail!("SshLspAdapter does not support fetch_latest_server_version") + } + + async fn fetch_server_binary( + &self, + _: (), + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + anyhow::bail!("SshLspAdapter does not support fetch_server_binary") + } +} + #[async_trait(?Send)] impl LspAdapter for SshLspAdapter { fn name(&self) -> LanguageServerName { @@ -12794,39 +12826,6 @@ impl LspAdapter for SshLspAdapter { fn code_action_kinds(&self) -> Option> { self.code_action_kinds.clone() } - - async fn check_if_user_installed( - &self, - _: &dyn LspAdapterDelegate, - _: Option, - _: &AsyncApp, - ) -> Option { - Some(self.binary.clone()) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - None - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - anyhow::bail!("SshLspAdapter does not support fetch_latest_server_version") - } - - async fn fetch_server_binary( - &self, - _: Box, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - anyhow::bail!("SshLspAdapter does not support fetch_server_binary") - } } pub fn language_server_settings<'a>( From 9733d34c9404280c59a9050289bca5934b2f99c7 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 6 Aug 2025 12:46:59 +0200 Subject: [PATCH 2/2] Don't spawn for `will_fetch_server` as it is being immediately awaited anyways --- crates/language/src/language.rs | 10 ++++------ crates/languages/src/go.rs | 29 +++++++++++++---------------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 332022a2bd..8bdf61f5f0 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -29,7 +29,7 @@ use async_trait::async_trait; use collections::{HashMap, HashSet, IndexSet}; use fs::Fs; use futures::Future; -use gpui::{App, AsyncApp, Entity, SharedString, Task}; +use gpui::{App, AsyncApp, Entity, SharedString}; pub use highlight_map::HighlightMap; use http_client::HttpClient; pub use language_registry::{ @@ -491,8 +491,8 @@ pub trait LspInstaller { &self, _: &Arc, _: &mut AsyncApp, - ) -> Option>> { - None + ) -> impl Future> { + async { Ok(()) } } fn check_if_version_installed( @@ -545,9 +545,7 @@ where container_dir: PathBuf, cx: &mut AsyncApp, ) -> Result { - if let Some(task) = self.will_fetch_server(delegate, cx) { - task.await?; - } + self.will_fetch_server(delegate, cx).await?; let name = self.name(); diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 8257678c3f..5dffbe07d5 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -83,31 +83,28 @@ impl LspInstaller for GoLspAdapter { }) } - fn will_fetch_server( + async fn will_fetch_server( &self, delegate: &Arc, cx: &mut AsyncApp, - ) -> Option>> { + ) -> Result<()> { static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false); const NOTIFICATION_MESSAGE: &str = "Could not install the Go language server `gopls`, because `go` was not found."; - let delegate = delegate.clone(); - Some(cx.spawn(async move |cx| { - if delegate.which("go".as_ref()).await.is_none() { - if DID_SHOW_NOTIFICATION - .compare_exchange(false, true, SeqCst, SeqCst) - .is_ok() - { - cx.update(|cx| { - delegate.show_notification(NOTIFICATION_MESSAGE, cx); - })? - } - anyhow::bail!("cannot install gopls"); + if delegate.which("go".as_ref()).await.is_none() { + if DID_SHOW_NOTIFICATION + .compare_exchange(false, true, SeqCst, SeqCst) + .is_ok() + { + cx.update(|cx| { + delegate.show_notification(NOTIFICATION_MESSAGE, cx); + })? } - Ok(()) - })) + anyhow::bail!("cannot install gopls"); + } + Ok(()) } async fn fetch_server_binary(