From a52095f5581567c31a9fa2041329cc4569e51ed8 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 24 Mar 2025 19:35:29 -0400 Subject: [PATCH] copilot: Switch to official `@github/copilot-language-server` (#27401) 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). --- Cargo.lock | 3 - crates/copilot/Cargo.toml | 3 - crates/copilot/src/copilot.rs | 135 +++++++++++----------------------- 3 files changed, 42 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5826311f42..986536aea4 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 867e8fd3bb..bcdfa8b993 100644 --- a/crates/copilot/Cargo.toml +++ b/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 diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 5c963399e3..a945fee2b2 100644 --- a/crates/copilot/src/copilot.rs +++ b/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, node_runtime: NodeRuntime, server: CopilotServer, buffers: HashSet>, @@ -337,13 +332,11 @@ impl Copilot { fn start( new_server_id: LanguageServerId, - http: Arc, node_runtime: NodeRuntime, cx: &mut Context, ) -> 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, node_runtime: NodeRuntime, env: Option>, this: WeakEntity, @@ -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 = 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::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::(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) -> anyhow::Result { - const SERVER_PATH: &str = "dist/language-server.js"; +async fn get_copilot_lsp(node_runtime: NodeRuntime) -> anyhow::Result { + const PACKAGE_NAME: &str = "@github/copilot-language-server"; + const SERVER_PATH: &str = + "node_modules/@github/copilot-language-server/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) -> anyhow::Result { - let release = - get_release_by_tag_name("zed-industries/copilot", "v0.7.0", http.clone()).await?; + let latest_version = node_runtime + .npm_package_latest_version(PACKAGE_NAME) + .await?; + let server_path = paths::copilot_dir().join(SERVER_PATH); - 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) + 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)]