copilot: Switch to official `@github/copilot-language-server` (#27401)

Marshall Bowers created

This PR updates Copilot to use the official
[`@github/copilot-language-server`](https://github.com/github/copilot-language-server-release).

It's [available on
npm](https://www.npmjs.com/package/@github/copilot-language-server), so
we're installing it from there.

I tested it out locally and it seemed to be working as expected.

Release Notes:

- Updated Copilot to use the official
[`@github/copilot-language-server`](https://github.com/github/copilot-language-server-release).

Change summary

Cargo.lock                    |   3 
crates/copilot/Cargo.toml     |   3 
crates/copilot/src/copilot.rs | 139 +++++++++++-------------------------
3 files changed, 44 insertions(+), 101 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3243,9 +3243,7 @@ name = "copilot"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "async-compression",
  "async-std",
- "async-tar",
  "chrono",
  "client",
  "clock",
@@ -3273,7 +3271,6 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
- "smol",
  "strum",
  "task",
  "theme",

crates/copilot/Cargo.toml 🔗

@@ -26,8 +26,6 @@ test-support = [
 
 [dependencies]
 anyhow.workspace = true
-async-compression.workspace = true
-async-tar.workspace = true
 chrono.workspace = true
 client.workspace = true
 collections.workspace = true
@@ -49,7 +47,6 @@ schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
-smol.workspace = true
 strum.workspace = true
 task.workspace = true
 ui.workspace = true

crates/copilot/src/copilot.rs 🔗

@@ -4,9 +4,7 @@ pub mod request;
 mod sign_in;
 
 use ::fs::Fs;
-use anyhow::{anyhow, Context as _, Result};
-use async_compression::futures::bufread::GzipDecoder;
-use async_tar::Archive;
+use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet};
 use command_palette_hooks::CommandPaletteFilter;
 use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
@@ -14,7 +12,6 @@ use gpui::{
     actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Task,
     WeakEntity,
 };
-use http_client::github::get_release_by_tag_name;
 use http_client::HttpClient;
 use language::language_settings::CopilotSettings;
 use language::{
@@ -27,7 +24,6 @@ use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 use request::StatusNotification;
 use settings::SettingsStore;
-use smol::{fs, io::BufReader, stream::StreamExt};
 use std::{
     any::TypeId,
     env,
@@ -37,7 +33,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{fs::remove_matching, maybe, ResultExt};
+use util::{fs::remove_matching, ResultExt};
 
 pub use crate::copilot_completion_provider::CopilotCompletionProvider;
 pub use crate::sign_in::{initiate_sign_in, CopilotCodeVerification};
@@ -65,7 +61,7 @@ pub fn init(
 
     let copilot = cx.new({
         let node_runtime = node_runtime.clone();
-        move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
+        move |cx| Copilot::start(new_server_id, node_runtime, cx)
     });
     Copilot::set_global(copilot.clone(), cx);
     cx.observe(&copilot, |handle, cx| {
@@ -305,7 +301,6 @@ pub struct Completion {
 }
 
 pub struct Copilot {
-    http: Arc<dyn HttpClient>,
     node_runtime: NodeRuntime,
     server: CopilotServer,
     buffers: HashSet<WeakEntity<Buffer>>,
@@ -337,13 +332,11 @@ impl Copilot {
 
     fn start(
         new_server_id: LanguageServerId,
-        http: Arc<dyn HttpClient>,
         node_runtime: NodeRuntime,
         cx: &mut Context<Self>,
     ) -> Self {
         let mut this = Self {
             server_id: new_server_id,
-            http,
             node_runtime,
             server: CopilotServer::Disabled,
             buffers: Default::default(),
@@ -384,14 +377,12 @@ impl Copilot {
             return;
         }
         let server_id = self.server_id;
-        let http = self.http.clone();
         let node_runtime = self.node_runtime.clone();
         let env = self.build_env(&language_settings.edit_predictions.copilot);
         let start_task = cx
             .spawn(async move |this, cx| {
                 Self::start_language_server(
                     server_id,
-                    http,
                     node_runtime,
                     env,
                     this,
@@ -445,11 +436,9 @@ impl Copilot {
             Default::default(),
             &mut cx.to_async(),
         );
-        let http = http_client::FakeHttpClient::create(|_| async { unreachable!() });
         let node_runtime = NodeRuntime::unavailable();
         let this = cx.new(|cx| Self {
             server_id: LanguageServerId(0),
-            http: http.clone(),
             node_runtime,
             server: CopilotServer::Running(RunningCopilotServer {
                 lsp: Arc::new(server),
@@ -464,7 +453,6 @@ impl Copilot {
 
     async fn start_language_server(
         new_server_id: LanguageServerId,
-        http: Arc<dyn HttpClient>,
         node_runtime: NodeRuntime,
         env: Option<HashMap<String, String>>,
         this: WeakEntity<Self>,
@@ -472,7 +460,7 @@ impl Copilot {
         cx: &mut AsyncApp,
     ) {
         let start_language_server = async {
-            let server_path = get_copilot_lsp(http).await?;
+            let server_path = get_copilot_lsp(node_runtime.clone()).await?;
             let node_path = node_runtime.binary_path().await?;
             let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
             let binary = LanguageServerBinary {
@@ -506,9 +494,23 @@ impl Copilot {
             let configuration = lsp::DidChangeConfigurationParams {
                 settings: Default::default(),
             };
+
+            let editor_info = request::SetEditorInfoParams {
+                editor_info: request::EditorInfo {
+                    name: "zed".into(),
+                    version: env!("CARGO_PKG_VERSION").into(),
+                },
+                editor_plugin_info: request::EditorPluginInfo {
+                    name: "zed-copilot".into(),
+                    version: "0.0.1".into(),
+                },
+            };
+            let editor_info_json = serde_json::to_value(&editor_info)?;
+
             let server = cx
                 .update(|cx| {
-                    let params = server.default_initialize_params(cx);
+                    let mut params = server.default_initialize_params(cx);
+                    params.initialization_options = Some(editor_info_json);
                     server.initialize(params, configuration.into(), cx)
                 })?
                 .await?;
@@ -520,16 +522,7 @@ impl Copilot {
                 .await?;
 
             server
-                .request::<request::SetEditorInfo>(request::SetEditorInfoParams {
-                    editor_info: request::EditorInfo {
-                        name: "zed".into(),
-                        version: env!("CARGO_PKG_VERSION").into(),
-                    },
-                    editor_plugin_info: request::EditorPluginInfo {
-                        name: "zed-copilot".into(),
-                        version: "0.0.1".into(),
-                    },
-                })
+                .request::<request::SetEditorInfo>(editor_info)
                 .await?;
 
             anyhow::Ok((server, status))
@@ -668,13 +661,11 @@ impl Copilot {
         let env = self.build_env(&language_settings.edit_predictions.copilot);
         let start_task = cx
             .spawn({
-                let http = self.http.clone();
                 let node_runtime = self.node_runtime.clone();
                 let server_id = self.server_id;
                 async move |this, cx| {
                     clear_copilot_dir().await;
-                    Self::start_language_server(server_id, http, node_runtime, env, this, false, cx)
-                        .await
+                    Self::start_language_server(server_id, node_runtime, env, this, false, cx).await
                 }
             })
             .shared();
@@ -1056,73 +1047,31 @@ async fn clear_copilot_config_dir() {
     remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
 }
 
-async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
-    const SERVER_PATH: &str = "dist/language-server.js";
-
-    ///Check for the latest copilot language server and download it if we haven't already
-    async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
-        let release =
-            get_release_by_tag_name("zed-industries/copilot", "v0.7.0", http.clone()).await?;
-
-        let version_dir = &paths::copilot_dir().join(format!("copilot-{}", release.tag_name));
-
-        fs::create_dir_all(version_dir).await?;
-        let server_path = version_dir.join(SERVER_PATH);
-
-        if fs::metadata(&server_path).await.is_err() {
-            // Copilot LSP looks for this dist dir specifically, so lets add it in.
-            let dist_dir = version_dir.join("dist");
-            fs::create_dir_all(dist_dir.as_path()).await?;
-
-            let url = &release
-                .assets
-                .first()
-                .context("Github release for copilot contained no assets")?
-                .browser_download_url;
-
-            let mut response = http
-                .get(url, Default::default(), true)
-                .await
-                .context("error downloading copilot release")?;
-            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
-            let archive = Archive::new(decompressed_bytes);
-            archive.unpack(dist_dir).await?;
-
-            remove_matching(paths::copilot_dir(), |entry| entry != version_dir).await;
-        }
-
-        Ok(server_path)
+async fn get_copilot_lsp(node_runtime: NodeRuntime) -> anyhow::Result<PathBuf> {
+    const PACKAGE_NAME: &str = "@github/copilot-language-server";
+    const SERVER_PATH: &str =
+        "node_modules/@github/copilot-language-server/dist/language-server.js";
+
+    let latest_version = node_runtime
+        .npm_package_latest_version(PACKAGE_NAME)
+        .await?;
+    let server_path = paths::copilot_dir().join(SERVER_PATH);
+
+    let should_install = node_runtime
+        .should_install_npm_package(
+            PACKAGE_NAME,
+            &server_path,
+            paths::copilot_dir(),
+            &latest_version,
+        )
+        .await;
+    if should_install {
+        node_runtime
+            .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
+            .await?;
     }
 
-    match fetch_latest(http).await {
-        ok @ Result::Ok(..) => ok,
-        e @ Err(..) => {
-            e.log_err();
-            // Fetch a cached binary, if it exists
-            maybe!(async {
-                let mut last_version_dir = None;
-                let mut entries = fs::read_dir(paths::copilot_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.ok_or_else(|| anyhow!("no cached binary"))?;
-                let server_path = last_version_dir.join(SERVER_PATH);
-                if server_path.exists() {
-                    Ok(server_path)
-                } else {
-                    Err(anyhow!(
-                        "missing executable in directory {:?}",
-                        last_version_dir
-                    ))
-                }
-            })
-            .await
-        }
-    }
+    Ok(server_path)
 }
 
 #[cfg(test)]