copilot.rs

  1use anyhow::{anyhow, Ok};
  2use async_compression::futures::bufread::GzipDecoder;
  3use client::Client;
  4use gpui::{actions, MutableAppContext};
  5use smol::{fs, io::BufReader, stream::StreamExt};
  6use std::{env::consts, path::PathBuf, sync::Arc};
  7use util::{
  8    fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
  9};
 10
 11actions!(copilot, [SignIn]);
 12
 13pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
 14    cx.add_global_action(move |_: &SignIn, cx: &mut MutableAppContext| {
 15        Copilot::sign_in(client.http_client(), cx)
 16    });
 17}
 18
 19#[derive(Debug)]
 20struct Copilot {
 21    copilot_server: PathBuf,
 22}
 23
 24impl Copilot {
 25    fn sign_in(http: Arc<dyn HttpClient>, cx: &mut MutableAppContext) {
 26        let maybe_copilot = cx.default_global::<Option<Arc<Copilot>>>().clone();
 27
 28        cx.spawn(|mut cx| async move {
 29            // Lazily download / initialize copilot LSP
 30            let copilot = if let Some(copilot) = maybe_copilot {
 31                copilot
 32            } else {
 33                let copilot_server = get_lsp_binary(http).await?; // TODO: Make this error user visible
 34                let new_copilot = Arc::new(Copilot { copilot_server });
 35                cx.update({
 36                    let new_copilot = new_copilot.clone();
 37                    move |cx| cx.set_global(Some(new_copilot.clone()))
 38                });
 39                new_copilot
 40            };
 41
 42            dbg!(copilot);
 43
 44            Ok(())
 45        })
 46        .detach();
 47    }
 48}
 49
 50async fn get_lsp_binary(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
 51    ///Check for the latest copilot language server and download it if we haven't already
 52    async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
 53        let release = latest_github_release("zed-industries/copilotserver", http.clone()).await?;
 54        let asset_name = format!("copilot-darwin-{}.gz", consts::ARCH);
 55        let asset = release
 56            .assets
 57            .iter()
 58            .find(|asset| asset.name == asset_name)
 59            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
 60
 61        let destination_path =
 62            paths::COPILOT_DIR.join(format!("copilot-{}-{}", release.name, consts::ARCH));
 63
 64        if fs::metadata(&destination_path).await.is_err() {
 65            let mut response = http
 66                .get(&asset.browser_download_url, Default::default(), true)
 67                .await
 68                .map_err(|err| anyhow!("error downloading release: {}", err))?;
 69            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
 70            let mut file = fs::File::create(&destination_path).await?;
 71            futures::io::copy(decompressed_bytes, &mut file).await?;
 72            fs::set_permissions(
 73                &destination_path,
 74                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
 75            )
 76            .await?;
 77
 78            remove_matching(&paths::COPILOT_DIR, |entry| entry != destination_path).await;
 79        }
 80
 81        Ok(destination_path)
 82    }
 83
 84    match fetch_latest(http).await {
 85        ok @ Result::Ok(..) => ok,
 86        e @ Err(..) => {
 87            e.log_err();
 88            // Fetch a cached binary, if it exists
 89            (|| async move {
 90                let mut last = None;
 91                let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?;
 92                while let Some(entry) = entries.next().await {
 93                    last = Some(entry?.path());
 94                }
 95                last.ok_or_else(|| anyhow!("no cached binary"))
 96            })()
 97            .await
 98        }
 99    }
100}