Correctly handle network issues during LSP server installation (#9460)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/issues/9458

When flying in a plane being totally offline, I've discovered that my
Rust projects do not have any LSP support and rust-analyzer disappeared
out of `~/Library/Application Support/Zed/languages/rust-analyzer/`
directory.

Looking at the
[bad.log](https://github.com/zed-industries/zed/files/14627508/bad.log),
it appears that `get_language_server_command` tries to find a newer LSP
server version and fails on
https://github.com/zed-industries/zed/blob/80bc6c8cc800e23ba79723b0c46b731a62203224/crates/language/src/language.rs#L339

bailing out of all installation related-methods up to here:


https://github.com/zed-industries/zed/blob/80bc6c8cc800e23ba79723b0c46b731a62203224/crates/project/src/project.rs#L2916

where the code thinks that the binary installation process had failed,
cleans the existing directory and tries to install the language server
again:

```log
[2024-03-17T15:14:13+02:00 WARN  isahc::handler] request completed with error: failed to resolve host name
[2024-03-17T15:14:13+02:00 ERROR project] failed to start language server "rust-analyzer": error fetching latest release
[2024-03-17T15:14:13+02:00 ERROR project] server stderr: Some("")
[2024-03-17T15:14:13+02:00 INFO  project] retrying installation of language server "rust-analyzer" in 1s
[2024-03-17T15:14:13+02:00 ERROR util] crates/lsp/src/lsp.rs:720: oneshot canceled
[2024-03-17T15:14:14+02:00 INFO  project] About to spawn test binary
[2024-03-17T15:14:14+02:00 WARN  project] test binary failed to launch
[2024-03-17T15:14:14+02:00 WARN  project] test binary check failed
[2024-03-17T15:14:14+02:00 INFO  project] beginning to reinstall server
[2024-03-17T15:14:14+02:00 INFO  language::language_registry] deleting server container
[2024-03-17T15:14:14+02:00 INFO  language::language_registry] starting language server "rust-analyzer", path: "/Users/someonetoignore/work/other/local_test", id: 2
[2024-03-17T15:14:14+02:00 INFO  language] fetching latest version of language server "rust-analyzer"
[2024-03-17T15:14:14+02:00 WARN  isahc::handler] request completed with error: failed to resolve host name
[2024-03-17T15:14:14+02:00 ERROR project] failed to start language server "rust-analyzer": error fetching latest release
[2024-03-17T15:14:14+02:00 ERROR project] server stderr: Some("")
[2024-03-17T15:14:14+02:00 INFO  project] retrying installation of language server "rust-analyzer" in 1s
[2024-03-17T15:14:15+02:00 ERROR util] crates/languages/src/rust.rs:335: no cached binary
[2024-03-17T15:14:15+02:00 INFO  project] About to spawn test binary
............
```

The PR extracts away all binary fetching-related code into a single
method that does not fail the entire `get_language_server_command` and
allows it to recover and reuse the existing binary:


[good.log](https://github.com/zed-industries/zed/files/14627507/good.log)

```log
[2024-03-17T15:12:24+02:00 INFO  language::language_registry] starting language server "rust-analyzer", path: "/Users/someonetoignore/work/other/local_test", id: 1
[2024-03-17T15:12:24+02:00 INFO  language] fetching latest version of language server "rust-analyzer"
[2024-03-17T15:12:24+02:00 WARN  isahc::handler] request completed with error: failed to resolve host name
[2024-03-17T15:12:24+02:00 INFO  language] failed to fetch newest version of language server LanguageServerName("rust-analyzer"). falling back to using "/Users/someonetoignore/Library/Application Support/Zed/languages/rust-analyzer/rust-analyzer-2024-03-11"
[2024-03-17T15:12:24+02:00 INFO  lsp] starting language server. binary path: "/Users/someonetoignore/Library/Application Support/Zed/languages/rust-analyzer/rust-analyzer-2024-03-11", working directory: "/Users/someonetoignore/work/other/local_test", args: []
```

Release Notes:

- Fixed language servers erased from the disk when project is opened
offline

Change summary

crates/language/src/language.rs | 57 ++++++++++++++++++++--------------
crates/languages/src/rust.rs    |  4 +-
2 files changed, 35 insertions(+), 26 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -326,43 +326,25 @@ pub trait LspAdapter: 'static + Send + Sync {
                     .context("failed to create container directory")?;
             }
 
-            if let Some(task) = self.will_fetch_server(&delegate, cx) {
-                task.await?;
-            }
-
-            let name = self.name();
-            log::info!("fetching latest version of language server {:?}", name.0);
-            delegate.update_status(
-                name.clone(),
-                LanguageServerBinaryStatus::CheckingForUpdate,
-            );
-            let latest_version = self.fetch_latest_server_version(delegate.as_ref()).await?;
-
-            log::info!("downloading language server {:?}", name.0);
-            delegate.update_status(self.name(), LanguageServerBinaryStatus::Downloading);
-            let mut binary = self
-                .fetch_server_binary(latest_version, container_dir.to_path_buf(), delegate.as_ref())
-                .await;
-
-            delegate.update_status(name.clone(), LanguageServerBinaryStatus::Downloaded);
+            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
                 {
-                    delegate.update_status(name.clone(), LanguageServerBinaryStatus::Cached);
+                    delegate.update_status(self.name(), LanguageServerBinaryStatus::Cached);
                     log::info!(
                         "failed to fetch newest version of language server {:?}. falling back to using {:?}",
-                        name.clone(),
-                        prev_downloaded_binary.path.display()
+                        self.name(),
+                        prev_downloaded_binary.path
                     );
                     binary = Ok(prev_downloaded_binary);
                 } else {
                     delegate.update_status(
-                        name.clone(),
+                        self.name(),
                         LanguageServerBinaryStatus::Failed {
-                            error: format!("{:?}", error),
+                            error: format!("{error:?}"),
                         },
                     );
                 }
@@ -500,6 +482,33 @@ 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 AsyncAppContext,
+) -> Result<LanguageServerBinary> {
+    if let Some(task) = adapter.will_fetch_server(delegate, cx) {
+        task.await?;
+    }
+
+    let name = adapter.name();
+    log::info!("fetching latest version of language server {:?}", name.0);
+    delegate.update_status(name.clone(), LanguageServerBinaryStatus::CheckingForUpdate);
+    let latest_version = adapter
+        .fetch_latest_server_version(delegate.as_ref())
+        .await?;
+
+    log::info!("downloading language server {:?}", name.0);
+    delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading);
+    let binary = adapter
+        .fetch_server_binary(latest_version, container_dir, delegate.as_ref())
+        .await;
+
+    delegate.update_status(name.clone(), LanguageServerBinaryStatus::Downloaded);
+    binary
+}
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct CodeLabel {
     /// The text to display.

crates/languages/src/rust.rs 🔗

@@ -1,4 +1,4 @@
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_trait::async_trait;
 use futures::{io::BufReader, StreamExt};
@@ -79,7 +79,7 @@ impl LspAdapter for RustLspAdapter {
             .assets
             .iter()
             .find(|asset| asset.name == asset_name)
-            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
         Ok(Box::new(GitHubLspBinaryVersion {
             name: release.tag_name,
             url: asset.browser_download_url.clone(),