Add copilot crate
Refactor HTTP and github release downloading into util Lazily download / upgrade the copilot LSP from Zed Co-authored-by: Max <max@zed.dev> Co-Authored-By: Antonio <antonio@zed.dev>
This commit is contained in:
parent
35b2aceffb
commit
455cdc8b37
41 changed files with 435 additions and 265 deletions
21
crates/copilot/Cargo.toml
Normal file
21
crates/copilot/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "copilot"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/copilot.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
settings = { path = "../settings" }
|
||||
lsp = { path = "../lsp" }
|
||||
util = { path = "../util" }
|
||||
client = { path = "../client" }
|
||||
workspace = { path = "../workspace" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
anyhow = "1.0"
|
||||
smol = "1.2.5"
|
||||
futures = "0.3"
|
21
crates/copilot/readme.md
Normal file
21
crates/copilot/readme.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
Basic idea:
|
||||
|
||||
Run the `copilot-node-server` as an LSP
|
||||
Reuse our LSP code to use it
|
||||
|
||||
Issues:
|
||||
- Re-use our github authentication for copilot - ??
|
||||
- Integrate Copilot suggestions with `SuggestionMap`
|
||||
|
||||
|
||||
|
||||
THE PLAN:
|
||||
- Copilot crate.
|
||||
- Instantiated with a project / listens to them
|
||||
- Listens to events from the project about adding worktrees
|
||||
- Manages the copilot language servers per worktree
|
||||
- Editor <-?-> Copilot
|
||||
|
||||
|
||||
From anotonio in Slack:
|
||||
- soooo regarding copilot i was thinking… if it doesn’t really behave like a language server (but they implemented like that because of the protocol, etc.), it might be nice to just have a singleton that is not even set when we’re signed out. when we sign in, we set the global. then, the editor can access the global (e.g. cx.global::<Option<Copilot>>) after typing some character (and with some debouncing mechanism). the Copilot struct could hold a lsp::LanguageServer and then our job is to write an adapter that can then be used to start the language server, but it’s kinda orthogonal to the language servers we store in the project. what do you think?
|
97
crates/copilot/src/copilot.rs
Normal file
97
crates/copilot/src/copilot.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use anyhow::{anyhow, Ok};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use client::Client;
|
||||
use gpui::{actions, MutableAppContext};
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
use std::{env::consts, path::PathBuf, sync::Arc};
|
||||
use util::{
|
||||
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
|
||||
};
|
||||
|
||||
actions!(copilot, [SignIn]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action(move |_: &SignIn, cx: &mut MutableAppContext| {
|
||||
Copilot::sign_in(client.http_client(), cx)
|
||||
});
|
||||
}
|
||||
|
||||
struct Copilot {
|
||||
copilot_server: PathBuf,
|
||||
}
|
||||
|
||||
impl Copilot {
|
||||
fn sign_in(http: Arc<dyn HttpClient>, cx: &mut MutableAppContext) {
|
||||
let copilot = cx.global::<Option<Arc<Copilot>>>().clone();
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
// Lazily download / initialize copilot LSP
|
||||
let copilot = if let Some(copilot) = copilot {
|
||||
copilot
|
||||
} else {
|
||||
let copilot_server = get_lsp_binary(http).await?; // TODO: Make this error user visible
|
||||
let new_copilot = Arc::new(Copilot { copilot_server });
|
||||
cx.update({
|
||||
let new_copilot = new_copilot.clone();
|
||||
move |cx| cx.set_global(Some(new_copilot.clone()))
|
||||
});
|
||||
new_copilot
|
||||
};
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_lsp_binary(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
///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 = latest_github_release("zed-industries/copilotserver", http.clone()).await?;
|
||||
let asset_name = format!("copilot-darwin-{}.gz", consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
|
||||
|
||||
let destination_path =
|
||||
paths::COPILOT_DIR.join(format!("copilot-{}-{}", release.name, consts::ARCH));
|
||||
|
||||
if fs::metadata(&destination_path).await.is_err() {
|
||||
let mut response = http
|
||||
.get(&asset.browser_download_url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let mut file = fs::File::create(&destination_path).await?;
|
||||
futures::io::copy(decompressed_bytes, &mut file).await?;
|
||||
fs::set_permissions(
|
||||
&destination_path,
|
||||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||||
)
|
||||
.await?;
|
||||
|
||||
remove_matching(&paths::COPILOT_DIR, |entry| entry != destination_path).await;
|
||||
}
|
||||
|
||||
Ok(destination_path)
|
||||
}
|
||||
|
||||
match fetch_latest(http).await {
|
||||
ok @ Result::Ok(..) => ok,
|
||||
e @ Err(..) => {
|
||||
e.log_err();
|
||||
// Fetch a cached binary, if it exists
|
||||
(|| async move {
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
last.ok_or_else(|| anyhow!("no cached binary"))
|
||||
})()
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue