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).
This commit is contained in:
Marshall Bowers 2025-03-24 19:35:29 -04:00 committed by GitHub
parent be83c5e1c5
commit a52095f558
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 42 additions and 99 deletions

3
Cargo.lock generated
View file

@ -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",

View file

@ -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

View file

@ -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";
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";
///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 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)]