Split Lsp installation methods from LspAdapter

This commit is contained in:
Lukas Wirth 2025-08-06 11:48:05 +02:00
parent 85865fc950
commit aae59a2842
13 changed files with 719 additions and 717 deletions

View file

@ -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<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
toolchains: Option<Toolchain>,
binary_options: LanguageServerBinaryOptions,
mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
cx: &'a mut AsyncApp,
) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
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<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
None
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>>;
fn will_fetch_server(
&self,
_: &Arc<dyn LspAdapterDelegate>,
_: &mut AsyncApp,
) -> Option<Task<Result<()>>> {
None
}
async fn check_if_version_installed(
&self,
_version: &(dyn 'static + Send + Any),
_container_dir: &PathBuf,
_delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
None
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary>;
async fn cached_server_binary(
&self,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary>;
fn process_diagnostics(
&self,
_: &mut lsp::PublishDiagnosticsParams,
@ -590,40 +471,182 @@ pub trait LspAdapter: 'static + Send + Sync {
}
}
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(
adapter: &L,
delegate: &Arc<dyn LspAdapterDelegate>,
container_dir: PathBuf,
cx: &mut AsyncApp,
) -> Result<LanguageServerBinary> {
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<Toolchain>,
_: &AsyncApp,
) -> impl Future<Output = Option<LanguageServerBinary>> {
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<Output = Result<Self::BinaryVersion>>;
let latest_version = adapter
.fetch_latest_server_version(delegate.as_ref())
.await?;
fn will_fetch_server(
&self,
_: &Arc<dyn LspAdapterDelegate>,
_: &mut AsyncApp,
) -> Option<Task<Result<()>>> {
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<Output = Option<LanguageServerBinary>> {
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<Output = Result<LanguageServerBinary>>;
fn cached_server_binary(
&self,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> impl Future<Output = Option<LanguageServerBinary>>;
}
#[async_trait(?Send)]
pub trait DynLspInstaller {
async fn try_fetch_server_binary(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
container_dir: PathBuf,
cx: &mut AsyncApp,
) -> Result<LanguageServerBinary>;
fn get_language_server_command<'a>(
self: Arc<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
toolchains: Option<Toolchain>,
binary_options: LanguageServerBinaryOptions,
cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
cx: &'a mut AsyncApp,
) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>>;
}
#[async_trait(?Send)]
impl<LI, BinaryVersion> DynLspInstaller for LI
where
LI: LspInstaller<BinaryVersion = BinaryVersion> + LspAdapter,
{
async fn try_fetch_server_binary(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
container_dir: PathBuf,
cx: &mut AsyncApp,
) -> Result<LanguageServerBinary> {
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<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
binary_options: LanguageServerBinaryOptions,
mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
cx: &'a mut AsyncApp,
) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
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<Self::BinaryVersion> {
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<Self>,
_: Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
_: LanguageServerBinaryOptions,
_: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
_: &'a mut AsyncApp,
) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
async move { Ok(self.language_server_binary.clone()) }.boxed_local()
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
unreachable!();
}
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: (),
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
@ -2225,6 +2234,14 @@ impl LspAdapter for FakeLspAdapter {
) -> Option<LanguageServerBinary> {
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<String> {
self.disk_based_diagnostics_sources.clone()