diff --git a/Cargo.lock b/Cargo.lock index 02b27566e4..0e1f5a807c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1113,7 +1113,6 @@ dependencies = [ "futures 0.3.25", "gpui", "image", - "isahc", "lazy_static", "log", "parking_lot 0.11.2", @@ -1332,6 +1331,22 @@ dependencies = [ "theme", ] +[[package]] +name = "copilot" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-compression", + "client", + "futures 0.3.25", + "gpui", + "lsp", + "settings", + "smol", + "util", + "workspace", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -7500,11 +7515,15 @@ dependencies = [ "dirs 3.0.2", "futures 0.3.25", "git2", + "isahc", "lazy_static", "log", "rand 0.8.5", + "serde", "serde_json", + "smol", "tempdir", + "url", ] [[package]] @@ -8460,6 +8479,7 @@ dependencies = [ "collections", "command_palette", "context_menu", + "copilot", "ctor", "db", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index 9f795992d5..bf9214f49e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/collections", "crates/command_palette", "crates/context_menu", + "crates/copilot", "crates/db", "crates/diagnostics", "crates/drag_and_drop", diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 4272d7b1af..3ad3380d26 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,8 +1,7 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; -use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; -use client::{ZED_APP_PATH, ZED_APP_VERSION}; +use client::{ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, @@ -14,6 +13,7 @@ use smol::{fs::File, io::AsyncReadExt, process::Command}; use std::{ffi::OsString, sync::Arc, time::Duration}; use update_notification::UpdateNotification; use util::channel::ReleaseChannel; +use util::http::HttpClient; use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index cb6f29a42e..9c772f519b 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -23,7 +23,6 @@ async-recursion = "0.3" async-tungstenite = { version = "0.16", features = ["async-tls"] } futures = "0.3" image = "0.23" -isahc = "1.7" lazy_static = "1.4.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 86d6bc9912..bb39b06699 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,7 +1,6 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; -pub mod http; pub mod telemetry; pub mod user; @@ -18,7 +17,6 @@ use gpui::{ AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion, AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; -use http::HttpClient; use lazy_static::lazy_static; use parking_lot::RwLock; use postage::watch; @@ -41,6 +39,7 @@ use telemetry::Telemetry; use thiserror::Error; use url::Url; use util::channel::ReleaseChannel; +use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; pub use rpc::*; @@ -130,7 +129,7 @@ pub enum EstablishConnectionError { #[error("{0}")] Other(#[from] anyhow::Error), #[error("{0}")] - Http(#[from] http::Error), + Http(#[from] util::http::Error), #[error("{0}")] Io(#[from] std::io::Error), #[error("{0}")] @@ -1396,10 +1395,11 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { #[cfg(test)] mod tests { use super::*; - use crate::test::{FakeHttpClient, FakeServer}; + use crate::test::FakeServer; use gpui::{executor::Deterministic, TestAppContext}; use parking_lot::Mutex; use std::future; + use util::http::FakeHttpClient; #[gpui::test(iterations = 10)] async fn test_reconnection(cx: &mut TestAppContext) { diff --git a/crates/client/src/http.rs b/crates/client/src/http.rs deleted file mode 100644 index 0757cebf3a..0000000000 --- a/crates/client/src/http.rs +++ /dev/null @@ -1,57 +0,0 @@ -pub use anyhow::{anyhow, Result}; -use futures::future::BoxFuture; -use isahc::{ - config::{Configurable, RedirectPolicy}, - AsyncBody, -}; -pub use isahc::{ - http::{Method, Uri}, - Error, -}; -use smol::future::FutureExt; -use std::{sync::Arc, time::Duration}; -pub use url::Url; - -pub type Request = isahc::Request; -pub type Response = isahc::Response; - -pub trait HttpClient: Send + Sync { - fn send(&self, req: Request) -> BoxFuture>; - - fn get<'a>( - &'a self, - uri: &str, - body: AsyncBody, - follow_redirects: bool, - ) -> BoxFuture<'a, Result> { - let request = isahc::Request::builder() - .redirect_policy(if follow_redirects { - RedirectPolicy::Follow - } else { - RedirectPolicy::None - }) - .method(Method::GET) - .uri(uri) - .body(body); - match request { - Ok(request) => self.send(request), - Err(error) => async move { Err(error.into()) }.boxed(), - } - } -} - -pub fn client() -> Arc { - Arc::new( - isahc::HttpClient::builder() - .connect_timeout(Duration::from_secs(5)) - .low_speed_timeout(100, Duration::from_secs(5)) - .build() - .unwrap(), - ) -} - -impl HttpClient for isahc::HttpClient { - fn send(&self, req: Request) -> BoxFuture> { - Box::pin(async move { self.send_async(req).await }) - } -} diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 9d486619d2..7ee099dfab 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,11 +1,9 @@ -use crate::http::HttpClient; use db::kvp::KEY_VALUE_STORE; use gpui::{ executor::Background, serde_json::{self, value::Map, Value}, AppContext, Task, }; -use isahc::Request; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; @@ -19,6 +17,7 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; use tempfile::NamedTempFile; +use util::http::HttpClient; use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -220,10 +219,10 @@ impl Telemetry { "App": true }), }])?; - let request = Request::post(MIXPANEL_ENGAGE_URL) - .header("Content-Type", "application/json") - .body(json_bytes.into())?; - this.http_client.send(request).await?; + + this.http_client + .post_json(MIXPANEL_ENGAGE_URL, json_bytes.into()) + .await?; anyhow::Ok(()) } .log_err(), @@ -316,10 +315,9 @@ impl Telemetry { json_bytes.clear(); serde_json::to_writer(&mut json_bytes, &events)?; - let request = Request::post(MIXPANEL_EVENTS_URL) - .header("Content-Type", "application/json") - .body(json_bytes.into())?; - this.http_client.send(request).await?; + this.http_client + .post_json(MIXPANEL_EVENTS_URL, json_bytes.into()) + .await?; anyhow::Ok(()) } .log_err(), diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index db9e0d8c48..4c12a20566 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -1,16 +1,14 @@ -use crate::{ - http::{self, HttpClient, Request, Response}, - Client, Connection, Credentials, EstablishConnectionError, UserStore, -}; +use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; use anyhow::{anyhow, Result}; -use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt}; +use futures::{stream::BoxStream, StreamExt}; use gpui::{executor, ModelHandle, TestAppContext}; use parking_lot::Mutex; use rpc::{ proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, ConnectionId, Peer, Receipt, TypedEnvelope, }; -use std::{fmt, rc::Rc, sync::Arc}; +use std::{rc::Rc, sync::Arc}; +use util::http::FakeHttpClient; pub struct FakeServer { peer: Arc, @@ -219,46 +217,3 @@ impl Drop for FakeServer { self.disconnect(); } } - -pub struct FakeHttpClient { - handler: Box< - dyn 'static - + Send - + Sync - + Fn(Request) -> BoxFuture<'static, Result>, - >, -} - -impl FakeHttpClient { - pub fn create(handler: F) -> Arc - where - Fut: 'static + Send + Future>, - F: 'static + Send + Sync + Fn(Request) -> Fut, - { - Arc::new(Self { - handler: Box::new(move |req| Box::pin(handler(req))), - }) - } - - pub fn with_404_response() -> Arc { - Self::create(|_| async move { - Ok(isahc::Response::builder() - .status(404) - .body(Default::default()) - .unwrap()) - }) - } -} - -impl fmt::Debug for FakeHttpClient { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("FakeHttpClient").finish() - } -} - -impl HttpClient for FakeHttpClient { - fn send(&self, req: Request) -> BoxFuture> { - let future = (self.handler)(req); - Box::pin(async move { future.await.map(Into::into) }) - } -} diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a0a730871d..8c6b141001 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,4 +1,4 @@ -use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; +use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; @@ -7,6 +7,7 @@ use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use settings::Settings; use std::sync::{Arc, Weak}; +use util::http::HttpClient; use util::{StaffMode, TryFutureExt as _}; #[derive(Default, Debug)] diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 91af40dc5a..9c0f9f3bd8 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -7,8 +7,7 @@ use crate::{ use anyhow::anyhow; use call::ActiveCall; use client::{ - self, proto::PeerId, test::FakeHttpClient, Client, Connection, Credentials, - EstablishConnectionError, UserStore, + self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; use collections::{HashMap, HashSet}; use fs::FakeFs; @@ -28,6 +27,7 @@ use std::{ }, }; use theme::ThemeRegistry; +use util::http::FakeHttpClient; use workspace::Workspace; mod integration_tests; diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml new file mode 100644 index 0000000000..bd79d053d1 --- /dev/null +++ b/crates/copilot/Cargo.toml @@ -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" diff --git a/crates/copilot/readme.md b/crates/copilot/readme.md new file mode 100644 index 0000000000..a916081970 --- /dev/null +++ b/crates/copilot/readme.md @@ -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::>) 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? diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs new file mode 100644 index 0000000000..097f3187b8 --- /dev/null +++ b/crates/copilot/src/copilot.rs @@ -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, 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, cx: &mut MutableAppContext) { + let copilot = cx.global::>>().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) -> anyhow::Result { + ///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 = 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, + ::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 + } + } +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 69053e9fc4..427f5e62f6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -10,7 +10,6 @@ mod buffer_tests; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use collections::HashMap; use futures::{ channel::oneshot, @@ -45,6 +44,7 @@ use syntax_map::SyntaxSnapshot; use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; +use util::http::HttpClient; use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; #[cfg(any(test, feature = "test-support"))] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fedfa0c863..8e3fc77aa8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -568,7 +568,7 @@ impl Project { let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background()); - let http_client = client::test::FakeHttpClient::with_404_response(); + let http_client = util::http::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let project = diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1d40dad864..2357052d2c 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3114,13 +3114,14 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { #[cfg(test)] mod tests { use super::*; - use client::test::FakeHttpClient; use fs::repository::FakeGitRepository; use fs::{FakeFs, RealFs}; use gpui::{executor::Deterministic, TestAppContext}; use rand::prelude::*; use serde_json::json; use std::{env, fmt::Write}; + use util::http::FakeHttpClient; + use util::test::temp_tree; #[gpui::test] diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index b13b8af956..558ca588b4 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -14,11 +14,15 @@ test-support = ["tempdir", "git2"] [dependencies] anyhow = "1.0.38" backtrace = "0.3" -futures = "0.3" log = { version = "0.4.16", features = ["kv_unstable_serde"] } lazy_static = "1.4.0" +futures = "0.3" +isahc = "1.7" +smol = "1.2.5" +url = "2.2" rand = { workspace = true } tempdir = { version = "0.3.7", optional = true } +serde = { version = "1.0", features = ["derive", "rc"] } serde_json = { version = "1.0", features = ["preserve_order"] } git2 = { version = "0.15", default-features = false, optional = true } dirs = "3.0" diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs new file mode 100644 index 0000000000..c6d562d15c --- /dev/null +++ b/crates/util/src/fs.rs @@ -0,0 +1,28 @@ +use std::path::Path; + +use smol::{fs, stream::StreamExt}; + +use crate::ResultExt; + +// Removes all files and directories matching the given predicate +pub async fn remove_matching(dir: &Path, predicate: F) +where + F: Fn(&Path) -> bool, +{ + if let Some(mut entries) = fs::read_dir(dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if predicate(entry_path.as_path()) { + if let Ok(metadata) = fs::metadata(&entry_path).await { + if metadata.is_file() { + fs::remove_file(&entry_path).await.log_err(); + } else { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + } +} diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs new file mode 100644 index 0000000000..33c0ea6a1a --- /dev/null +++ b/crates/util/src/github.rs @@ -0,0 +1,40 @@ +use crate::http::HttpClient; +use anyhow::{Context, Result}; +use futures::AsyncReadExt; +use serde::Deserialize; +use std::sync::Arc; + +#[derive(Deserialize)] +pub struct GithubRelease { + pub name: String, + pub assets: Vec, +} + +#[derive(Deserialize)] +pub struct GithubReleaseAsset { + pub name: String, + pub browser_download_url: String, +} + +pub async fn latest_github_release( + repo_name_with_owner: &str, + http: Arc, +) -> Result { + let mut response = http + .get( + &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"), + Default::default(), + true, + ) + .await + .context("error fetching latest release")?; + let mut body = Vec::new(); + response + .body_mut() + .read_to_end(&mut body) + .await + .context("error reading latest release")?; + let release: GithubRelease = + serde_json::from_slice(body.as_slice()).context("error deserializing latest release")?; + Ok(release) +} diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs new file mode 100644 index 0000000000..e29768a53e --- /dev/null +++ b/crates/util/src/http.rs @@ -0,0 +1,117 @@ +pub use anyhow::{anyhow, Result}; +use futures::future::BoxFuture; +use isahc::config::{Configurable, RedirectPolicy}; +pub use isahc::{ + http::{Method, Uri}, + Error, +}; +pub use isahc::{AsyncBody, Request, Response}; +use smol::future::FutureExt; +#[cfg(feature = "test-support")] +use std::fmt; +use std::{sync::Arc, time::Duration}; +pub use url::Url; + +pub trait HttpClient: Send + Sync { + fn send(&self, req: Request) -> BoxFuture, Error>>; + + fn get<'a>( + &'a self, + uri: &str, + body: AsyncBody, + follow_redirects: bool, + ) -> BoxFuture<'a, Result, Error>> { + let request = isahc::Request::builder() + .redirect_policy(if follow_redirects { + RedirectPolicy::Follow + } else { + RedirectPolicy::None + }) + .method(Method::GET) + .uri(uri) + .body(body); + match request { + Ok(request) => self.send(request), + Err(error) => async move { Err(error.into()) }.boxed(), + } + } + + fn post_json<'a>( + &'a self, + uri: &str, + body: AsyncBody, + ) -> BoxFuture<'a, Result, Error>> { + let request = isahc::Request::builder() + .method(Method::POST) + .uri(uri) + .header("Content-Type", "application/json") + .body(body); + match request { + Ok(request) => self.send(request), + Err(error) => async move { Err(error.into()) }.boxed(), + } + } +} + +pub fn client() -> Arc { + Arc::new( + isahc::HttpClient::builder() + .connect_timeout(Duration::from_secs(5)) + .low_speed_timeout(100, Duration::from_secs(5)) + .build() + .unwrap(), + ) +} + +impl HttpClient for isahc::HttpClient { + fn send(&self, req: Request) -> BoxFuture, Error>> { + Box::pin(async move { self.send_async(req).await }) + } +} + +#[cfg(feature = "test-support")] +pub struct FakeHttpClient { + handler: Box< + dyn 'static + + Send + + Sync + + Fn(Request) -> BoxFuture<'static, Result, Error>>, + >, +} + +#[cfg(feature = "test-support")] +impl FakeHttpClient { + pub fn create(handler: F) -> Arc + where + Fut: 'static + Send + futures::Future, Error>>, + F: 'static + Send + Sync + Fn(Request) -> Fut, + { + Arc::new(Self { + handler: Box::new(move |req| Box::pin(handler(req))), + }) + } + + pub fn with_404_response() -> Arc { + Self::create(|_| async move { + Ok(Response::builder() + .status(404) + .body(Default::default()) + .unwrap()) + }) + } +} + +#[cfg(feature = "test-support")] +impl fmt::Debug for FakeHttpClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FakeHttpClient").finish() + } +} + +#[cfg(feature = "test-support")] +impl HttpClient for FakeHttpClient { + fn send(&self, req: Request) -> BoxFuture, Error>> { + let future = (self.handler)(req); + Box::pin(async move { future.await.map(Into::into) }) + } +} diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 63c3c6d884..e38f76d8a6 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -6,6 +6,7 @@ lazy_static::lazy_static! { pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); + pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot"); pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 6a5ccb8bd5..07b2ffd0da 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1,4 +1,7 @@ pub mod channel; +pub mod fs; +pub mod github; +pub mod http; pub mod paths; #[cfg(any(test, feature = "test-support"))] pub mod test; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fe60065486..eb04e05286 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -449,7 +449,7 @@ impl AppState { let fs = fs::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::test()); - let http_client = client::test::FakeHttpClient::with_404_response(); + let http_client = util::http::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let themes = ThemeRegistry::new((), cx.font_cache().clone()); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4d7ce828d6..812fae9e0a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -28,6 +28,7 @@ command_palette = { path = "../command_palette" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } +copilot = { path = "../copilot" } diagnostics = { path = "../diagnostics" } db = { path = "../db" } editor = { path = "../editor" } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index c49c77f076..3a23afb970 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,11 +1,11 @@ use anyhow::Context; -use client::http::HttpClient; use gpui::executor::Background; pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; use theme::ThemeRegistry; +use util::http::HttpClient; mod c; mod elixir; diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 906592fc2d..88f5c4553b 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -1,13 +1,16 @@ -use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; pub use language::*; use smol::fs::{self, File}; use std::{any::Any, path::PathBuf, sync::Arc}; +use util::fs::remove_matching; +use util::github::latest_github_release; +use util::http::HttpClient; use util::ResultExt; +use super::github::GitHubLspBinaryVersion; + pub struct CLspAdapter; #[async_trait] @@ -69,16 +72,7 @@ impl super::LspAdapter for CLspAdapter { Err(anyhow!("failed to unzip clangd archive"))?; } - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index 9f921a0c40..ecd4028fe0 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -1,14 +1,17 @@ -use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; pub use language::*; use lsp::{CompletionItemKind, SymbolKind}; use smol::fs::{self, File}; use std::{any::Any, path::PathBuf, sync::Arc}; +use util::fs::remove_matching; +use util::github::latest_github_release; +use util::http::HttpClient; use util::ResultExt; +use super::github::GitHubLspBinaryVersion; + pub struct ElixirLspAdapter; #[async_trait] @@ -76,22 +79,7 @@ impl LspAdapter for ElixirLspAdapter { Err(anyhow!("failed to unzip clangd archive"))?; } - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - if let Ok(metadata) = fs::metadata(&entry_path).await { - if metadata.is_file() { - fs::remove_file(&entry_path).await.log_err(); - } else { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/github.rs b/crates/zed/src/languages/github.rs index 8fdef50790..9e0dd9b582 100644 --- a/crates/zed/src/languages/github.rs +++ b/crates/zed/src/languages/github.rs @@ -1,8 +1,8 @@ use anyhow::{Context, Result}; -use client::http::HttpClient; use serde::Deserialize; use smol::io::AsyncReadExt; use std::sync::Arc; +use util::http::HttpClient; pub struct GitHubLspBinaryVersion { pub name: String, diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 9af309839f..760c5f353d 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -1,13 +1,15 @@ -use super::github::latest_github_release; use anyhow::{anyhow, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; pub use language::*; use lazy_static::lazy_static; use regex::Regex; use smol::{fs, process}; -use std::{any::Any, ffi::OsString, ops::Range, path::PathBuf, str, sync::Arc}; +use std::ffi::{OsStr, OsString}; +use std::{any::Any, ops::Range, path::PathBuf, str, sync::Arc}; +use util::fs::remove_matching; +use util::github::latest_github_release; +use util::http::HttpClient; use util::ResultExt; fn server_binary_arguments() -> Vec { @@ -55,18 +57,10 @@ impl super::LspAdapter for GoLspAdapter { let binary_path = container_dir.join(&format!("gopls_{version}")); if let Ok(metadata) = fs::metadata(&binary_path).await { if metadata.is_file() { - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != binary_path - && entry.file_name() != "gobin" - { - fs::remove_file(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| { + entry != binary_path && entry.file_name() != Some(OsStr::new("gobin")) + }) + .await; return Ok(LanguageServerBinary { path: binary_path.to_path_buf(), diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index a2cfbac96b..f77b264fbf 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -1,17 +1,15 @@ use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use serde_json::json; use smol::fs; -use std::{ - any::Any, - ffi::OsString, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::ffi::OsString; +use std::path::Path; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::fs::remove_matching; +use util::http::HttpClient; use util::ResultExt; fn server_binary_arguments(server_path: &Path) -> Vec { @@ -69,16 +67,7 @@ impl LspAdapter for HtmlLspAdapter { ) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(container_dir.as_path(), |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 479308f370..97c158fd1f 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,7 +1,6 @@ use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use collections::HashMap; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::MutableAppContext; @@ -17,6 +16,7 @@ use std::{ sync::Arc, }; use theme::ThemeRegistry; +use util::{fs::remove_matching, http::HttpClient}; use util::{paths, ResultExt, StaffMode}; const SERVER_PATH: &'static str = @@ -84,16 +84,7 @@ impl LspAdapter for JsonLspAdapter { ) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != server_path).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs index 38f50d2d88..9b82713d08 100644 --- a/crates/zed/src/languages/language_plugin.rs +++ b/crates/zed/src/languages/language_plugin.rs @@ -1,12 +1,12 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use client::http::HttpClient; use collections::HashMap; use futures::lock::Mutex; use gpui::executor::Background; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn}; use std::{any::Any, path::PathBuf, sync::Arc}; +use util::http::HttpClient; use util::ResultExt; #[allow(dead_code)] diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 7ffdac5218..f16761d870 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -1,16 +1,14 @@ -use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc}; - use anyhow::{anyhow, bail, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; -use client::http::HttpClient; use futures::{io::BufReader, StreamExt}; use language::{LanguageServerBinary, LanguageServerName}; use smol::fs; -use util::{async_iife, ResultExt}; +use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc}; +use util::{async_iife, github::latest_github_release, http::HttpClient, ResultExt}; -use super::github::{latest_github_release, GitHubLspBinaryVersion}; +use super::github::GitHubLspBinaryVersion; #[derive(Copy, Clone)] pub struct LuaLspAdapter; diff --git a/crates/zed/src/languages/node_runtime.rs b/crates/zed/src/languages/node_runtime.rs index 41cbefbb73..079b6a5e45 100644 --- a/crates/zed/src/languages/node_runtime.rs +++ b/crates/zed/src/languages/node_runtime.rs @@ -1,7 +1,6 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; -use client::http::HttpClient; use futures::{future::Shared, FutureExt}; use gpui::{executor::Background, Task}; use parking_lot::Mutex; @@ -12,6 +11,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use util::http::HttpClient; const VERSION: &str = "v18.15.0"; diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 9a09c63bb6..3a671c60f6 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -1,7 +1,6 @@ use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use smol::fs; @@ -11,6 +10,8 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use util::fs::remove_matching; +use util::http::HttpClient; use util::ResultExt; fn server_binary_arguments(server_path: &Path) -> Vec { @@ -60,16 +61,7 @@ impl LspAdapter for PythonLspAdapter { .npm_install_packages([("pyright", version.as_str())], &version_dir) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/ruby.rs b/crates/zed/src/languages/ruby.rs index 662c1f464d..d387f815f0 100644 --- a/crates/zed/src/languages/ruby.rs +++ b/crates/zed/src/languages/ruby.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use client::http::HttpClient; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use std::{any::Any, path::PathBuf, sync::Arc}; +use util::http::HttpClient; pub struct RubyLanguageServer; diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 0f8e90d7b2..b95a64fa1e 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -2,13 +2,14 @@ use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; -use client::http::HttpClient; use futures::{io::BufReader, StreamExt}; pub use language::*; use lazy_static::lazy_static; use regex::Regex; use smol::fs::{self, File}; use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use util::fs::remove_matching; +use util::http::HttpClient; use util::ResultExt; pub struct RustLspAdapter; @@ -60,16 +61,7 @@ impl LspAdapter for RustLspAdapter { ) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != destination_path { - fs::remove_file(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != destination_path).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index f9baf4f8f7..d3704c84c8 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,7 +1,6 @@ use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use serde_json::json; @@ -12,6 +11,8 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use util::fs::remove_matching; +use util::http::HttpClient; use util::ResultExt; fn server_binary_arguments(server_path: &Path) -> Vec { @@ -90,16 +91,7 @@ impl LspAdapter for TypeScriptLspAdapter { ) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index b6e82842de..6028ecd134 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -1,7 +1,6 @@ use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::MutableAppContext; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; @@ -16,6 +15,7 @@ use std::{ sync::Arc, }; use util::ResultExt; +use util::{fs::remove_matching, http::HttpClient}; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -68,16 +68,7 @@ impl LspAdapter for YamlLspAdapter { .npm_install_packages([("yaml-language-server", version.as_str())], &version_dir) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index fb6c6227c3..c88f2e94f9 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -8,11 +8,7 @@ use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, }; -use client::{ - self, - http::{self, HttpClient}, - UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, -}; +use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; use futures::{ channel::{mpsc, oneshot}, @@ -36,6 +32,7 @@ use std::{ path::PathBuf, sync::Arc, thread, time::Duration, }; use terminal_view::{get_working_directory, TerminalView}; +use util::http::{self, HttpClient}; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; @@ -165,6 +162,7 @@ fn main() { terminal_view::init(cx); theme_testbench::init(cx); recent_projects::init(cx); + copilot::init(client.clone(), cx); cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) .detach(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 788be77e75..32706cb47f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -652,7 +652,6 @@ fn open_bundled_file( mod tests { use super::*; use assets::Assets; - use client::test::FakeHttpClient; use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use gpui::{ executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle, @@ -665,6 +664,7 @@ mod tests { path::{Path, PathBuf}, }; use theme::ThemeRegistry; + use util::http::FakeHttpClient; use workspace::{ item::{Item, ItemHandle}, open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle,