Detailed changes
@@ -28,6 +28,8 @@ use anyhow::{Context as _, Result};
use async_trait::async_trait;
use collections::{HashMap, HashSet, IndexSet};
use futures::Future;
+use futures::future::LocalBoxFuture;
+use futures::lock::OwnedMutexGuard;
use gpui::{App, AsyncApp, Entity, SharedString};
pub use highlight_map::HighlightMap;
use http_client::HttpClient;
@@ -51,7 +53,6 @@ use std::{
mem,
ops::{DerefMut, Range},
path::{Path, PathBuf},
- pin::Pin,
str,
sync::{
Arc, LazyLock,
@@ -152,7 +153,14 @@ pub struct Location {
}
type ServerBinaryCache = futures::lock::Mutex<Option<(bool, LanguageServerBinary)>>;
-
+type DownloadableLanguageServerBinary = LocalBoxFuture<'static, Result<LanguageServerBinary>>;
+pub type LanguageServerBinaryLocations = LocalBoxFuture<
+ 'static,
+ (
+ Result<LanguageServerBinary>,
+ Option<DownloadableLanguageServerBinary>,
+ ),
+>;
/// Represents a Language Server, with certain cached sync properties.
/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
/// once at startup, and caches the results.
@@ -162,7 +170,7 @@ pub struct CachedLspAdapter {
pub disk_based_diagnostics_progress_token: Option<String>,
language_ids: HashMap<LanguageName, String>,
pub adapter: Arc<dyn LspAdapter>,
- cached_binary: ServerBinaryCache,
+ cached_binary: Arc<ServerBinaryCache>,
}
impl Debug for CachedLspAdapter {
@@ -209,18 +217,15 @@ impl CachedLspAdapter {
toolchains: Option<Toolchain>,
binary_options: LanguageServerBinaryOptions,
cx: &mut AsyncApp,
- ) -> Result<LanguageServerBinary> {
- let mut cached_binary = self.cached_binary.lock().await;
- self.adapter
- .clone()
- .get_language_server_command(
- delegate,
- toolchains,
- binary_options,
- &mut cached_binary,
- cx,
- )
- .await
+ ) -> LanguageServerBinaryLocations {
+ let cached_binary = self.cached_binary.clone().lock_owned().await;
+ self.adapter.clone().get_language_server_command(
+ delegate,
+ toolchains,
+ binary_options,
+ cached_binary,
+ cx.clone(),
+ )
}
pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -513,14 +518,14 @@ pub trait DynLspInstaller {
pre_release: bool,
cx: &mut AsyncApp,
) -> Result<LanguageServerBinary>;
- fn get_language_server_command<'a>(
+ fn get_language_server_command(
self: Arc<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
toolchains: Option<Toolchain>,
binary_options: LanguageServerBinaryOptions,
- cached_binary: &'a mut Option<(bool, LanguageServerBinary)>,
- cx: &'a mut AsyncApp,
- ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>>;
+ cached_binary: OwnedMutexGuard<Option<(bool, LanguageServerBinary)>>,
+ cx: AsyncApp,
+ ) -> LanguageServerBinaryLocations;
}
#[async_trait(?Send)]
@@ -562,15 +567,16 @@ where
binary
}
}
- fn get_language_server_command<'a>(
+ fn get_language_server_command(
self: Arc<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
binary_options: LanguageServerBinaryOptions,
- cached_binary: &'a mut Option<(bool, LanguageServerBinary)>,
- cx: &'a mut AsyncApp,
- ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
+ mut cached_binary: OwnedMutexGuard<Option<(bool, LanguageServerBinary)>>,
+ mut cx: AsyncApp,
+ ) -> LanguageServerBinaryLocations {
async move {
+ let cached_binary_deref = cached_binary.deref_mut();
// 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:
@@ -584,7 +590,7 @@ where
// 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)
+ .check_if_user_installed(delegate.as_ref(), toolchain, &mut cx)
.await
{
log::info!(
@@ -593,62 +599,77 @@ where
binary.path,
binary.arguments
);
- return Ok(binary);
+ return (Ok(binary), None);
}
- anyhow::ensure!(
- binary_options.allow_binary_download,
- "downloading language servers disabled"
- );
+ if !binary_options.allow_binary_download {
+ return (
+ Err(anyhow::anyhow!("downloading language servers disabled")),
+ None,
+ );
+ }
- if let Some((pre_release, cached_binary)) = cached_binary
+ if let Some((pre_release, cached_binary)) = cached_binary_deref
&& *pre_release == binary_options.pre_release
{
- return Ok(cached_binary.clone());
+ return (Ok(cached_binary.clone()), None);
}
let Some(container_dir) = delegate.language_server_download_dir(&self.name()).await
else {
- anyhow::bail!("no language server download dir defined")
+ return (
+ Err(anyhow::anyhow!("no language server download dir defined")),
+ None,
+ );
};
- let mut binary = self
- .try_fetch_server_binary(
- &delegate,
- container_dir.to_path_buf(),
- binary_options.pre_release,
- cx,
- )
- .await;
+ let last_downloaded_binary = self
+ .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
+ .await
+ .context(
+ "did not find existing language server binary, falling back to downloading",
+ );
+ let download_binary = async move {
+ let mut binary = self
+ .try_fetch_server_binary(
+ &delegate,
+ container_dir.to_path_buf(),
+ binary_options.pre_release,
+ &mut 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 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_options.pre_release, binary.clone()));
}
- }
- if let Ok(binary) = &binary {
- *cached_binary = Some((binary_options.pre_release, binary.clone()));
+ binary
}
-
- binary
+ .boxed_local();
+ (last_downloaded_binary, Some(download_binary))
}
.boxed_local()
}
@@ -1,17 +1,16 @@
use std::ops::Range;
use std::path::PathBuf;
-use std::pin::Pin;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use collections::{HashMap, HashSet};
use extension::{Extension, ExtensionLanguageServerProxy, WorktreeDelegate};
-use futures::{Future, FutureExt, future::join_all};
+use futures::{FutureExt, future::join_all, lock::OwnedMutexGuard};
use gpui::{App, AppContext, AsyncApp, Task};
use language::{
- BinaryStatus, CodeLabel, DynLspInstaller, HighlightId, Language, LanguageName, LspAdapter,
- LspAdapterDelegate, Toolchain,
+ BinaryStatus, CodeLabel, DynLspInstaller, HighlightId, Language, LanguageName,
+ LanguageServerBinaryLocations, LspAdapter, LspAdapterDelegate, Toolchain,
};
use lsp::{
CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName,
@@ -155,47 +154,51 @@ impl ExtensionLspAdapter {
#[async_trait(?Send)]
impl DynLspInstaller for ExtensionLspAdapter {
- fn get_language_server_command<'a>(
+ fn get_language_server_command(
self: Arc<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
_: LanguageServerBinaryOptions,
- _: &'a mut Option<(bool, LanguageServerBinary)>,
- _: &'a mut AsyncApp,
- ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
+ _: OwnedMutexGuard<Option<(bool, LanguageServerBinary)>>,
+ _: AsyncApp,
+ ) -> LanguageServerBinaryLocations {
async move {
- let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
- let command = self
- .extension
- .language_server_command(
- self.language_server_id.clone(),
- self.language_name.clone(),
- delegate,
- )
- .await?;
-
- let path = self.extension.path_from_extension(command.command.as_ref());
-
- // TODO: This should now be done via the `zed::make_file_executable` function in
- // Zed extension API, but we're leaving these existing usages in place temporarily
- // to avoid any compatibility issues between Zed and the extension versions.
- //
- // We can remove once the following extension versions no longer see any use:
- // - toml@0.0.2
- // - zig@0.0.1
- if ["toml", "zig"].contains(&self.extension.manifest().id.as_ref())
- && path.starts_with(&self.extension.work_dir())
- {
- make_file_executable(&path)
- .await
- .context("failed to set file permissions")?;
- }
+ let ret = maybe!(async move {
+ let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
+ let command = self
+ .extension
+ .language_server_command(
+ self.language_server_id.clone(),
+ self.language_name.clone(),
+ delegate,
+ )
+ .await?;
+
+ let path = self.extension.path_from_extension(command.command.as_ref());
+
+ // TODO: This should now be done via the `zed::make_file_executable` function in
+ // Zed extension API, but we're leaving these existing usages in place temporarily
+ // to avoid any compatibility issues between Zed and the extension versions.
+ //
+ // We can remove once the following extension versions no longer see any use:
+ // - toml@0.0.2
+ // - zig@0.0.1
+ if ["toml", "zig"].contains(&self.extension.manifest().id.as_ref())
+ && path.starts_with(&self.extension.work_dir())
+ {
+ make_file_executable(&path)
+ .await
+ .context("failed to set file permissions")?;
+ }
- Ok(LanguageServerBinary {
- path,
- arguments: command.args.into_iter().map(|arg| arg.into()).collect(),
- env: Some(command.env.into_iter().collect()),
+ Ok(LanguageServerBinary {
+ path,
+ arguments: command.args.into_iter().map(|arg| arg.into()).collect(),
+ env: Some(command.env.into_iter().collect()),
+ })
})
+ .await;
+ (ret, None)
}
.boxed_local()
}
@@ -1,13 +1,11 @@
-use anyhow::{Context as _, Result};
+use anyhow::Result;
use async_trait::async_trait;
-use futures::StreamExt;
use gpui::AsyncApp;
use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::json;
-use smol::fs;
use std::{
ffi::OsString,
path::{Path, PathBuf},
@@ -176,19 +174,10 @@ async fn get_cached_server_binary(
node: &NodeRuntime,
) -> Option<LanguageServerBinary> {
maybe!(async {
- let mut last_version_dir = None;
- 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_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.context("no cached binary")?;
- let server_path = last_version_dir.join(SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
anyhow::ensure!(
server_path.exists(),
- "missing executable in directory {last_version_dir:?}"
+ "missing executable in directory {server_path:?}"
);
Ok(LanguageServerBinary {
path: node.binary_path().await?,
@@ -301,20 +301,10 @@ async fn get_cached_server_binary(
node: &NodeRuntime,
) -> Option<LanguageServerBinary> {
maybe!(async {
- let mut last_version_dir = None;
- 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_dir() {
- last_version_dir = Some(entry.path());
- }
- }
-
- let last_version_dir = last_version_dir.context("no cached binary")?;
- let server_path = last_version_dir.join(SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
anyhow::ensure!(
server_path.exists(),
- "missing executable in directory {last_version_dir:?}"
+ "missing executable in directory {server_path:?}"
);
Ok(LanguageServerBinary {
path: node.binary_path().await?,
@@ -1,14 +1,12 @@
-use anyhow::{Context as _, Result};
+use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
-use futures::StreamExt;
use gpui::AsyncApp;
use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::{Value, json};
-use smol::fs;
use std::{
ffi::OsString,
path::{Path, PathBuf},
@@ -198,19 +196,10 @@ async fn get_cached_server_binary(
node: &NodeRuntime,
) -> Option<LanguageServerBinary> {
maybe!(async {
- let mut last_version_dir = None;
- 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_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.context("no cached binary")?;
- let server_path = last_version_dir.join(SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
anyhow::ensure!(
server_path.exists(),
- "missing executable in directory {last_version_dir:?}"
+ "missing executable in directory {server_path:?}"
);
Ok(LanguageServerBinary {
path: node.binary_path().await?,
@@ -1,6 +1,5 @@
-use anyhow::{Context as _, Result};
+use anyhow::Result;
use async_trait::async_trait;
-use futures::StreamExt;
use gpui::AsyncApp;
use language::{
LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain, language_settings::AllLanguageSettings,
@@ -10,7 +9,6 @@ use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::Value;
use settings::{Settings, SettingsLocation};
-use smol::fs;
use std::{
ffi::OsString,
path::{Path, PathBuf},
@@ -171,19 +169,10 @@ async fn get_cached_server_binary(
node: &NodeRuntime,
) -> Option<LanguageServerBinary> {
maybe!(async {
- let mut last_version_dir = None;
- 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_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.context("no cached binary")?;
- let server_path = last_version_dir.join(SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
anyhow::ensure!(
server_path.exists(),
- "missing executable in directory {last_version_dir:?}"
+ "missing executable in directory {server_path:?}"
);
Ok(LanguageServerBinary {
path: node.binary_path().await?,
@@ -139,6 +139,7 @@ pub use worktree::{
const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
pub const SERVER_PROGRESS_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100);
const WORKSPACE_DIAGNOSTICS_TOKEN_START: &str = "id:";
+const SERVER_DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub enum ProgressToken {
@@ -599,14 +600,36 @@ impl LocalLspStore {
};
cx.spawn(async move |cx| {
- let binary_result = adapter
+ let (existing_binary, maybe_download_binary) = adapter
.clone()
.get_language_server_command(delegate.clone(), toolchain, lsp_binary_options, cx)
+ .await
.await;
delegate.update_status(adapter.name.clone(), BinaryStatus::None);
- let mut binary = binary_result?;
+ let mut binary = match (existing_binary, maybe_download_binary) {
+ (binary, None) => binary?,
+ (Err(_), Some(downloader)) => downloader.await?,
+ (Ok(existing_binary), Some(downloader)) => {
+ let mut download_timeout = cx
+ .background_executor()
+ .timer(SERVER_DOWNLOAD_TIMEOUT)
+ .fuse();
+ let mut downloader = downloader.fuse();
+ futures::select! {
+ _ = download_timeout => {
+ // Return existing binary and kick the existing work to the background.
+ cx.spawn(async move |_| downloader.await).detach();
+ Ok(existing_binary)
+ },
+ downloaded_or_existing_binary = downloader => {
+ // If download fails, this results in the existing binary.
+ downloaded_or_existing_binary
+ }
+ }?
+ }
+ };
let mut shell_env = delegate.shell_env().await;
shell_env.extend(binary.env.unwrap_or_default());