copilot: Bypass Copilot LSP node shim (#56635)

We currently run node on the JS wrapper around the native binaries
shipped with https://github.com/github/copilot-language-server-release.
According to their README, this is not required, and it seems like it is
just an option provided so that you can run the server with `npx`.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #55891

Release Notes:

- Stopped relying on node for running the Copilot language server that
provides edit predictions. The system node version should no longer
affect whether Copilot edit predictions work in Zed
This commit is contained in:
Ben Kunkle authored and GitHub committed 2026-05-13 14:48:39 +00:00
1 parent 41a9074e46
commit b921a1f1ca
3 files changed
+47 -59

No files matched your search

Generated
-1
View File
@@ -3665,7 +3665,6 @@ dependencies = [
"pretty_assertions",
"project",
"rpc",
"semver",
"serde",
"serde_json",
"settings",
-1
View File
@@ -40,7 +40,6 @@ node_runtime.workspace = true
parking_lot.workspace = true
paths.workspace = true
project.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
+47 -57
View File
@@ -27,7 +27,6 @@ use parking_lot::Mutex;
use project::project_settings::ProjectSettings;
use project::{DisableAiSettings, Project};
use request::DidChangeStatus;
use semver::Version;
use serde_json::json;
use settings::{Settings, SettingsStore};
use std::{
@@ -567,17 +566,11 @@ impl Copilot {
cx: &mut AsyncApp,
) {
let start_language_server = async {
let server_path = get_copilot_lsp(fs, node_runtime.clone()).await?;
let node_path = node_runtime.binary_path().await?;
ensure_node_version_for_copilot(&node_path).await?;
let server_path = get_copilot_lsp(fs, node_runtime).await?;
let arguments: Vec<OsString> = vec![
"--experimental-sqlite".into(),
server_path.into(),
"--stdio".into(),
];
let arguments: Vec<OsString> = vec!["--stdio".into()];
let binary = LanguageServerBinary {
path: node_path,
path: server_path,
arguments,
env,
};
@@ -1396,44 +1389,6 @@ async fn clear_copilot_config_dir() {
remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
}
async fn ensure_node_version_for_copilot(node_path: &Path) -> anyhow::Result<()> {
const MIN_COPILOT_NODE_VERSION: Version = Version::new(20, 8, 0);
log::info!("Checking Node.js version for Copilot at: {:?}", node_path);
let output = util::command::new_command(node_path)
.arg("--version")
.output()
.await
.with_context(|| format!("checking Node.js version at {:?}", node_path))?;
if !output.status.success() {
anyhow::bail!(
"failed to run node --version for Copilot. stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
let version_str = String::from_utf8_lossy(&output.stdout);
let version = Version::parse(version_str.trim().trim_start_matches('v'))
.with_context(|| format!("parsing Node.js version from '{}'", version_str.trim()))?;
if version < MIN_COPILOT_NODE_VERSION {
anyhow::bail!(
"GitHub Copilot language server requires Node.js {MIN_COPILOT_NODE_VERSION} or later, but found {version}. \
Please update your Node.js version or configure a different Node.js path in settings."
);
}
log::info!(
"Node.js version {} meets Copilot requirements (>= {})",
version,
MIN_COPILOT_NODE_VERSION
);
Ok(())
}
async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::Result<PathBuf> {
const PACKAGE_NAME: &str = "@github/copilot-language-server";
const SERVER_PATH: &str =
@@ -1443,17 +1398,19 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
.npm_package_latest_version(PACKAGE_NAME)
.await?;
let server_path = paths::copilot_dir().join(SERVER_PATH);
let binary_path = copilot_lsp_native_binary_path()?;
fs.create_dir(paths::copilot_dir()).await?;
let should_install = node_runtime
.should_install_npm_package(
PACKAGE_NAME,
&server_path,
paths::copilot_dir(),
VersionStrategy::Latest(&latest_version),
)
.await;
let should_install = !fs.is_file(&binary_path).await
|| node_runtime
.should_install_npm_package(
PACKAGE_NAME,
&server_path,
paths::copilot_dir(),
VersionStrategy::Latest(&latest_version),
)
.await;
if should_install {
node_runtime
.npm_install_packages(
@@ -1463,7 +1420,40 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
.await?;
}
Ok(server_path)
if fs.is_file(&binary_path).await {
return Ok(binary_path);
}
anyhow::bail!("GitHub Copilot native language server binary was not installed")
}
fn copilot_lsp_native_binary_path() -> anyhow::Result<PathBuf> {
let platform = match env::consts::OS {
"linux" => "linux",
"macos" => "darwin",
"windows" => "win32",
platform => anyhow::bail!("unsupported Copilot language server platform: {platform}"),
};
let architecture = match env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "x64",
architecture => {
anyhow::bail!("unsupported Copilot language server architecture: {architecture}")
}
};
let package_name = format!("copilot-language-server-{platform}-{architecture}");
let executable_name = if cfg!(target_os = "windows") {
"copilot-language-server.exe"
} else {
"copilot-language-server"
};
Ok(paths::copilot_dir()
.join("node_modules")
.join("@github")
.join(package_name)
.join(executable_name))
}
#[cfg(test)]