From 455cdc8b37016563cb359ba10286b5e7a94b5264 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 22 Mar 2023 19:22:08 -0700 Subject: [PATCH 01/53] Add copilot crate Refactor HTTP and github release downloading into util Lazily download / upgrade the copilot LSP from Zed Co-authored-by: Max Co-Authored-By: Antonio --- Cargo.lock | 22 +++- Cargo.toml | 1 + crates/auto_update/src/auto_update.rs | 4 +- crates/client/Cargo.toml | 1 - crates/client/src/client.rs | 8 +- crates/client/src/http.rs | 57 ---------- crates/client/src/telemetry.rs | 18 ++- crates/client/src/test.rs | 53 +-------- crates/client/src/user.rs | 3 +- crates/collab/src/tests.rs | 4 +- crates/copilot/Cargo.toml | 21 ++++ crates/copilot/readme.md | 21 ++++ crates/copilot/src/copilot.rs | 97 ++++++++++++++++ crates/language/src/language.rs | 2 +- crates/project/src/project.rs | 2 +- crates/project/src/worktree.rs | 3 +- crates/util/Cargo.toml | 6 +- crates/util/src/fs.rs | 28 +++++ crates/util/src/github.rs | 40 +++++++ crates/util/src/http.rs | 117 ++++++++++++++++++++ crates/util/src/paths.rs | 1 + crates/util/src/util.rs | 3 + crates/workspace/src/workspace.rs | 2 +- crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 2 +- crates/zed/src/languages/c.rs | 18 +-- crates/zed/src/languages/elixir.rs | 24 +--- crates/zed/src/languages/github.rs | 2 +- crates/zed/src/languages/go.rs | 24 ++-- crates/zed/src/languages/html.rs | 23 +--- crates/zed/src/languages/json.rs | 13 +-- crates/zed/src/languages/language_plugin.rs | 2 +- crates/zed/src/languages/lua.rs | 8 +- crates/zed/src/languages/node_runtime.rs | 2 +- crates/zed/src/languages/python.rs | 14 +-- crates/zed/src/languages/ruby.rs | 2 +- crates/zed/src/languages/rust.rs | 14 +-- crates/zed/src/languages/typescript.rs | 14 +-- crates/zed/src/languages/yaml.rs | 13 +-- crates/zed/src/main.rs | 8 +- crates/zed/src/zed.rs | 2 +- 41 files changed, 435 insertions(+), 265 deletions(-) delete mode 100644 crates/client/src/http.rs create mode 100644 crates/copilot/Cargo.toml create mode 100644 crates/copilot/readme.md create mode 100644 crates/copilot/src/copilot.rs create mode 100644 crates/util/src/fs.rs create mode 100644 crates/util/src/github.rs create mode 100644 crates/util/src/http.rs 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, From 9a99eaee9643ee4c0ae58b08e6c4d01024ec079d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 22 Mar 2023 22:11:31 -0700 Subject: [PATCH 02/53] Fix bad global --- crates/copilot/src/copilot.rs | 7 +++++-- crates/util/src/github.rs | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 097f3187b8..cf531de418 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -16,17 +16,18 @@ pub fn init(client: Arc, cx: &mut MutableAppContext) { }); } +#[derive(Debug)] struct Copilot { copilot_server: PathBuf, } impl Copilot { fn sign_in(http: Arc, cx: &mut MutableAppContext) { - let copilot = cx.global::>>().clone(); + let maybe_copilot = cx.default_global::>>().clone(); cx.spawn(|mut cx| async move { // Lazily download / initialize copilot LSP - let copilot = if let Some(copilot) = copilot { + let copilot = if let Some(copilot) = maybe_copilot { copilot } else { let copilot_server = get_lsp_binary(http).await?; // TODO: Make this error user visible @@ -38,6 +39,8 @@ impl Copilot { new_copilot }; + dbg!(copilot); + Ok(()) }) .detach(); diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index 33c0ea6a1a..02082108e1 100644 --- a/crates/util/src/github.rs +++ b/crates/util/src/github.rs @@ -34,6 +34,7 @@ pub async fn latest_github_release( .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) From 54712170891ea3d6841b4906bc329267406b5def Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Mar 2023 10:29:25 +0100 Subject: [PATCH 03/53] Use the same serde version across the entire workspace --- Cargo.lock | 2 ++ crates/auto_update/Cargo.toml | 6 +++--- crates/cli/Cargo.toml | 4 ++-- crates/client/Cargo.toml | 4 ++-- crates/collab/Cargo.toml | 8 ++++---- crates/collab_ui/Cargo.toml | 4 ++-- crates/command_palette/Cargo.toml | 2 +- crates/copilot/Cargo.toml | 2 ++ crates/db/Cargo.toml | 4 ++-- crates/diagnostics/Cargo.toml | 2 +- crates/editor/Cargo.toml | 2 +- crates/feedback/Cargo.toml | 4 ++-- crates/file_finder/Cargo.toml | 2 +- crates/fs/Cargo.toml | 2 +- crates/gpui/Cargo.toml | 6 +++--- crates/language/Cargo.toml | 6 +++--- crates/live_kit_client/Cargo.toml | 10 +++++----- crates/live_kit_server/Cargo.toml | 4 ++-- crates/lsp/Cargo.toml | 6 +++--- crates/picker/Cargo.toml | 2 +- crates/plugin/Cargo.toml | 4 ++-- crates/plugin_macros/Cargo.toml | 4 ++-- crates/plugin_runtime/Cargo.toml | 6 +++--- crates/project/Cargo.toml | 6 +++--- crates/project_panel/Cargo.toml | 2 +- crates/rpc/Cargo.toml | 4 ++-- crates/search/Cargo.toml | 6 +++--- crates/settings/Cargo.toml | 2 +- crates/terminal/Cargo.toml | 4 ++-- crates/terminal_view/Cargo.toml | 4 ++-- crates/theme/Cargo.toml | 6 +++--- crates/util/Cargo.toml | 4 ++-- crates/vim/Cargo.toml | 6 +++--- crates/workspace/Cargo.toml | 6 +++--- crates/zed/Cargo.toml | 8 ++++---- plugins/json_language/Cargo.toml | 2 +- 36 files changed, 80 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e1f5a807c..028e20d984 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,6 +1341,8 @@ dependencies = [ "futures 0.3.25", "gpui", "lsp", + "serde", + "serde_derive", "settings", "smol", "util", diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 8edb1957af..6b11f5ddbc 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -22,8 +22,8 @@ anyhow = "1.0.38" isahc = "1.7" lazy_static = "1.4" log = "0.4" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } smol = "1.2.5" tempdir = "0.3.7" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index bf2e583d2c..6b814941b8 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -17,8 +17,8 @@ anyhow = "1.0" clap = { version = "3.1", features = ["derive"] } dirs = "3.0" ipc-channel = "0.16" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9" diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 9c772f519b..c75adf5bfa 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -34,8 +34,8 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] } tiny_http = "0.8" uuid = { version = "1.1.2", features = ["v4"] } url = "2.2" -serde = { version = "*", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } settings = { path = "../settings" } tempfile = "3" diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 2ee93f1a86..b85d999298 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -41,9 +41,9 @@ scrypt = "0.7" # Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released. sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] } sea-query = "0.27" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = "1.0" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } sha-1 = "0.9" sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } time = { version = "0.3", features = ["serde", "serde-well-known"] } @@ -79,7 +79,7 @@ env_logger = "0.9" util = { path = "../util" } lazy_static = "1.4" sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } sqlx = { version = "0.6", features = ["sqlite"] } unindent = "0.1" diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 516a1b4fe4..50f81c335c 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -43,8 +43,8 @@ anyhow = "1.0" futures = "0.3" log = "0.4" postage = { workspace = true } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 555deff1ce..6965a3f183 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -24,7 +24,7 @@ workspace = { path = "../workspace" } gpui = { path = "../gpui", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index bd79d053d1..4e1339ee50 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -17,5 +17,7 @@ client = { path = "../client" } workspace = { path = "../workspace" } async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } anyhow = "1.0" +serde = { workspace = true } +serde_derive = { workspace = true } smol = "1.2.5" futures = "0.3" diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml index 16ec37019a..767bf57ba9 100644 --- a/crates/db/Cargo.toml +++ b/crates/db/Cargo.toml @@ -23,8 +23,8 @@ async-trait = "0.1" lazy_static = "1.4.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" -serde = { version = "1.0", features = ["derive"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } smol = "1.2" [dev-dependencies] diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index e28a378a67..8ef2546b5d 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -29,4 +29,4 @@ editor = { path = "../editor", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } -serde_json = { version = "1", features = ["preserve_order"] } +serde_json = { workspace = true } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 44d0936808..5bbd72745d 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -54,7 +54,7 @@ parking_lot = "0.11" postage = { workspace = true } rand = { version = "0.8.3", optional = true } serde = { workspace = true } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde_derive = { workspace = true } smallvec = { version = "1.6", features = ["union"] } smol = "1.2" tree-sitter-rust = { version = "*", optional = true } diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 1c0d0e93ea..57b91876e3 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -24,8 +24,8 @@ lazy_static = "1.4.0" postage = { workspace = true } project = { path = "../project" } search = { path = "../search" } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } settings = { path = "../settings" } sysinfo = "0.27.1" theme = { path = "../theme" } diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index a1a3dbf71a..8c4d853234 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -23,7 +23,7 @@ postage = { workspace = true } [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 66708943f9..f4981ac13a 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -24,7 +24,7 @@ smol = "1.2.5" regex = "1.5" git2 = { version = "0.15", default-features = false } serde = { workspace = true } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde_derive = { workspace = true } serde_json = { workspace = true } log = { version = "0.4.16", features = ["kv_unstable_serde"] } libc = "0.2" diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index bd994b5407..8715142dd3 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -41,9 +41,9 @@ rand = "0.8.3" resvg = "0.14" schemars = "0.8" seahash = "4.1" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = "1.0" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } smallvec = { version = "1.6", features = ["union"] } smol = "1.2" time = { version = "0.3", features = ["serde", "serde-well-known"] } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index ac4badbe2a..4311f04391 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -46,9 +46,9 @@ parking_lot = "0.11.1" postage = { workspace = true } rand = { version = "0.8.3", optional = true } regex = "1.5" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } similar = "1.3" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" diff --git a/crates/live_kit_client/Cargo.toml b/crates/live_kit_client/Cargo.toml index 71a6235b95..70032d83aa 100644 --- a/crates/live_kit_client/Cargo.toml +++ b/crates/live_kit_client/Cargo.toml @@ -62,12 +62,12 @@ jwt = "0.16" lazy_static = "1.4" objc = "0.2" parking_lot = "0.11.1" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } sha2 = "0.10" simplelog = "0.9" [build-dependencies] -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/live_kit_server/Cargo.toml b/crates/live_kit_server/Cargo.toml index 319a026456..8cced6d089 100644 --- a/crates/live_kit_server/Cargo.toml +++ b/crates/live_kit_server/Cargo.toml @@ -19,8 +19,8 @@ jwt = "0.16" prost = "0.8" prost-types = "0.8" reqwest = "0.11" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } sha2 = "0.10" [build-dependencies] diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index aa1f49977c..4370aaab06 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -22,9 +22,9 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } lsp-types = "0.91" parking_lot = "0.11" postage = { workspace = true } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["raw_value"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } smol = "1.2" [dev-dependencies] diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index e7a8079caa..2371cfa9fd 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -21,7 +21,7 @@ parking_lot = "0.11.1" [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/plugin/Cargo.toml b/crates/plugin/Cargo.toml index 6b86b19fc8..8c3a2fb83f 100644 --- a/crates/plugin/Cargo.toml +++ b/crates/plugin/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -serde = "1.0" -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } bincode = "1.3" plugin_macros = { path = "../plugin_macros" } diff --git a/crates/plugin_macros/Cargo.toml b/crates/plugin_macros/Cargo.toml index e661485373..51cb78c7a1 100644 --- a/crates/plugin_macros/Cargo.toml +++ b/crates/plugin_macros/Cargo.toml @@ -11,6 +11,6 @@ proc-macro = true syn = { version = "1.0", features = ["full", "extra-traits"] } quote = "1.0" proc-macro2 = "1.0" -serde = "1.0" -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } bincode = "1.3" diff --git a/crates/plugin_runtime/Cargo.toml b/crates/plugin_runtime/Cargo.toml index 13efa10dc2..cf509a20d3 100644 --- a/crates/plugin_runtime/Cargo.toml +++ b/crates/plugin_runtime/Cargo.toml @@ -9,9 +9,9 @@ wasmtime = "0.38" wasmtime-wasi = "0.38" wasi-common = "0.38" anyhow = { version = "1.0", features = ["std"] } -serde = "1.0" -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = "1.0" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } bincode = "1.3" pollster = "0.2.5" smol = "1.2.5" diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index b42a6fc674..f5c144a3ad 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -49,9 +49,9 @@ postage = { workspace = true } pulldown-cmark = { version = "0.9.1", default-features = false } rand = "0.8.3" regex = "1.5" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } sha2 = "0.10" similar = "1.3" smol = "1.2.5" diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index d245700d58..2b72959e25 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -27,4 +27,4 @@ unicase = "2.6" editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index ff71a2493e..2773dd2f3b 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -26,8 +26,8 @@ parking_lot = "0.11.1" prost = "0.8" rand = "0.8" rsa = "0.4" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } smol-timeout = "0.6" tracing = { version = "0.1.34", features = ["log"] } zstd = "0.11" diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index e8c03a1a5e..f786d4abc6 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -23,14 +23,14 @@ anyhow = "1.0" futures = "0.3" log = { version = "0.4.16", features = ["kv_unstable_serde"] } postage = { workspace = true } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } smallvec = { version = "1.6", features = ["union"] } smol = "1.2" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } workspace = { path = "../workspace", features = ["test-support"] } unindent = "0.1" diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 566fcfe355..6eeab7d7d9 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -25,7 +25,7 @@ json_comments = "0.2" postage = { workspace = true } schemars = "0.8" serde = { workspace = true } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde_derive = { workspace = true } serde_json = { workspace = true } serde_path_to_error = "0.1.4" toml = "0.5" diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 56a8a3c452..56796fca59 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -29,8 +29,8 @@ libc = "0.2" anyhow = "1" thiserror = "1.0" lazy_static = "1.4.0" -serde = { version = "1.0", features = ["derive"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } [dev-dependencies] rand = "0.8.5" diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 1e5b9d6070..726a1a674f 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -33,8 +33,8 @@ libc = "0.2" anyhow = "1" thiserror = "1.0" lazy_static = "1.4.0" -serde = { version = "1.0", features = ["derive"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index a0ef4ad9f8..cf8ff1db14 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -13,8 +13,8 @@ gpui = { path = "../gpui" } anyhow = "1.0.38" indexmap = "1.6.2" parking_lot = "0.11.1" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } serde_path_to_error = "0.1.4" toml = "0.5" diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 558ca588b4..0e3a8d96be 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -22,8 +22,8 @@ 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"] } +serde = { workspace = true } +serde_json = { workspace = true } git2 = { version = "0.15", default-features = false, optional = true } dirs = "3.0" diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index dd79d56d8f..bafa2c7a55 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -12,8 +12,8 @@ doctest = false neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"] [dependencies] -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } itertools = "0.10" log = { version = "0.4.16", features = ["kv_unstable_serde"] } @@ -21,7 +21,7 @@ async-compat = { version = "0.2.1", "optional" = true } async-trait = { version = "0.1", "optional" = true } nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true } tokio = { version = "1.15", "optional" = true } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } assets = { path = "../assets" } collections = { path = "../collections" } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 9a6813f627..5a2380de3f 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -44,9 +44,9 @@ env_logger = "0.9.1" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { workspace = true } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } smallvec = { version = "1.6", features = ["union"] } indoc = "1.0.4" uuid = { version = "1.1.2", features = ["v4"] } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 812fae9e0a..a9cbfbc737 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -88,9 +88,9 @@ rand = "0.8.3" regex = "1.5" rsa = "0.4" rust-embed = { version = "6.3", features = ["include-exclude"] } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } serde_path_to_error = "0.1.4" simplelog = "0.9" smallvec = { version = "1.6", features = ["union"] } @@ -138,7 +138,7 @@ util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } env_logger = "0.9" -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } unindent = "0.1.7" [package.metadata.bundle-dev] diff --git a/plugins/json_language/Cargo.toml b/plugins/json_language/Cargo.toml index effbf2ed8a..5a5072995f 100644 --- a/plugins/json_language/Cargo.toml +++ b/plugins/json_language/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] plugin = { path = "../../crates/plugin" } -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = "1.0" From 797bb7d7809880eb21f80d80a419c0a73bf16b2d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Mar 2023 10:29:56 +0100 Subject: [PATCH 04/53] Start copilot and check sign in status --- crates/copilot/src/copilot.rs | 105 +++++++++++++++++++++++++--------- crates/copilot/src/request.rs | 22 +++++++ 2 files changed, 100 insertions(+), 27 deletions(-) create mode 100644 crates/copilot/src/request.rs diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index cf531de418..22d1246550 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1,9 +1,16 @@ -use anyhow::{anyhow, Ok}; +mod request; + +use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use client::Client; -use gpui::{actions, MutableAppContext}; +use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext}; +use lsp::LanguageServer; use smol::{fs, io::BufReader, stream::StreamExt}; -use std::{env::consts, path::PathBuf, sync::Arc}; +use std::{ + env::consts, + path::{Path, PathBuf}, + sync::Arc, +}; use util::{ fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, }; @@ -11,46 +18,89 @@ use util::{ 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) - }); + let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); + cx.set_global(copilot); +} + +enum CopilotServer { + Downloading, + Error(String), + Started { + server: Arc, + status: SignInStatus, + }, +} + +enum SignInStatus { + Authorized, + Unauthorized, + SignedOut, } -#[derive(Debug)] struct Copilot { - copilot_server: PathBuf, + server: CopilotServer, +} + +impl Entity for Copilot { + type Event = (); } impl Copilot { - fn sign_in(http: Arc, cx: &mut MutableAppContext) { - let maybe_copilot = cx.default_global::>>().clone(); + fn global(cx: &AppContext) -> Option> { + if cx.has_global::>() { + Some(cx.global::>().clone()) + } else { + None + } + } - cx.spawn(|mut cx| async move { - // Lazily download / initialize copilot LSP - let copilot = if let Some(copilot) = maybe_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 + fn start(http: Arc, cx: &mut ModelContext) -> Self { + let copilot = Self { + server: CopilotServer::Downloading, + }; + cx.spawn(|this, mut cx| async move { + let start_language_server = async { + let server_path = get_lsp_binary(http).await?; + let server = + LanguageServer::new(0, &server_path, &["--stdio"], Path::new("/"), cx.clone())?; + let server = server.initialize(Default::default()).await?; + let status = server + .request::(request::CheckStatusParams { + local_checks_only: false, + }) + .await?; + let status = match status.status.as_str() { + "OK" | "MaybeOk" => SignInStatus::Authorized, + "NotAuthorized" => SignInStatus::Unauthorized, + _ => SignInStatus::SignedOut, + }; + anyhow::Ok((server, status)) }; - dbg!(copilot); - - Ok(()) + let server = start_language_server.await; + this.update(&mut cx, |this, cx| { + cx.notify(); + match server { + Ok((server, status)) => { + this.server = CopilotServer::Started { server, status }; + Ok(()) + } + Err(error) => { + this.server = CopilotServer::Error(error.to_string()); + Err(error) + } + } + }) }) - .detach(); + .detach_and_log_err(cx); + copilot } } 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 release = latest_github_release("zed-industries/copilot", http.clone()).await?; let asset_name = format!("copilot-darwin-{}.gz", consts::ARCH); let asset = release .assets @@ -58,6 +108,7 @@ async fn get_lsp_binary(http: Arc) -> anyhow::Result { .find(|asset| asset.name == asset_name) .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + fs::create_dir_all(&*paths::COPILOT_DIR).await?; let destination_path = paths::COPILOT_DIR.join(format!("copilot-{}-{}", release.name, consts::ARCH)); diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs new file mode 100644 index 0000000000..3f1f66482e --- /dev/null +++ b/crates/copilot/src/request.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +pub enum CheckStatus {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckStatusParams { + pub local_checks_only: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckStatusResult { + pub status: String, + pub user: Option, +} + +impl lsp::request::Request for CheckStatus { + type Params = CheckStatusParams; + type Result = CheckStatusResult; + const METHOD: &'static str = "checkStatus"; +} From 59d9277a74f32e9b2871fd09de05a9495e732247 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Mar 2023 14:17:15 +0100 Subject: [PATCH 05/53] Implement Copilot sign in and sign out --- Cargo.lock | 1 + crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 186 ++++++++++++++++++++++++++-------- crates/copilot/src/request.rs | 83 +++++++++++++-- 4 files changed, 223 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 028e20d984..a5886a9466 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,6 +1340,7 @@ dependencies = [ "client", "futures 0.3.25", "gpui", + "log", "lsp", "serde", "serde_derive", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 4e1339ee50..301051a3b0 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -17,6 +17,7 @@ client = { path = "../client" } workspace = { path = "../workspace" } async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } anyhow = "1.0" +log = "0.4" serde = { workspace = true } serde_derive = { workspace = true } smol = "1.2.5" diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 22d1246550..8bea11b1f7 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -3,7 +3,7 @@ mod request; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use client::Client; -use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext}; +use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use lsp::LanguageServer; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ @@ -15,11 +15,32 @@ use util::{ fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, }; -actions!(copilot, [SignIn]); +actions!(copilot, [SignIn, SignOut]); pub fn init(client: Arc, cx: &mut MutableAppContext) { - let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); + let (copilot, task) = Copilot::start(client.http_client(), cx); cx.set_global(copilot); + cx.spawn(|mut cx| async move { + task.await?; + cx.update(|cx| { + cx.add_global_action(|_: &SignIn, cx: &mut MutableAppContext| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + } + }); + cx.add_global_action(|_: &SignOut, cx: &mut MutableAppContext| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .detach_and_log_err(cx); + } + }); + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } enum CopilotServer { @@ -31,18 +52,26 @@ enum CopilotServer { }, } +#[derive(Clone, Debug, PartialEq, Eq)] enum SignInStatus { - Authorized, - Unauthorized, + Authorized { user: String }, + Unauthorized { user: String }, SignedOut, } +pub enum Event { + PromptUserDeviceFlow { + user_code: String, + verification_uri: String, + }, +} + struct Copilot { server: CopilotServer, } impl Entity for Copilot { - type Event = (); + type Event = Event; } impl Copilot { @@ -54,46 +83,123 @@ impl Copilot { } } - fn start(http: Arc, cx: &mut ModelContext) -> Self { - let copilot = Self { + fn start( + http: Arc, + cx: &mut MutableAppContext, + ) -> (ModelHandle, Task>) { + let this = cx.add_model(|_| Self { server: CopilotServer::Downloading, - }; - cx.spawn(|this, mut cx| async move { - let start_language_server = async { - let server_path = get_lsp_binary(http).await?; - let server = - LanguageServer::new(0, &server_path, &["--stdio"], Path::new("/"), cx.clone())?; - let server = server.initialize(Default::default()).await?; - let status = server - .request::(request::CheckStatusParams { - local_checks_only: false, - }) - .await?; - let status = match status.status.as_str() { - "OK" | "MaybeOk" => SignInStatus::Authorized, - "NotAuthorized" => SignInStatus::Unauthorized, - _ => SignInStatus::SignedOut, + }); + let task = cx.spawn({ + let this = this.clone(); + |mut cx| async move { + let start_language_server = async { + let server_path = get_lsp_binary(http).await?; + let server = LanguageServer::new( + 0, + &server_path, + &["--stdio"], + Path::new("/"), + cx.clone(), + )?; + let server = server.initialize(Default::default()).await?; + let status = server + .request::(request::CheckStatusParams { + local_checks_only: false, + }) + .await?; + anyhow::Ok((server, status)) }; - anyhow::Ok((server, status)) - }; - let server = start_language_server.await; - this.update(&mut cx, |this, cx| { - cx.notify(); - match server { - Ok((server, status)) => { - this.server = CopilotServer::Started { server, status }; - Ok(()) - } - Err(error) => { - this.server = CopilotServer::Error(error.to_string()); - Err(error) + let server = start_language_server.await; + this.update(&mut cx, |this, cx| { + cx.notify(); + match server { + Ok((server, status)) => { + this.server = CopilotServer::Started { + server, + status: SignInStatus::SignedOut, + }; + this.update_sign_in_status(status, cx); + Ok(()) + } + Err(error) => { + this.server = CopilotServer::Error(error.to_string()); + Err(error) + } } + }) + } + }); + (this, task) + } + + fn sign_in(&mut self, cx: &mut ModelContext) -> Task> { + if let CopilotServer::Started { server, .. } = &self.server { + let server = server.clone(); + cx.spawn(|this, mut cx| async move { + let sign_in = server + .request::(request::SignInInitiateParams {}) + .await?; + if let request::SignInInitiateResult::PromptUserDeviceFlow(flow) = sign_in { + this.update(&mut cx, |_, cx| { + cx.emit(Event::PromptUserDeviceFlow { + user_code: flow.user_code.clone(), + verification_uri: flow.verification_uri, + }); + }); + let response = server + .request::(request::SignInConfirmParams { + user_code: flow.user_code, + }) + .await?; + this.update(&mut cx, |this, cx| this.update_sign_in_status(response, cx)); } + anyhow::Ok(()) }) - }) - .detach_and_log_err(cx); - copilot + } else { + Task::ready(Err(anyhow!("copilot hasn't started yet"))) + } + } + + fn sign_out(&mut self, cx: &mut ModelContext) -> Task> { + if let CopilotServer::Started { server, .. } = &self.server { + let server = server.clone(); + cx.spawn(|this, mut cx| async move { + server + .request::(request::SignOutParams {}) + .await?; + this.update(&mut cx, |this, cx| { + if let CopilotServer::Started { status, .. } = &mut this.server { + *status = SignInStatus::SignedOut; + cx.notify(); + } + }); + + anyhow::Ok(()) + }) + } else { + Task::ready(Err(anyhow!("copilot hasn't started yet"))) + } + } + + fn update_sign_in_status( + &mut self, + lsp_status: request::SignInStatus, + cx: &mut ModelContext, + ) { + if let CopilotServer::Started { status, .. } = &mut self.server { + *status = match lsp_status { + request::SignInStatus::Ok { user } | request::SignInStatus::MaybeOk { user } => { + SignInStatus::Authorized { user } + } + request::SignInStatus::NotAuthorized { user } => { + SignInStatus::Unauthorized { user } + } + _ => SignInStatus::SignedOut, + }; + cx.notify(); + } } } diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 3f1f66482e..1b02227273 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -8,15 +8,82 @@ pub struct CheckStatusParams { pub local_checks_only: bool, } -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CheckStatusResult { - pub status: String, - pub user: Option, -} - impl lsp::request::Request for CheckStatus { type Params = CheckStatusParams; - type Result = CheckStatusResult; + type Result = SignInStatus; const METHOD: &'static str = "checkStatus"; } + +pub enum SignInInitiate {} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SignInInitiateParams {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "status")] +pub enum SignInInitiateResult { + AlreadySignedIn { user: String }, + PromptUserDeviceFlow(PromptUserDeviceFlow), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PromptUserDeviceFlow { + pub user_code: String, + pub verification_uri: String, +} + +impl lsp::request::Request for SignInInitiate { + type Params = SignInInitiateParams; + type Result = SignInInitiateResult; + const METHOD: &'static str = "signInInitiate"; +} + +pub enum SignInConfirm {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignInConfirmParams { + pub user_code: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "status")] +pub enum SignInStatus { + #[serde(rename = "OK")] + Ok { + user: String, + }, + MaybeOk { + user: String, + }, + AlreadySignedIn { + user: String, + }, + NotAuthorized { + user: String, + }, + NotSignedIn, +} + +impl lsp::request::Request for SignInConfirm { + type Params = SignInConfirmParams; + type Result = SignInStatus; + const METHOD: &'static str = "signInConfirm"; +} + +pub enum SignOut {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignOutParams {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignOutResult {} + +impl lsp::request::Request for SignOut { + type Params = SignOutParams; + type Result = SignOutResult; + const METHOD: &'static str = "signOut"; +} From 8ba9e63ab886028e2c7db794a984733ae25cf1d4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Mar 2023 14:20:39 +0100 Subject: [PATCH 06/53] :art: --- crates/copilot/src/copilot.rs | 118 ++++++++++++++-------------------- 1 file changed, 49 insertions(+), 69 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 8bea11b1f7..c768442611 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -18,29 +18,22 @@ use util::{ actions!(copilot, [SignIn, SignOut]); pub fn init(client: Arc, cx: &mut MutableAppContext) { - let (copilot, task) = Copilot::start(client.http_client(), cx); + let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); cx.set_global(copilot); - cx.spawn(|mut cx| async move { - task.await?; - cx.update(|cx| { - cx.add_global_action(|_: &SignIn, cx: &mut MutableAppContext| { - if let Some(copilot) = Copilot::global(cx) { - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - } - }); - cx.add_global_action(|_: &SignOut, cx: &mut MutableAppContext| { - if let Some(copilot) = Copilot::global(cx) { - copilot - .update(cx, |copilot, cx| copilot.sign_out(cx)) - .detach_and_log_err(cx); - } - }); - }); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + cx.add_global_action(|_: &SignIn, cx: &mut MutableAppContext| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + } + }); + cx.add_global_action(|_: &SignOut, cx: &mut MutableAppContext| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .detach_and_log_err(cx); + } + }); } enum CopilotServer { @@ -83,55 +76,42 @@ impl Copilot { } } - fn start( - http: Arc, - cx: &mut MutableAppContext, - ) -> (ModelHandle, Task>) { - let this = cx.add_model(|_| Self { - server: CopilotServer::Downloading, - }); - let task = cx.spawn({ - let this = this.clone(); - |mut cx| async move { - let start_language_server = async { - let server_path = get_lsp_binary(http).await?; - let server = LanguageServer::new( - 0, - &server_path, - &["--stdio"], - Path::new("/"), - cx.clone(), - )?; - let server = server.initialize(Default::default()).await?; - let status = server - .request::(request::CheckStatusParams { - local_checks_only: false, - }) - .await?; - anyhow::Ok((server, status)) - }; + fn start(http: Arc, cx: &mut ModelContext) -> Self { + cx.spawn(|this, mut cx| async move { + let start_language_server = async { + let server_path = get_lsp_binary(http).await?; + let server = + LanguageServer::new(0, &server_path, &["--stdio"], Path::new("/"), cx.clone())?; + let server = server.initialize(Default::default()).await?; + let status = server + .request::(request::CheckStatusParams { + local_checks_only: false, + }) + .await?; + anyhow::Ok((server, status)) + }; - let server = start_language_server.await; - this.update(&mut cx, |this, cx| { - cx.notify(); - match server { - Ok((server, status)) => { - this.server = CopilotServer::Started { - server, - status: SignInStatus::SignedOut, - }; - this.update_sign_in_status(status, cx); - Ok(()) - } - Err(error) => { - this.server = CopilotServer::Error(error.to_string()); - Err(error) - } + let server = start_language_server.await; + this.update(&mut cx, |this, cx| { + cx.notify(); + match server { + Ok((server, status)) => { + this.server = CopilotServer::Started { + server, + status: SignInStatus::SignedOut, + }; + this.update_sign_in_status(status, cx); } - }) - } - }); - (this, task) + Err(error) => { + this.server = CopilotServer::Error(error.to_string()); + } + } + }) + }) + .detach(); + Self { + server: CopilotServer::Downloading, + } } fn sign_in(&mut self, cx: &mut ModelContext) -> Task> { From 180371929b0cd09ff0190b44facde994bba57cce Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Mar 2023 14:45:14 +0100 Subject: [PATCH 07/53] Start on copilot completions --- Cargo.lock | 1 + crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 73 +++++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5886a9466..202b511a6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1340,6 +1340,7 @@ dependencies = [ "client", "futures 0.3.25", "gpui", + "language", "log", "lsp", "serde", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 301051a3b0..190a399475 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] gpui = { path = "../gpui" } +language = { path = "../language" } settings = { path = "../settings" } lsp = { path = "../lsp" } util = { path = "../util" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index c768442611..4abdef0ab4 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use client::Client; use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +use language::{Buffer, ToPointUtf16}; use lsp::LanguageServer; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ @@ -38,7 +39,7 @@ pub fn init(client: Arc, cx: &mut MutableAppContext) { enum CopilotServer { Downloading, - Error(String), + Error(Arc), Started { server: Arc, status: SignInStatus, @@ -59,6 +60,21 @@ pub enum Event { }, } +#[derive(Debug)] +pub enum Status { + Downloading, + Error(Arc), + SignedOut, + Unauthorized, + Authorized, +} + +impl Status { + fn is_authorized(&self) -> bool { + matches!(self, Status::Authorized) + } +} + struct Copilot { server: CopilotServer, } @@ -70,7 +86,12 @@ impl Entity for Copilot { impl Copilot { fn global(cx: &AppContext) -> Option> { if cx.has_global::>() { - Some(cx.global::>().clone()) + let copilot = cx.global::>().clone(); + if copilot.read(cx).status().is_authorized() { + Some(copilot) + } else { + None + } } else { None } @@ -103,7 +124,7 @@ impl Copilot { this.update_sign_in_status(status, cx); } Err(error) => { - this.server = CopilotServer::Error(error.to_string()); + this.server = CopilotServer::Error(error.to_string().into()); } } }) @@ -163,6 +184,35 @@ impl Copilot { } } + pub fn completions( + &self, + buffer: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task> + where + T: ToPointUtf16, + { + let server = match self.authenticated_server() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; + + cx.spawn(|this, cx| async move { anyhow::Ok(()) }) + } + + pub fn status(&self) -> Status { + match &self.server { + CopilotServer::Downloading => Status::Downloading, + CopilotServer::Error(error) => Status::Error(error.clone()), + CopilotServer::Started { status, .. } => match status { + SignInStatus::Authorized { .. } => Status::Authorized, + SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::SignedOut => Status::SignedOut, + }, + } + } + fn update_sign_in_status( &mut self, lsp_status: request::SignInStatus, @@ -181,6 +231,23 @@ impl Copilot { cx.notify(); } } + + fn authenticated_server(&self) -> Result> { + match &self.server { + CopilotServer::Downloading => Err(anyhow!("copilot is still downloading")), + CopilotServer::Error(error) => Err(anyhow!( + "copilot was not started because of an error: {}", + error + )), + CopilotServer::Started { server, status } => { + if matches!(status, SignInStatus::Authorized { .. }) { + Ok(server.clone()) + } else { + Err(anyhow!("must sign in before using copilot")) + } + } + } + } } async fn get_lsp_binary(http: Arc) -> anyhow::Result { From 155594c8b8c6cdc7c00a4dc9891f96216819c600 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Mar 2023 16:11:39 +0100 Subject: [PATCH 08/53] Successfully fetch completions from Copilot We still need to process them and return them into a more Zed-friendly structure, but we're getting there. --- crates/copilot/Cargo.toml | 9 +++++ crates/copilot/src/copilot.rs | 70 ++++++++++++++++++++++++++++++++++- crates/copilot/src/request.rs | 45 ++++++++++++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 190a399475..c17e7cac59 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -23,3 +23,12 @@ serde = { workspace = true } serde_derive = { workspace = true } smol = "1.2.5" futures = "0.3" + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +client = { path = "../client", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 4abdef0ab4..eafc4d2d98 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,8 +4,9 @@ use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use client::Client; use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; -use language::{Buffer, ToPointUtf16}; +use language::{point_to_lsp, Buffer, ToPointUtf16}; use lsp::LanguageServer; +use settings::Settings; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ env::consts, @@ -53,6 +54,7 @@ enum SignInStatus { SignedOut, } +#[derive(Debug)] pub enum Event { PromptUserDeviceFlow { user_code: String, @@ -198,7 +200,45 @@ impl Copilot { Err(error) => return Task::ready(Err(error)), }; - cx.spawn(|this, cx| async move { anyhow::Ok(()) }) + let buffer = buffer.read(cx).snapshot(); + let position = position.to_point_utf16(&buffer); + let language_name = buffer.language_at(position).map(|language| language.name()); + let language_name = language_name.as_deref(); + + let path; + let relative_path; + if let Some(file) = buffer.file() { + if let Some(file) = file.as_local() { + path = file.abs_path(cx); + } else { + path = file.full_path(cx); + } + relative_path = file.path().to_path_buf(); + } else { + path = PathBuf::from("/untitled"); + relative_path = PathBuf::from("untitled"); + } + + let settings = cx.global::(); + let request = server.request::(request::GetCompletionsParams { + doc: request::GetCompletionsDocument { + source: buffer.text(), + tab_size: settings.tab_size(language_name).into(), + indent_size: 1, + insert_spaces: !settings.hard_tabs(language_name), + uri: lsp::Url::from_file_path(&path).unwrap(), + path: path.to_string_lossy().into(), + relative_path: relative_path.to_string_lossy().into(), + language_id: "csharp".into(), + position: point_to_lsp(position), + version: 0, + }, + }); + cx.spawn(|this, cx| async move { + dbg!(request.await?); + + anyhow::Ok(()) + }) } pub fn status(&self) -> Status { @@ -302,3 +342,29 @@ async fn get_lsp_binary(http: Arc) -> anyhow::Result { } } } + +#[cfg(test)] +mod tests { + use super::*; + use gpui::TestAppContext; + use util::http; + + #[gpui::test] + async fn test_smoke(cx: &mut TestAppContext) { + Settings::test_async(cx); + let http = http::client(); + let copilot = cx.add_model(|cx| Copilot::start(http, cx)); + smol::Timer::after(std::time::Duration::from_secs(5)).await; + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .await + .unwrap(); + dbg!(copilot.read_with(cx, |copilot, _| copilot.status())); + + let buffer = cx.add_model(|cx| language::Buffer::new(0, "Lorem ipsum dol", cx)); + copilot + .update(cx, |copilot, cx| copilot.completions(&buffer, 15, cx)) + .await + .unwrap(); + } +} diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 1b02227273..3fe04532e1 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -87,3 +87,48 @@ impl lsp::request::Request for SignOut { type Result = SignOutResult; const METHOD: &'static str = "signOut"; } + +pub enum GetCompletions {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCompletionsParams { + pub doc: GetCompletionsDocument, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCompletionsDocument { + pub source: String, + pub tab_size: u32, + pub indent_size: u32, + pub insert_spaces: bool, + pub uri: lsp::Url, + pub path: String, + pub relative_path: String, + pub language_id: String, + pub position: lsp::Position, + pub version: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCompletionsResult { + completions: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Completion { + text: String, + position: lsp::Position, + uuid: String, + range: lsp::Range, + display_text: String, +} + +impl lsp::request::Request for GetCompletions { + type Params = GetCompletionsParams; + type Result = GetCompletionsResult; + const METHOD: &'static str = "getCompletions"; +} From 591e2464500e20bd1470feab67fc6e6e084bb3fd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 23 Mar 2023 17:01:40 +0100 Subject: [PATCH 09/53] Implement `Copilot::completions_cycling` Co-Authored-By: Mikayla Maki --- crates/copilot/src/copilot.rs | 155 ++++++++++++++++++++++++---------- crates/copilot/src/request.rs | 20 +++-- 2 files changed, 126 insertions(+), 49 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index eafc4d2d98..9a24139ad6 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use client::Client; use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; -use language::{point_to_lsp, Buffer, ToPointUtf16}; +use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, ToPointUtf16}; use lsp::LanguageServer; use settings::Settings; use smol::{fs, io::BufReader, stream::StreamExt}; @@ -77,6 +77,12 @@ impl Status { } } +#[derive(Debug)] +pub struct Completion { + pub position: Anchor, + pub text: String, +} + struct Copilot { server: CopilotServer, } @@ -186,12 +192,12 @@ impl Copilot { } } - pub fn completions( + pub fn completion( &self, buffer: &ModelHandle, position: T, cx: &mut ModelContext, - ) -> Task> + ) -> Task>> where T: ToPointUtf16, { @@ -201,43 +207,45 @@ impl Copilot { }; let buffer = buffer.read(cx).snapshot(); - let position = position.to_point_utf16(&buffer); - let language_name = buffer.language_at(position).map(|language| language.name()); - let language_name = language_name.as_deref(); + let request = server + .request::(build_completion_params(&buffer, position, cx)); + cx.background().spawn(async move { + let result = request.await?; + let completion = result + .completions + .into_iter() + .next() + .map(|completion| completion_from_lsp(completion, &buffer)); + anyhow::Ok(completion) + }) + } - let path; - let relative_path; - if let Some(file) = buffer.file() { - if let Some(file) = file.as_local() { - path = file.abs_path(cx); - } else { - path = file.full_path(cx); - } - relative_path = file.path().to_path_buf(); - } else { - path = PathBuf::from("/untitled"); - relative_path = PathBuf::from("untitled"); - } + pub fn completions_cycling( + &self, + buffer: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>> + where + T: ToPointUtf16, + { + let server = match self.authenticated_server() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; - let settings = cx.global::(); - let request = server.request::(request::GetCompletionsParams { - doc: request::GetCompletionsDocument { - source: buffer.text(), - tab_size: settings.tab_size(language_name).into(), - indent_size: 1, - insert_spaces: !settings.hard_tabs(language_name), - uri: lsp::Url::from_file_path(&path).unwrap(), - path: path.to_string_lossy().into(), - relative_path: relative_path.to_string_lossy().into(), - language_id: "csharp".into(), - position: point_to_lsp(position), - version: 0, - }, - }); - cx.spawn(|this, cx| async move { - dbg!(request.await?); - - anyhow::Ok(()) + let buffer = buffer.read(cx).snapshot(); + let request = server.request::(build_completion_params( + &buffer, position, cx, + )); + cx.background().spawn(async move { + let result = request.await?; + let completions = result + .completions + .into_iter() + .map(|completion| completion_from_lsp(completion, &buffer)) + .collect(); + anyhow::Ok(completions) }) } @@ -290,6 +298,62 @@ impl Copilot { } } +fn build_completion_params( + buffer: &BufferSnapshot, + position: T, + cx: &AppContext, +) -> request::GetCompletionsParams +where + T: ToPointUtf16, +{ + let position = position.to_point_utf16(&buffer); + let language_name = buffer.language_at(position).map(|language| language.name()); + let language_name = language_name.as_deref(); + + let path; + let relative_path; + if let Some(file) = buffer.file() { + if let Some(file) = file.as_local() { + path = file.abs_path(cx); + } else { + path = file.full_path(cx); + } + relative_path = file.path().to_path_buf(); + } else { + path = PathBuf::from("/untitled"); + relative_path = PathBuf::from("untitled"); + } + + let settings = cx.global::(); + let language_id = match language_name { + Some("Plain Text") => "plaintext".to_string(), + Some(language_name) => language_name.to_lowercase(), + None => "plaintext".to_string(), + }; + request::GetCompletionsParams { + doc: request::GetCompletionsDocument { + source: buffer.text(), + tab_size: settings.tab_size(language_name).into(), + indent_size: 1, + insert_spaces: !settings.hard_tabs(language_name), + uri: lsp::Url::from_file_path(&path).unwrap(), + path: path.to_string_lossy().into(), + relative_path: relative_path.to_string_lossy().into(), + language_id, + position: point_to_lsp(position), + version: 0, + }, + } +} + +fn completion_from_lsp(completion: request::Completion, buffer: &BufferSnapshot) -> Completion { + let position = buffer.clip_point_utf16(point_from_lsp(completion.position), Bias::Left); + Completion { + position: buffer.anchor_before(position), + text: completion.display_text, + } +} + 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 { @@ -354,17 +418,22 @@ mod tests { Settings::test_async(cx); let http = http::client(); let copilot = cx.add_model(|cx| Copilot::start(http, cx)); - smol::Timer::after(std::time::Duration::from_secs(5)).await; + smol::Timer::after(std::time::Duration::from_secs(2)).await; copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) .await .unwrap(); dbg!(copilot.read_with(cx, |copilot, _| copilot.status())); - let buffer = cx.add_model(|cx| language::Buffer::new(0, "Lorem ipsum dol", cx)); - copilot - .update(cx, |copilot, cx| copilot.completions(&buffer, 15, cx)) + let buffer = cx.add_model(|cx| language::Buffer::new(0, "fn foo() -> ", cx)); + dbg!(copilot + .update(cx, |copilot, cx| copilot.completion(&buffer, 12, cx)) .await - .unwrap(); + .unwrap()); + dbg!(copilot + .update(cx, |copilot, cx| copilot + .completions_cycling(&buffer, 12, cx)) + .await + .unwrap()); } } diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 3fe04532e1..f3a86698e1 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -114,17 +114,17 @@ pub struct GetCompletionsDocument { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetCompletionsResult { - completions: Vec, + pub completions: Vec, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Completion { - text: String, - position: lsp::Position, - uuid: String, - range: lsp::Range, - display_text: String, + pub text: String, + pub position: lsp::Position, + pub uuid: String, + pub range: lsp::Range, + pub display_text: String, } impl lsp::request::Request for GetCompletions { @@ -132,3 +132,11 @@ impl lsp::request::Request for GetCompletions { type Result = GetCompletionsResult; const METHOD: &'static str = "getCompletions"; } + +pub enum GetCompletionsCycling {} + +impl lsp::request::Request for GetCompletionsCycling { + type Params = GetCompletionsParams; + type Result = GetCompletionsResult; + const METHOD: &'static str = "getCompletionsCycling"; +} From b57d5174aac056eec0df9d262fe7b9d574c379e2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 23 Mar 2023 12:49:52 -0700 Subject: [PATCH 10/53] Add copilot theme, start sketching out the auth modal --- crates/copilot/src/auth_modal.rs | 20 ++++++++++++++++++++ crates/copilot/src/copilot.rs | 23 ++++++++++++++++++----- crates/theme/src/theme.rs | 6 ++++++ styles/src/styleTree/app.ts | 2 ++ styles/src/styleTree/copilot.ts | 11 +++++++++++ 5 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 crates/copilot/src/auth_modal.rs create mode 100644 styles/src/styleTree/copilot.ts diff --git a/crates/copilot/src/auth_modal.rs b/crates/copilot/src/auth_modal.rs new file mode 100644 index 0000000000..4786f1d470 --- /dev/null +++ b/crates/copilot/src/auth_modal.rs @@ -0,0 +1,20 @@ +use gpui::{elements::Label, Element, Entity, View}; +use settings::Settings; + +pub struct AuthModal {} + +impl Entity for AuthModal { + type Event = (); +} + +impl View for AuthModal { + fn ui_name() -> &'static str { + "AuthModal" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + let style = &cx.global::().theme.copilot; + + Label::new("[COPILOT AUTH INFO]", style.auth_modal.clone()).boxed() + } +} diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 9a24139ad6..2bdcedb2cc 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1,7 +1,9 @@ +mod auth_modal; mod request; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; +use auth_modal::AuthModal; use client::Client; use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, ToPointUtf16}; @@ -16,26 +18,36 @@ use std::{ use util::{ fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, }; +use workspace::Workspace; -actions!(copilot, [SignIn, SignOut]); +actions!(copilot, [SignIn, SignOut, ToggleAuthStatus]); pub fn init(client: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); - cx.set_global(copilot); - cx.add_global_action(|_: &SignIn, cx: &mut MutableAppContext| { + cx.set_global(copilot.clone()); + cx.add_action(|workspace: &mut Workspace, _: &SignIn, cx| { if let Some(copilot) = Copilot::global(cx) { + if copilot.read(cx).status() == Status::Authorized { + return; + } + copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) .detach_and_log_err(cx); + + workspace.toggle_modal(cx, |_workspace, cx| cx.add_view(|_cx| AuthModal {})); } }); - cx.add_global_action(|_: &SignOut, cx: &mut MutableAppContext| { + cx.add_action(|_: &mut Workspace, _: &SignOut, cx| { if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| copilot.sign_out(cx)) .detach_and_log_err(cx); } }); + cx.add_action(|workspace: &mut Workspace, _: &ToggleAuthStatus, cx| { + workspace.toggle_modal(cx, |_workspace, cx| cx.add_view(|_cx| AuthModal {})) + }) } enum CopilotServer { @@ -62,7 +74,7 @@ pub enum Event { }, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Status { Downloading, Error(Arc), @@ -138,6 +150,7 @@ impl Copilot { }) }) .detach(); + Self { server: CopilotServer::Downloading, } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d64a1d2499..98419d1f4c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -23,6 +23,7 @@ pub struct Theme { pub context_menu: ContextMenu, pub contacts_popover: ContactsPopover, pub contact_list: ContactList, + pub copilot: Copilot, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, pub command_palette: CommandPalette, @@ -115,6 +116,11 @@ pub struct AvatarStyle { pub outer_corner_radius: f32, } +#[derive(Deserialize, Default)] +pub struct Copilot { + pub auth_modal: TextStyle, +} + #[derive(Deserialize, Default)] pub struct ContactsPopover { #[serde(flatten)] diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 423ce37d48..f3315aa7cd 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -21,6 +21,7 @@ import incomingCallNotification from "./incomingCallNotification" import { ColorScheme } from "../themes/common/colorScheme" import feedback from "./feedback" import welcome from "./welcome" +import copilot from "./copilot" export default function app(colorScheme: ColorScheme): Object { return { @@ -34,6 +35,7 @@ export default function app(colorScheme: ColorScheme): Object { incomingCallNotification: incomingCallNotification(colorScheme), picker: picker(colorScheme), workspace: workspace(colorScheme), + copilot: copilot(colorScheme), welcome: welcome(colorScheme), contextMenu: contextMenu(colorScheme), editor: editor(colorScheme), diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts new file mode 100644 index 0000000000..2c087da5a0 --- /dev/null +++ b/styles/src/styleTree/copilot.ts @@ -0,0 +1,11 @@ +import { ColorScheme } from "../themes/common/colorScheme" +import { text } from "./components"; + + +export default function copilot(colorScheme: ColorScheme) { + let layer = colorScheme.highest; + + return { + authModal: text(layer, "sans") + } +} From 15e29d44b942d7c3d5aa51063ae53ad130d71a53 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 23 Mar 2023 19:58:36 -0700 Subject: [PATCH 11/53] Add basic copilot modal --- crates/copilot/src/auth_modal.rs | 72 +++++++++++++++++++-- crates/copilot/src/copilot.rs | 108 +++++++++++++++++++++---------- crates/theme/src/theme.rs | 5 +- styles/src/styleTree/copilot.ts | 14 +++- 4 files changed, 158 insertions(+), 41 deletions(-) diff --git a/crates/copilot/src/auth_modal.rs b/crates/copilot/src/auth_modal.rs index 4786f1d470..b9ba50507b 100644 --- a/crates/copilot/src/auth_modal.rs +++ b/crates/copilot/src/auth_modal.rs @@ -1,10 +1,19 @@ -use gpui::{elements::Label, Element, Entity, View}; +use gpui::{ + elements::{Flex, Label, MouseEventHandler, ParentElement, Stack}, + Axis, Element, Entity, View, ViewContext, +}; use settings::Settings; +use crate::{Copilot, PromptingUser}; + +pub enum Event { + Dismiss, +} + pub struct AuthModal {} impl Entity for AuthModal { - type Event = (); + type Event = Event; } impl View for AuthModal { @@ -13,8 +22,63 @@ impl View for AuthModal { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let style = &cx.global::().theme.copilot; + let style = cx.global::().theme.copilot.clone(); - Label::new("[COPILOT AUTH INFO]", style.auth_modal.clone()).boxed() + let user_code_and_url = Copilot::global(cx).read(cx).prompting_user().cloned(); + let auth_text = style.auth_text.clone(); + MouseEventHandler::::new(0, cx, move |_state, cx| { + Stack::new() + .with_child(match user_code_and_url { + Some(PromptingUser { + user_code, + verification_uri, + }) => Flex::new(Axis::Vertical) + .with_children([ + Label::new(user_code, auth_text.clone()) + .constrained() + .with_width(540.) + .boxed(), + MouseEventHandler::::new(1, cx, move |_state, _cx| { + Label::new("Click here to open github!", auth_text.clone()) + .constrained() + .with_width(540.) + .boxed() + }) + .on_click(gpui::MouseButton::Left, move |_click, cx| { + cx.platform().open_url(&verification_uri) + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed(), + ]) + .boxed(), + None => Label::new("Not signing in", style.auth_text.clone()) + .constrained() + .with_width(540.) + .boxed(), + }) + .contained() + .with_style(style.auth_modal) + .constrained() + .with_max_width(540.) + .with_max_height(420.) + .named("Copilot Authentication status modal") + }) + .on_hover(|_, _| {}) + .on_click(gpui::MouseButton::Left, |_, _| {}) + .on_click(gpui::MouseButton::Left, |_, _| {}) + .boxed() + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.emit(Event::Dismiss) + } +} + +impl AuthModal { + pub fn new(cx: &mut ViewContext) -> Self { + cx.observe(&Copilot::global(cx), |_, _, cx| cx.notify()) + .detach(); + + AuthModal {} } } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 2bdcedb2cc..a688cfe85c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -26,30 +26,44 @@ pub fn init(client: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); cx.set_global(copilot.clone()); cx.add_action(|workspace: &mut Workspace, _: &SignIn, cx| { - if let Some(copilot) = Copilot::global(cx) { - if copilot.read(cx).status() == Status::Authorized { - return; - } - - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - - workspace.toggle_modal(cx, |_workspace, cx| cx.add_view(|_cx| AuthModal {})); + let copilot = Copilot::global(cx); + if copilot.read(cx).status() == Status::Authorized { + return; } + + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + + workspace.toggle_modal(cx, |_workspace, cx| build_auth_modal(cx)); }); - cx.add_action(|_: &mut Workspace, _: &SignOut, cx| { - if let Some(copilot) = Copilot::global(cx) { - copilot - .update(cx, |copilot, cx| copilot.sign_out(cx)) - .detach_and_log_err(cx); + cx.add_action(|workspace: &mut Workspace, _: &SignOut, cx| { + let copilot = Copilot::global(cx); + + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .detach_and_log_err(cx); + + if workspace.modal::().is_some() { + workspace.dismiss_modal(cx) } }); cx.add_action(|workspace: &mut Workspace, _: &ToggleAuthStatus, cx| { - workspace.toggle_modal(cx, |_workspace, cx| cx.add_view(|_cx| AuthModal {})) + workspace.toggle_modal(cx, |_workspace, cx| build_auth_modal(cx)) }) } +fn build_auth_modal(cx: &mut gpui::ViewContext) -> gpui::ViewHandle { + let modal = cx.add_view(|cx| AuthModal::new(cx)); + + cx.subscribe(&modal, |workspace, _, e: &auth_modal::Event, cx| match e { + auth_modal::Event::Dismiss => workspace.dismiss_modal(cx), + }) + .detach(); + + modal +} + enum CopilotServer { Downloading, Error(Arc), @@ -59,10 +73,17 @@ enum CopilotServer { }, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct PromptingUser { + user_code: String, + verification_uri: String, +} + #[derive(Clone, Debug, PartialEq, Eq)] enum SignInStatus { Authorized { user: String }, Unauthorized { user: String }, + PromptingUser(PromptingUser), SignedOut, } @@ -104,20 +125,12 @@ impl Entity for Copilot { } impl Copilot { - fn global(cx: &AppContext) -> Option> { - if cx.has_global::>() { - let copilot = cx.global::>().clone(); - if copilot.read(cx).status().is_authorized() { - Some(copilot) - } else { - None - } - } else { - None - } + fn global(cx: &AppContext) -> ModelHandle { + cx.global::>().clone() } fn start(http: Arc, cx: &mut ModelContext) -> Self { + // TODO: Don't eagerly download the LSP cx.spawn(|this, mut cx| async move { let start_language_server = async { let server_path = get_lsp_binary(http).await?; @@ -164,17 +177,20 @@ impl Copilot { .request::(request::SignInInitiateParams {}) .await?; if let request::SignInInitiateResult::PromptUserDeviceFlow(flow) = sign_in { - this.update(&mut cx, |_, cx| { - cx.emit(Event::PromptUserDeviceFlow { - user_code: flow.user_code.clone(), - verification_uri: flow.verification_uri, - }); + this.update(&mut cx, |this, cx| { + this.update_prompting_user( + flow.user_code.clone(), + flow.verification_uri, + cx, + ); }); + // TODO: catch an error here and clear the corresponding user code let response = server .request::(request::SignInConfirmParams { user_code: flow.user_code, }) .await?; + this.update(&mut cx, |this, cx| this.update_sign_in_status(response, cx)); } anyhow::Ok(()) @@ -268,12 +284,38 @@ impl Copilot { CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Started { status, .. } => match status { SignInStatus::Authorized { .. } => Status::Authorized, - SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::Unauthorized { .. } | SignInStatus::PromptingUser { .. } => { + Status::Unauthorized + } SignInStatus::SignedOut => Status::SignedOut, }, } } + pub fn prompting_user(&self) -> Option<&PromptingUser> { + if let CopilotServer::Started { status, .. } = &self.server { + if let SignInStatus::PromptingUser(prompt) = status { + return Some(prompt); + } + } + None + } + + fn update_prompting_user( + &mut self, + user_code: String, + verification_uri: String, + cx: &mut ModelContext, + ) { + if let CopilotServer::Started { status, .. } = &mut self.server { + *status = SignInStatus::PromptingUser(PromptingUser { + user_code, + verification_uri, + }); + cx.notify(); + } + } + fn update_sign_in_status( &mut self, lsp_status: request::SignInStatus, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 98419d1f4c..d6a4a431e9 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -116,9 +116,10 @@ pub struct AvatarStyle { pub outer_corner_radius: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, Clone)] pub struct Copilot { - pub auth_modal: TextStyle, + pub auth_modal: ContainerStyle, + pub auth_text: TextStyle, } #[derive(Deserialize, Default)] diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index 2c087da5a0..66f5c63b4e 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -1,11 +1,21 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { text } from "./components"; +import { background, border, text } from "./components"; export default function copilot(colorScheme: ColorScheme) { let layer = colorScheme.highest; + return { - authModal: text(layer, "sans") + authModal: { + background: background(colorScheme.lowest), + border: border(colorScheme.lowest), + shadow: colorScheme.modalShadow, + cornerRadius: 12, + padding: { + bottom: 4, + }, + }, + authText: text(layer, "sans") } } From 19cc86a2d4a7836f06d574d1cf265b92dcb20cf0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 23 Mar 2023 20:34:58 -0700 Subject: [PATCH 12/53] Wait to show the auth modal until the sign request has returned --- crates/copilot/src/copilot.rs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index a688cfe85c..3c0c492920 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -25,17 +25,28 @@ actions!(copilot, [SignIn, SignOut, ToggleAuthStatus]); pub fn init(client: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); cx.set_global(copilot.clone()); - cx.add_action(|workspace: &mut Workspace, _: &SignIn, cx| { + cx.add_action(|_workspace: &mut Workspace, _: &SignIn, cx| { let copilot = Copilot::global(cx); if copilot.read(cx).status() == Status::Authorized { return; } + if !copilot.read(cx).has_subscription() { + let display_subscription = + cx.subscribe(&copilot, |workspace, _copilot, e, cx| match e { + Event::PromptUserDeviceFlow => { + workspace.toggle_modal(cx, |_workspace, cx| build_auth_modal(cx)); + } + }); + + copilot.update(cx, |copilot, _cx| { + copilot.set_subscription(display_subscription) + }) + } + copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) .detach_and_log_err(cx); - - workspace.toggle_modal(cx, |_workspace, cx| build_auth_modal(cx)); }); cx.add_action(|workspace: &mut Workspace, _: &SignOut, cx| { let copilot = Copilot::global(cx); @@ -89,10 +100,7 @@ enum SignInStatus { #[derive(Debug)] pub enum Event { - PromptUserDeviceFlow { - user_code: String, - verification_uri: String, - }, + PromptUserDeviceFlow, } #[derive(Debug, PartialEq, Eq)] @@ -118,6 +126,7 @@ pub struct Completion { struct Copilot { server: CopilotServer, + _display_subscription: Option, } impl Entity for Copilot { @@ -129,6 +138,15 @@ impl Copilot { cx.global::>().clone() } + fn has_subscription(&self) -> bool { + self._display_subscription.is_some() + } + + fn set_subscription(&mut self, display_subscription: gpui::Subscription) { + debug_assert!(self._display_subscription.is_none()); + self._display_subscription = Some(display_subscription); + } + fn start(http: Arc, cx: &mut ModelContext) -> Self { // TODO: Don't eagerly download the LSP cx.spawn(|this, mut cx| async move { @@ -166,6 +184,7 @@ impl Copilot { Self { server: CopilotServer::Downloading, + _display_subscription: None, } } @@ -183,6 +202,8 @@ impl Copilot { flow.verification_uri, cx, ); + + cx.emit(Event::PromptUserDeviceFlow) }); // TODO: catch an error here and clear the corresponding user code let response = server From 99cca59c84c180ca34d8d48ed47301d97ba6e96d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Mar 2023 12:05:07 +0100 Subject: [PATCH 13/53] Restructure verification code prompting to open a window instead Also, prevent multiple calls to `sign_in` from racing with each other. --- crates/copilot/src/auth_modal.rs | 84 --------- crates/copilot/src/copilot.rs | 240 +++++++++++-------------- crates/copilot/src/request.rs | 2 +- crates/copilot/src/sign_in.rs | 98 ++++++++++ crates/gpui/src/platform/mac/window.rs | 1 + 5 files changed, 203 insertions(+), 222 deletions(-) delete mode 100644 crates/copilot/src/auth_modal.rs create mode 100644 crates/copilot/src/sign_in.rs diff --git a/crates/copilot/src/auth_modal.rs b/crates/copilot/src/auth_modal.rs deleted file mode 100644 index b9ba50507b..0000000000 --- a/crates/copilot/src/auth_modal.rs +++ /dev/null @@ -1,84 +0,0 @@ -use gpui::{ - elements::{Flex, Label, MouseEventHandler, ParentElement, Stack}, - Axis, Element, Entity, View, ViewContext, -}; -use settings::Settings; - -use crate::{Copilot, PromptingUser}; - -pub enum Event { - Dismiss, -} - -pub struct AuthModal {} - -impl Entity for AuthModal { - type Event = Event; -} - -impl View for AuthModal { - fn ui_name() -> &'static str { - "AuthModal" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let style = cx.global::().theme.copilot.clone(); - - let user_code_and_url = Copilot::global(cx).read(cx).prompting_user().cloned(); - let auth_text = style.auth_text.clone(); - MouseEventHandler::::new(0, cx, move |_state, cx| { - Stack::new() - .with_child(match user_code_and_url { - Some(PromptingUser { - user_code, - verification_uri, - }) => Flex::new(Axis::Vertical) - .with_children([ - Label::new(user_code, auth_text.clone()) - .constrained() - .with_width(540.) - .boxed(), - MouseEventHandler::::new(1, cx, move |_state, _cx| { - Label::new("Click here to open github!", auth_text.clone()) - .constrained() - .with_width(540.) - .boxed() - }) - .on_click(gpui::MouseButton::Left, move |_click, cx| { - cx.platform().open_url(&verification_uri) - }) - .with_cursor_style(gpui::CursorStyle::PointingHand) - .boxed(), - ]) - .boxed(), - None => Label::new("Not signing in", style.auth_text.clone()) - .constrained() - .with_width(540.) - .boxed(), - }) - .contained() - .with_style(style.auth_modal) - .constrained() - .with_max_width(540.) - .with_max_height(420.) - .named("Copilot Authentication status modal") - }) - .on_hover(|_, _| {}) - .on_click(gpui::MouseButton::Left, |_, _| {}) - .on_click(gpui::MouseButton::Left, |_, _| {}) - .boxed() - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - cx.emit(Event::Dismiss) - } -} - -impl AuthModal { - pub fn new(cx: &mut ViewContext) -> Self { - cx.observe(&Copilot::global(cx), |_, _, cx| cx.notify()) - .detach(); - - AuthModal {} - } -} diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 3c0c492920..f9f019268d 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1,10 +1,10 @@ -mod auth_modal; mod request; +mod sign_in; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; -use auth_modal::AuthModal; use client::Client; +use futures::{future::Shared, FutureExt, TryFutureExt}; use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, ToPointUtf16}; use lsp::LanguageServer; @@ -18,61 +18,25 @@ use std::{ use util::{ fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, }; -use workspace::Workspace; -actions!(copilot, [SignIn, SignOut, ToggleAuthStatus]); +actions!(copilot, [SignIn, SignOut]); pub fn init(client: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); cx.set_global(copilot.clone()); - cx.add_action(|_workspace: &mut Workspace, _: &SignIn, cx| { + cx.add_global_action(|_: &SignIn, cx| { let copilot = Copilot::global(cx); - if copilot.read(cx).status() == Status::Authorized { - return; - } - - if !copilot.read(cx).has_subscription() { - let display_subscription = - cx.subscribe(&copilot, |workspace, _copilot, e, cx| match e { - Event::PromptUserDeviceFlow => { - workspace.toggle_modal(cx, |_workspace, cx| build_auth_modal(cx)); - } - }); - - copilot.update(cx, |copilot, _cx| { - copilot.set_subscription(display_subscription) - }) - } - copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) .detach_and_log_err(cx); }); - cx.add_action(|workspace: &mut Workspace, _: &SignOut, cx| { + cx.add_global_action(|_: &SignOut, cx| { let copilot = Copilot::global(cx); - copilot .update(cx, |copilot, cx| copilot.sign_out(cx)) .detach_and_log_err(cx); - - if workspace.modal::().is_some() { - workspace.dismiss_modal(cx) - } }); - cx.add_action(|workspace: &mut Workspace, _: &ToggleAuthStatus, cx| { - workspace.toggle_modal(cx, |_workspace, cx| build_auth_modal(cx)) - }) -} - -fn build_auth_modal(cx: &mut gpui::ViewContext) -> gpui::ViewHandle { - let modal = cx.add_view(|cx| AuthModal::new(cx)); - - cx.subscribe(&modal, |workspace, _, e: &auth_modal::Event, cx| match e { - auth_modal::Event::Dismiss => workspace.dismiss_modal(cx), - }) - .detach(); - - modal + sign_in::init(cx); } enum CopilotServer { @@ -84,40 +48,33 @@ enum CopilotServer { }, } -#[derive(Clone, Debug, PartialEq, Eq)] -struct PromptingUser { - user_code: String, - verification_uri: String, -} - -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] enum SignInStatus { - Authorized { user: String }, - Unauthorized { user: String }, - PromptingUser(PromptingUser), + Authorized { + user: String, + }, + Unauthorized { + user: String, + }, + SigningIn { + prompt: Option, + task: Shared>>>, + }, SignedOut, } -#[derive(Debug)] -pub enum Event { - PromptUserDeviceFlow, -} - #[derive(Debug, PartialEq, Eq)] pub enum Status { Downloading, Error(Arc), SignedOut, + SigningIn { + prompt: Option, + }, Unauthorized, Authorized, } -impl Status { - fn is_authorized(&self) -> bool { - matches!(self, Status::Authorized) - } -} - #[derive(Debug)] pub struct Completion { pub position: Anchor, @@ -126,11 +83,10 @@ pub struct Completion { struct Copilot { server: CopilotServer, - _display_subscription: Option, } impl Entity for Copilot { - type Event = Event; + type Event = (); } impl Copilot { @@ -138,15 +94,6 @@ impl Copilot { cx.global::>().clone() } - fn has_subscription(&self) -> bool { - self._display_subscription.is_some() - } - - fn set_subscription(&mut self, display_subscription: gpui::Subscription) { - debug_assert!(self._display_subscription.is_none()); - self._display_subscription = Some(display_subscription); - } - fn start(http: Arc, cx: &mut ModelContext) -> Self { // TODO: Don't eagerly download the LSP cx.spawn(|this, mut cx| async move { @@ -184,57 +131,99 @@ impl Copilot { Self { server: CopilotServer::Downloading, - _display_subscription: None, } } fn sign_in(&mut self, cx: &mut ModelContext) -> Task> { - if let CopilotServer::Started { server, .. } = &self.server { - let server = server.clone(); - cx.spawn(|this, mut cx| async move { - let sign_in = server - .request::(request::SignInInitiateParams {}) - .await?; - if let request::SignInInitiateResult::PromptUserDeviceFlow(flow) = sign_in { - this.update(&mut cx, |this, cx| { - this.update_prompting_user( - flow.user_code.clone(), - flow.verification_uri, - cx, - ); - - cx.emit(Event::PromptUserDeviceFlow) - }); - // TODO: catch an error here and clear the corresponding user code - let response = server - .request::(request::SignInConfirmParams { - user_code: flow.user_code, - }) - .await?; - - this.update(&mut cx, |this, cx| this.update_sign_in_status(response, cx)); + if let CopilotServer::Started { server, status } = &mut self.server { + let task = match status { + SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { + Task::ready(Ok(())).shared() } - anyhow::Ok(()) - }) + SignInStatus::SigningIn { task, .. } => task.clone(), + SignInStatus::SignedOut => { + let server = server.clone(); + let task = cx + .spawn(|this, mut cx| async move { + let sign_in = async { + let sign_in = server + .request::( + request::SignInInitiateParams {}, + ) + .await?; + match sign_in { + request::SignInInitiateResult::AlreadySignedIn { user } => { + Ok(request::SignInStatus::Ok { user }) + } + request::SignInInitiateResult::PromptUserDeviceFlow(flow) => { + this.update(&mut cx, |this, cx| { + if let CopilotServer::Started { status, .. } = + &mut this.server + { + if let SignInStatus::SigningIn { + prompt: prompt_flow, + .. + } = status + { + *prompt_flow = Some(flow.clone()); + cx.notify(); + } + } + }); + let response = server + .request::( + request::SignInConfirmParams { + user_code: flow.user_code, + }, + ) + .await?; + Ok(response) + } + } + }; + + let sign_in = sign_in.await; + this.update(&mut cx, |this, cx| match sign_in { + Ok(status) => { + this.update_sign_in_status(status, cx); + Ok(()) + } + Err(error) => { + this.update_sign_in_status( + request::SignInStatus::NotSignedIn, + cx, + ); + Err(Arc::new(error)) + } + }) + }) + .shared(); + *status = SignInStatus::SigningIn { + prompt: None, + task: task.clone(), + }; + cx.notify(); + task + } + }; + + cx.foreground() + .spawn(task.map_err(|err| anyhow!("{:?}", err))) } else { Task::ready(Err(anyhow!("copilot hasn't started yet"))) } } fn sign_out(&mut self, cx: &mut ModelContext) -> Task> { - if let CopilotServer::Started { server, .. } = &self.server { + if let CopilotServer::Started { server, status } = &mut self.server { + *status = SignInStatus::SignedOut; + cx.notify(); + let server = server.clone(); - cx.spawn(|this, mut cx| async move { + cx.background().spawn(async move { server .request::(request::SignOutParams {}) .await?; - this.update(&mut cx, |this, cx| { - if let CopilotServer::Started { status, .. } = &mut this.server { - *status = SignInStatus::SignedOut; - cx.notify(); - } - }); - anyhow::Ok(()) }) } else { @@ -305,38 +294,15 @@ impl Copilot { CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Started { status, .. } => match status { SignInStatus::Authorized { .. } => Status::Authorized, - SignInStatus::Unauthorized { .. } | SignInStatus::PromptingUser { .. } => { - Status::Unauthorized - } + SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { + prompt: prompt.clone(), + }, SignInStatus::SignedOut => Status::SignedOut, }, } } - pub fn prompting_user(&self) -> Option<&PromptingUser> { - if let CopilotServer::Started { status, .. } = &self.server { - if let SignInStatus::PromptingUser(prompt) = status { - return Some(prompt); - } - } - None - } - - fn update_prompting_user( - &mut self, - user_code: String, - verification_uri: String, - cx: &mut ModelContext, - ) { - if let CopilotServer::Started { status, .. } = &mut self.server { - *status = SignInStatus::PromptingUser(PromptingUser { - user_code, - verification_uri, - }); - cx.notify(); - } - } - fn update_sign_in_status( &mut self, lsp_status: request::SignInStatus, diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index f3a86698e1..ea7f4577b6 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -26,7 +26,7 @@ pub enum SignInInitiateResult { PromptUserDeviceFlow(PromptUserDeviceFlow), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PromptUserDeviceFlow { pub user_code: String, diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs new file mode 100644 index 0000000000..9551b2c413 --- /dev/null +++ b/crates/copilot/src/sign_in.rs @@ -0,0 +1,98 @@ +use crate::{request::PromptUserDeviceFlow, Copilot}; +use gpui::{ + elements::*, + geometry::{rect::RectF, vector::vec2f}, + Axis, Element, Entity, MutableAppContext, View, ViewContext, WindowKind, WindowOptions, +}; +use settings::Settings; + +pub fn init(cx: &mut MutableAppContext) { + let copilot = Copilot::global(cx); + + let mut code_verification_window_id = None; + cx.observe(&copilot, move |copilot, cx| { + match copilot.read(cx).status() { + crate::Status::SigningIn { + prompt: Some(prompt), + } => { + if let Some(window_id) = code_verification_window_id.take() { + cx.remove_window(window_id); + } + + let screen = cx.platform().screens().pop(); + let (window_id, _) = cx.add_window( + WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new( + vec2f(100., 100.), + vec2f(300., 300.), + )), + titlebar: None, + center: false, + focus: false, + kind: WindowKind::Normal, + is_movable: true, + screen, + }, + |_| CopilotCodeVerification::new(prompt), + ); + code_verification_window_id = Some(window_id); + } + _ => { + if let Some(window_id) = code_verification_window_id.take() { + cx.remove_window(window_id); + } + } + } + }) + .detach(); +} + +pub enum Event { + Dismiss, +} + +pub struct CopilotCodeVerification { + prompt: PromptUserDeviceFlow, +} + +impl Entity for CopilotCodeVerification { + type Event = Event; +} + +impl View for CopilotCodeVerification { + fn ui_name() -> &'static str { + "CopilotCodeVerification" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + let style = cx.global::().theme.copilot.clone(); + + let auth_text = style.auth_text.clone(); + let prompt = self.prompt.clone(); + Flex::new(Axis::Vertical) + .with_child(Label::new(prompt.user_code.clone(), auth_text.clone()).boxed()) + .with_child( + MouseEventHandler::::new(1, cx, move |_state, _cx| { + Label::new("Click here to open GitHub!", auth_text.clone()).boxed() + }) + .on_click(gpui::MouseButton::Left, move |_click, cx| { + cx.platform().open_url(&prompt.verification_uri) + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed(), + ) + .contained() + .with_style(style.auth_modal) + .named("Copilot Authentication status modal") + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.emit(Event::Dismiss) + } +} + +impl CopilotCodeVerification { + pub fn new(prompt: PromptUserDeviceFlow) -> Self { + CopilotCodeVerification { prompt } + } +} diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index a0c1820368..af3e244e75 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -473,6 +473,7 @@ impl Window { WindowBounds::Fixed(rect) => { let screen_frame = screen.visibleFrame(); let ns_rect = rect.to_ns_rect(); + dbg!(screen_frame.as_CGRect(), ns_rect.as_CGRect()); if ns_rect.intersects(screen_frame) { native_window.setFrame_display_(ns_rect, YES); } else { From 9713d1bb3179a9752fb5b1f2c44d7775348f6196 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Mar 2023 16:44:43 +0100 Subject: [PATCH 14/53] Fix invalid translation between bottom/top left coordinate spaces Co-Authored-By: Mikayla Maki --- crates/gpui/src/platform/mac/geometry.rs | 53 ++---------------------- crates/gpui/src/platform/mac/window.rs | 31 +++++++++++--- 2 files changed, 29 insertions(+), 55 deletions(-) diff --git a/crates/gpui/src/platform/mac/geometry.rs b/crates/gpui/src/platform/mac/geometry.rs index 6a47968118..3ff6c1d8cb 100644 --- a/crates/gpui/src/platform/mac/geometry.rs +++ b/crates/gpui/src/platform/mac/geometry.rs @@ -1,7 +1,6 @@ use cocoa::{ - appkit::NSWindow, base::id, - foundation::{NSPoint, NSRect, NSSize}, + foundation::{NSPoint, NSRect}, }; use objc::{msg_send, sel, sel_impl}; use pathfinder_geometry::{ @@ -25,61 +24,15 @@ impl Vector2FExt for Vector2F { } } -pub trait RectFExt { - /// Converts self to an NSRect with y axis pointing up. - /// The resulting NSRect will have an origin at the bottom left of the rectangle. - /// Also takes care of converting from window scaled coordinates to screen coordinates - fn to_screen_ns_rect(&self, native_window: id) -> NSRect; - - /// Converts self to an NSRect with y axis point up. - /// The resulting NSRect will have an origin at the bottom left of the rectangle. - /// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale - fn to_ns_rect(&self) -> NSRect; -} -impl RectFExt for RectF { - fn to_screen_ns_rect(&self, native_window: id) -> NSRect { - unsafe { native_window.convertRectToScreen_(self.to_ns_rect()) } - } - - fn to_ns_rect(&self) -> NSRect { - NSRect::new( - NSPoint::new( - self.origin_x() as f64, - -(self.origin_y() + self.height()) as f64, - ), - NSSize::new(self.width() as f64, self.height() as f64), - ) - } -} - pub trait NSRectExt { - /// Converts self to a RectF with y axis pointing down. - /// The resulting RectF will have an origin at the top left of the rectangle. - /// Also takes care of converting from screen scale coordinates to window coordinates - fn to_window_rectf(&self, native_window: id) -> RectF; - - /// Converts self to a RectF with y axis pointing down. - /// The resulting RectF will have an origin at the top left of the rectangle. - /// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale fn to_rectf(&self) -> RectF; - fn intersects(&self, other: Self) -> bool; } -impl NSRectExt for NSRect { - fn to_window_rectf(&self, native_window: id) -> RectF { - unsafe { - self.origin.x; - let rect: NSRect = native_window.convertRectFromScreen_(*self); - rect.to_rectf() - } - } +impl NSRectExt for NSRect { fn to_rectf(&self) -> RectF { RectF::new( - vec2f( - self.origin.x as f32, - -(self.origin.y + self.size.height) as f32, - ), + vec2f(self.origin.x as f32, self.origin.y as f32), vec2f(self.size.width as f32, self.size.height as f32), ) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index af3e244e75..c954a41729 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -8,7 +8,7 @@ use crate::{ mac::platform::NSViewLayerContentsRedrawDuringViewResize, platform::{ self, - mac::{geometry::RectFExt, renderer::Renderer, screen::Screen}, + mac::{renderer::Renderer, screen::Screen}, Event, WindowBounds, }, InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, @@ -372,7 +372,8 @@ impl WindowState { } let window_frame = self.frame(); - if window_frame == self.native_window.screen().visibleFrame().to_rectf() { + let screen_frame = self.native_window.screen().visibleFrame().to_rectf(); + if window_frame.size() == screen_frame.size() { WindowBounds::Maximized } else { WindowBounds::Fixed(window_frame) @@ -383,8 +384,19 @@ impl WindowState { // Returns the window bounds in window coordinates fn frame(&self) -> RectF { unsafe { - let ns_frame = NSWindow::frame(self.native_window); - ns_frame.to_rectf() + let screen_frame = self.native_window.screen().visibleFrame(); + let window_frame = NSWindow::frame(self.native_window); + RectF::new( + vec2f( + window_frame.origin.x as f32, + (screen_frame.size.height - window_frame.origin.y - window_frame.size.height) + as f32, + ), + vec2f( + window_frame.size.width as f32, + window_frame.size.height as f32, + ), + ) } } @@ -472,7 +484,16 @@ impl Window { } WindowBounds::Fixed(rect) => { let screen_frame = screen.visibleFrame(); - let ns_rect = rect.to_ns_rect(); + let ns_rect = NSRect::new( + NSPoint::new( + rect.origin_x() as f64, + screen_frame.size.height + - rect.origin_y() as f64 + - rect.height() as f64, + ), + NSSize::new(rect.width() as f64, rect.height() as f64), + ); + dbg!(screen_frame.as_CGRect(), ns_rect.as_CGRect()); if ns_rect.intersects(screen_frame) { native_window.setFrame_display_(ns_rect, YES); From b16e2169ce3c55fd2b02ecefc6f291927be60ef1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 24 Mar 2023 16:50:27 +0100 Subject: [PATCH 15/53] WIP: Start on showing window for authenticating with copilot Co-Authored-By: Mikayla Maki --- crates/copilot/src/sign_in.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 9551b2c413..1b1f0f816d 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -2,7 +2,7 @@ use crate::{request::PromptUserDeviceFlow, Copilot}; use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, - Axis, Element, Entity, MutableAppContext, View, ViewContext, WindowKind, WindowOptions, + Axis, Element, Entity, MutableAppContext, View, WindowKind, WindowOptions, }; use settings::Settings; @@ -19,19 +19,18 @@ pub fn init(cx: &mut MutableAppContext) { cx.remove_window(window_id); } - let screen = cx.platform().screens().pop(); let (window_id, _) = cx.add_window( WindowOptions { bounds: gpui::WindowBounds::Fixed(RectF::new( - vec2f(100., 100.), - vec2f(300., 300.), + Default::default(), + vec2f(600., 400.), )), titlebar: None, - center: false, + center: true, focus: false, kind: WindowKind::Normal, is_movable: true, - screen, + screen: None, }, |_| CopilotCodeVerification::new(prompt), ); @@ -47,16 +46,12 @@ pub fn init(cx: &mut MutableAppContext) { .detach(); } -pub enum Event { - Dismiss, -} - pub struct CopilotCodeVerification { prompt: PromptUserDeviceFlow, } impl Entity for CopilotCodeVerification { - type Event = Event; + type Event = (); } impl View for CopilotCodeVerification { @@ -85,10 +80,6 @@ impl View for CopilotCodeVerification { .with_style(style.auth_modal) .named("Copilot Authentication status modal") } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - cx.emit(Event::Dismiss) - } } impl CopilotCodeVerification { From 2f95510a2ee708e7f7dcc5cf68421a29d12fc3af Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 26 Mar 2023 12:30:21 +0200 Subject: [PATCH 16/53] Start integrating Copilot with editor There's still a bit to do in terms of reusing the previous suggestion when the prefix matches, but we're getting there. --- Cargo.lock | 1 + crates/copilot/src/copilot.rs | 26 ++-- crates/copilot/src/sign_in.rs | 2 +- crates/editor/Cargo.toml | 6 +- crates/editor/src/editor.rs | 159 +++++++++++++++++++++++++ crates/gpui/src/platform/mac/window.rs | 1 - 6 files changed, 183 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 202b511a6d..5c43455d54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1951,6 +1951,7 @@ dependencies = [ "clock", "collections", "context_menu", + "copilot", "ctor", "db", "drag_and_drop", diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index f9f019268d..af9b7042c8 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -25,13 +25,13 @@ pub fn init(client: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); cx.set_global(copilot.clone()); cx.add_global_action(|_: &SignIn, cx| { - let copilot = Copilot::global(cx); + let copilot = Copilot::global(cx).unwrap(); copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) .detach_and_log_err(cx); }); cx.add_global_action(|_: &SignOut, cx| { - let copilot = Copilot::global(cx); + let copilot = Copilot::global(cx).unwrap(); copilot .update(cx, |copilot, cx| copilot.sign_out(cx)) .detach_and_log_err(cx); @@ -75,13 +75,19 @@ pub enum Status { Authorized, } +impl Status { + pub fn is_authorized(&self) -> bool { + matches!(self, Status::Authorized) + } +} + #[derive(Debug)] pub struct Completion { pub position: Anchor, pub text: String, } -struct Copilot { +pub struct Copilot { server: CopilotServer, } @@ -90,8 +96,12 @@ impl Entity for Copilot { } impl Copilot { - fn global(cx: &AppContext) -> ModelHandle { - cx.global::>().clone() + pub fn global(cx: &AppContext) -> Option> { + if cx.has_global::>() { + Some(cx.global::>().clone()) + } else { + None + } } fn start(http: Arc, cx: &mut ModelContext) -> Self { @@ -240,7 +250,7 @@ impl Copilot { where T: ToPointUtf16, { - let server = match self.authenticated_server() { + let server = match self.authorized_server() { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; @@ -268,7 +278,7 @@ impl Copilot { where T: ToPointUtf16, { - let server = match self.authenticated_server() { + let server = match self.authorized_server() { Ok(server) => server, Err(error) => return Task::ready(Err(error)), }; @@ -322,7 +332,7 @@ impl Copilot { } } - fn authenticated_server(&self) -> Result> { + fn authorized_server(&self) -> Result> { match &self.server { CopilotServer::Downloading => Err(anyhow!("copilot is still downloading")), CopilotServer::Error(error) => Err(anyhow!( diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 1b1f0f816d..67b93385ac 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -7,7 +7,7 @@ use gpui::{ use settings::Settings; pub fn init(cx: &mut MutableAppContext) { - let copilot = Copilot::global(cx); + let copilot = Copilot::global(cx).unwrap(); let mut code_verification_window_id = None; cx.observe(&copilot, move |copilot, cx| { diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 5bbd72745d..ef2489d7ec 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -22,10 +22,10 @@ test-support = [ ] [dependencies] -drag_and_drop = { path = "../drag_and_drop" } -text = { path = "../text" } clock = { path = "../clock" } +copilot = { path = "../copilot" } db = { path = "../db" } +drag_and_drop = { path = "../drag_and_drop" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } fuzzy = { path = "../fuzzy" } @@ -38,10 +38,12 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } +text = { path = "../text" } theme = { path = "../theme" } util = { path = "../util" } sqlez = { path = "../sqlez" } workspace = { path = "../workspace" } + aho-corasick = "0.7" anyhow = "1.0" futures = "0.3" diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b9388dca78..49c45eed1f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -24,6 +24,7 @@ use anyhow::Result; use blink_manager::BlinkManager; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; +use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; @@ -96,6 +97,7 @@ const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); +pub const COPILOT_TIMEOUT: Duration = Duration::from_secs(1); #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { @@ -260,6 +262,7 @@ actions!( ToggleSoftWrap, RevealInFinder, CopyHighlightJson + CycleCopilotSuggestions ] ); @@ -388,6 +391,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::rename); cx.add_async_action(Editor::confirm_rename); cx.add_async_action(Editor::find_all_references); + cx.add_action(Editor::cycle_copilot_suggestions); hover_popover::init(cx); link_go_to_definition::init(cx); @@ -506,6 +510,7 @@ pub struct Editor { hover_state: HoverState, gutter_hovered: bool, link_go_to_definition_state: LinkGoToDefinitionState, + copilot_state: CopilotState, _subscriptions: Vec, } @@ -1003,6 +1008,30 @@ impl CodeActionsMenu { } } +struct CopilotState { + position: Anchor, + pending_refresh: Task>, + completions: Vec, + active_completion_index: usize, +} + +impl Default for CopilotState { + fn default() -> Self { + Self { + position: Anchor::min(), + pending_refresh: Task::ready(Some(())), + completions: Default::default(), + active_completion_index: 0, + } + } +} + +impl CopilotState { + fn active_completion(&self) -> Option<&copilot::Completion> { + self.completions.get(self.active_completion_index) + } +} + #[derive(Debug)] struct ActiveDiagnosticGroup { primary_range: Range, @@ -1176,6 +1205,7 @@ impl Editor { remote_id: None, hover_state: Default::default(), link_go_to_definition_state: Default::default(), + copilot_state: Default::default(), gutter_hovered: false, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -1385,6 +1415,7 @@ impl Editor { self.refresh_code_actions(cx); self.refresh_document_highlights(cx); refresh_matching_bracket_highlights(self, cx); + self.refresh_copilot_suggestions(cx); } self.blink_manager.update(cx, BlinkManager::pause_blinking); @@ -2677,6 +2708,129 @@ impl Editor { None } + fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext) -> Option<()> { + let copilot = Copilot::global(cx)?; + if self.mode != EditorMode::Full { + return None; + } + + self.copilot_state.completions.clear(); + self.copilot_state.active_completion_index = 0; + self.copilot_state.position = Anchor::min(); + self.display_map + .update(cx, |map, cx| map.replace_suggestion::(None, cx)); + cx.notify(); + + if !copilot.read(cx).status().is_authorized() { + return None; + } + + let selection = self.selections.newest_anchor(); + let position = if selection.start == selection.end { + selection.start + } else { + return None; + }; + let (buffer, buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(position, cx)?; + self.copilot_state.position = position; + self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move { + cx.background().timer(COPILOT_TIMEOUT).await; + let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| { + ( + copilot.completion(&buffer, buffer_position, cx), + copilot.completions_cycling(&buffer, buffer_position, cx), + ) + }); + + if let Some(completion) = completion.await.log_err() { + let this = this.upgrade(&cx)?; + this.update(&mut cx, |this, cx| { + if let Some(completion) = completion { + this.display_map.update(cx, |map, cx| { + map.replace_suggestion( + Some(Suggestion { + position, + text: completion.text.as_str().into(), + highlight_style: HighlightStyle { + color: Some(Color::from_u32(0x777777ff)), + ..Default::default() + }, + }), + cx, + ) + }); + this.copilot_state.completions.push(completion); + cx.notify(); + } + }); + } + + if let Some(completions) = completions_cycling.await.log_err() { + let this = this.upgrade(&cx)?; + this.update(&mut cx, |this, cx| { + let was_empty = this.copilot_state.completions.is_empty(); + if !completions.is_empty() { + if was_empty { + let completion = completions.first().unwrap(); + this.display_map.update(cx, |map, cx| { + map.replace_suggestion( + Some(Suggestion { + position, + text: completion.text.as_str().into(), + highlight_style: HighlightStyle { + color: Some(Color::from_u32(0x777777ff)), + ..Default::default() + }, + }), + cx, + ) + }); + cx.notify(); + } + this.copilot_state.completions.extend(completions); + } + }); + } + + Some(()) + }); + + Some(()) + } + + fn cycle_copilot_suggestions( + &mut self, + _: &CycleCopilotSuggestions, + cx: &mut ViewContext, + ) { + if self.copilot_state.completions.is_empty() { + return; + } + + self.copilot_state.active_completion_index = + (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len(); + if let Some(completion) = self.copilot_state.active_completion() { + self.display_map.update(cx, |map, cx| { + map.replace_suggestion( + Some(Suggestion { + position: self.copilot_state.position, + text: completion.text.as_str().into(), + highlight_style: HighlightStyle { + color: Some(Color::from_u32(0x777777ff)), + ..Default::default() + }, + }), + cx, + ) + }); + } + + cx.notify(); + } + pub fn render_code_actions_indicator( &self, style: &EditorStyle, @@ -2984,6 +3138,11 @@ impl Editor { } pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + if let Some(completion) = self.copilot_state.active_completion() { + self.insert(&completion.text.to_string(), cx); + return; + } + if self.move_to_next_snippet_tabstop(cx) { return; } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index c954a41729..5d28397f8b 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -494,7 +494,6 @@ impl Window { NSSize::new(rect.width() as f64, rect.height() as f64), ); - dbg!(screen_frame.as_CGRect(), ns_rect.as_CGRect()); if ns_rect.intersects(screen_frame) { native_window.setFrame_display_(ns_rect, YES); } else { From 093e0a30e9b880ed28d8615d75906a17acead1a0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 27 Mar 2023 11:00:36 +0200 Subject: [PATCH 17/53] Replace `editor::CycleCopilotSuggestions` with `copilot::NextSuggestion` --- assets/keymaps/default.json | 3 ++- crates/copilot/src/copilot.rs | 2 +- crates/editor/src/editor.rs | 9 ++------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index adda80d9ed..03e24c8bc3 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -176,7 +176,8 @@ { "focus": false } - ] + ], + "alt-]": "copilot::NextSuggestion" } }, { diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index af9b7042c8..f0d3ed7258 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -19,7 +19,7 @@ use util::{ fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, }; -actions!(copilot, [SignIn, SignOut]); +actions!(copilot, [SignIn, SignOut, NextSuggestion]); pub fn init(client: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 49c45eed1f..edfacae268 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -262,7 +262,6 @@ actions!( ToggleSoftWrap, RevealInFinder, CopyHighlightJson - CycleCopilotSuggestions ] ); @@ -391,7 +390,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::rename); cx.add_async_action(Editor::confirm_rename); cx.add_async_action(Editor::find_all_references); - cx.add_action(Editor::cycle_copilot_suggestions); + cx.add_action(Editor::next_copilot_suggestion); hover_popover::init(cx); link_go_to_definition::init(cx); @@ -2801,11 +2800,7 @@ impl Editor { Some(()) } - fn cycle_copilot_suggestions( - &mut self, - _: &CycleCopilotSuggestions, - cx: &mut ViewContext, - ) { + fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { if self.copilot_state.completions.is_empty() { return; } From 6715e5247cd78fc325375b550eeb73317b8c7004 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 27 Mar 2023 11:54:33 +0200 Subject: [PATCH 18/53] Rework `SuggestionMap` to take highlight style when retrieving chunks --- crates/editor/src/display_map.rs | 21 ++++-- crates/editor/src/display_map/block_map.rs | 7 +- .../editor/src/display_map/suggestion_map.rs | 24 ++++--- crates/editor/src/display_map/tab_map.rs | 18 +++-- crates/editor/src/display_map/wrap_map.rs | 12 ++-- crates/editor/src/editor.rs | 12 ---- crates/editor/src/element.rs | 70 ++++++++++--------- crates/theme/src/theme.rs | 1 + styles/src/styleTree/editor.ts | 3 + 9 files changed, 95 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index f49b9e34b8..7788e8d38d 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -382,7 +382,7 @@ impl DisplaySnapshot { /// Returns text chunks starting at the given display row until the end of the file pub fn text_chunks(&self, display_row: u32) -> impl Iterator { self.block_snapshot - .chunks(display_row..self.max_point().row() + 1, false, None) + .chunks(display_row..self.max_point().row() + 1, false, None, None) .map(|h| h.text) } @@ -390,7 +390,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None) + .chunks(row..row + 1, false, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -398,9 +398,18 @@ impl DisplaySnapshot { }) } - pub fn chunks(&self, display_rows: Range, language_aware: bool) -> DisplayChunks<'_> { - self.block_snapshot - .chunks(display_rows, language_aware, Some(&self.text_highlights)) + pub fn chunks( + &self, + display_rows: Range, + language_aware: bool, + suggestion_highlight: Option, + ) -> DisplayChunks<'_> { + self.block_snapshot.chunks( + display_rows, + language_aware, + Some(&self.text_highlights), + suggestion_highlight, + ) } pub fn chars_at( @@ -1691,7 +1700,7 @@ pub mod tests { ) -> Vec<(String, Option, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let mut chunks: Vec<(String, Option, Option)> = Vec::new(); - for chunk in snapshot.chunks(rows, true) { + for chunk in snapshot.chunks(rows, true, None) { let syntax_color = chunk .syntax_highlight_id .and_then(|id| id.style(theme)?.color); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index adea668179..c4af03e703 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; -use gpui::{ElementBox, RenderContext}; +use gpui::{fonts::HighlightStyle, ElementBox, RenderContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; use parking_lot::Mutex; use std::{ @@ -572,7 +572,7 @@ impl<'a> BlockMapWriter<'a> { impl BlockSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(0..self.transforms.summary().output_rows, false, None) + self.chunks(0..self.transforms.summary().output_rows, false, None, None) .map(|chunk| chunk.text) .collect() } @@ -582,6 +582,7 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + suggestion_highlight: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -614,6 +615,7 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, + suggestion_highlight, ), input_chunk: Default::default(), transforms: cursor, @@ -1498,6 +1500,7 @@ mod tests { start_row as u32..blocks_snapshot.max_point().row + 1, false, None, + None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/suggestion_map.rs b/crates/editor/src/display_map/suggestion_map.rs index 2d0225644f..e76904df17 100644 --- a/crates/editor/src/display_map/suggestion_map.rs +++ b/crates/editor/src/display_map/suggestion_map.rs @@ -60,7 +60,6 @@ impl SuggestionPoint { pub struct Suggestion { pub position: T, pub text: Rope, - pub highlight_style: HighlightStyle, } pub struct SuggestionMap(Mutex); @@ -93,7 +92,6 @@ impl SuggestionMap { Suggestion { position: fold_offset, text: new_suggestion.text, - highlight_style: new_suggestion.highlight_style, } }); @@ -369,7 +367,7 @@ impl SuggestionSnapshot { pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator { let start = self.to_offset(start); - self.chunks(start..self.len(), false, None) + self.chunks(start..self.len(), false, None, None) .flat_map(|chunk| chunk.text.chars()) } @@ -378,6 +376,7 @@ impl SuggestionSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + suggestion_highlight: Option, ) -> SuggestionChunks<'a> { if let Some(suggestion) = self.suggestion.as_ref() { let suggestion_range = @@ -421,7 +420,7 @@ impl SuggestionSnapshot { prefix_chunks, suggestion_chunks, suffix_chunks, - highlight_style: suggestion.highlight_style, + highlight_style: suggestion_highlight, } } else { SuggestionChunks { @@ -432,7 +431,7 @@ impl SuggestionSnapshot { )), suggestion_chunks: None, suffix_chunks: None, - highlight_style: Default::default(), + highlight_style: None, } } } @@ -467,7 +466,7 @@ impl SuggestionSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(Default::default()..self.len(), false, None) + self.chunks(Default::default()..self.len(), false, None, None) .map(|chunk| chunk.text) .collect() } @@ -477,7 +476,7 @@ pub struct SuggestionChunks<'a> { prefix_chunks: Option>, suggestion_chunks: Option>, suffix_chunks: Option>, - highlight_style: HighlightStyle, + highlight_style: Option, } impl<'a> Iterator for SuggestionChunks<'a> { @@ -497,7 +496,7 @@ impl<'a> Iterator for SuggestionChunks<'a> { return Some(Chunk { text: chunk, syntax_highlight_id: None, - highlight_style: Some(self.highlight_style), + highlight_style: self.highlight_style, diagnostic_severity: None, is_unnecessary: false, }); @@ -563,7 +562,6 @@ mod tests { Some(Suggestion { position: 3, text: "123\n456".into(), - highlight_style: Default::default(), }), fold_snapshot, Default::default(), @@ -692,7 +690,12 @@ mod tests { start = expected_text.clip_offset(start, Bias::Right); let actual_text = suggestion_snapshot - .chunks(SuggestionOffset(start)..SuggestionOffset(end), false, None) + .chunks( + SuggestionOffset(start)..SuggestionOffset(end), + false, + None, + None, + ) .map(|chunk| chunk.text) .collect::(); assert_eq!( @@ -816,7 +819,6 @@ mod tests { .collect::() .as_str() .into(), - highlight_style: Default::default(), }) }; diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 45c92ea7b8..b77556175d 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -3,6 +3,7 @@ use super::{ TextHighlights, }; use crate::MultiBufferSnapshot; +use gpui::fonts::HighlightStyle; use language::{Chunk, Point}; use parking_lot::Mutex; use std::{cmp, mem, num::NonZeroU32, ops::Range}; @@ -47,6 +48,7 @@ impl TabMap { suggestion_edit.old.end..old_max_offset, false, None, + None, ) { let patterns: &[_] = &['\t', '\n']; if let Some(ix) = chunk.text.find(patterns) { @@ -126,6 +128,7 @@ impl TabSnapshot { TabPoint::new(row, 0)..TabPoint::new(row + 1, 0), false, None, + None, ) .map(|chunk| chunk.text.len() as u32) .sum::() @@ -153,7 +156,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None) + .chunks(range.start..line_end, false, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -167,7 +170,12 @@ impl TabSnapshot { last_line_chars = first_line_chars; } else { for _ in self - .chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None) + .chunks( + TabPoint::new(range.end.row(), 0)..range.end, + false, + None, + None, + ) .flat_map(|chunk| chunk.text.chars()) { last_line_chars += 1; @@ -188,6 +196,7 @@ impl TabSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + suggestion_highlight: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_suggestion_point(range.start, Bias::Left); @@ -206,6 +215,7 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, + suggestion_highlight, ), column: expanded_char_column, output_position: range.start.0, @@ -225,7 +235,7 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false, None) + self.chunks(TabPoint::zero()..self.max_point(), false, None, None) .map(|chunk| chunk.text) .collect() } @@ -574,7 +584,7 @@ mod tests { assert_eq!( expected_text, tabs_snapshot - .chunks(start..end, false, None) + .chunks(start..end, false, None, None) .map(|c| c.text) .collect::(), "chunks({:?}..{:?})", diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f0d10ad423..e55953d2b5 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -5,8 +5,9 @@ use super::{ }; use crate::MultiBufferSnapshot; use gpui::{ - fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext, - Task, + fonts::{FontId, HighlightStyle}, + text_layout::LineWrapper, + Entity, ModelContext, ModelHandle, MutableAppContext, Task, }; use language::{Chunk, Point}; use lazy_static::lazy_static; @@ -444,6 +445,7 @@ impl WrapSnapshot { TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), false, None, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -573,6 +575,7 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + suggestion_highlight: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -590,6 +593,7 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, + suggestion_highlight, ), input_chunk: Default::default(), output_position: output_start, @@ -1315,7 +1319,7 @@ mod tests { } pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks(wrap_row..self.max_point().row() + 1, false, None) + self.chunks(wrap_row..self.max_point().row() + 1, false, None, None) .map(|h| h.text) } @@ -1339,7 +1343,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None) + .chunks(start_row..end_row, true, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index edfacae268..40428e1261 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2753,10 +2753,6 @@ impl Editor { Some(Suggestion { position, text: completion.text.as_str().into(), - highlight_style: HighlightStyle { - color: Some(Color::from_u32(0x777777ff)), - ..Default::default() - }, }), cx, ) @@ -2779,10 +2775,6 @@ impl Editor { Some(Suggestion { position, text: completion.text.as_str().into(), - highlight_style: HighlightStyle { - color: Some(Color::from_u32(0x777777ff)), - ..Default::default() - }, }), cx, ) @@ -2813,10 +2805,6 @@ impl Editor { Some(Suggestion { position: self.copilot_state.position, text: completion.text.as_str().into(), - highlight_style: HighlightStyle { - color: Some(Color::from_u32(0x777777ff)), - ..Default::default() - }, }), cx, ) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 780f8cd1d5..c349559d7d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1318,45 +1318,47 @@ impl EditorElement { .collect() } else { let style = &self.style; - let chunks = snapshot.chunks(rows.clone(), true).map(|chunk| { - let mut highlight_style = chunk - .syntax_highlight_id - .and_then(|id| id.style(&style.syntax)); + let chunks = snapshot + .chunks(rows.clone(), true, Some(style.theme.suggestion)) + .map(|chunk| { + let mut highlight_style = chunk + .syntax_highlight_id + .and_then(|id| id.style(&style.syntax)); + + if let Some(chunk_highlight) = chunk.highlight_style { + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(chunk_highlight); + } else { + highlight_style = Some(chunk_highlight); + } + } + + let mut diagnostic_highlight = HighlightStyle::default(); + + if chunk.is_unnecessary { + diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); + } + + if let Some(severity) = chunk.diagnostic_severity { + // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. + if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { + let diagnostic_style = super::diagnostic_style(severity, true, style); + diagnostic_highlight.underline = Some(Underline { + color: Some(diagnostic_style.message.text.color), + thickness: 1.0.into(), + squiggly: true, + }); + } + } - if let Some(chunk_highlight) = chunk.highlight_style { if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(chunk_highlight); + highlight_style.highlight(diagnostic_highlight); } else { - highlight_style = Some(chunk_highlight); + highlight_style = Some(diagnostic_highlight); } - } - let mut diagnostic_highlight = HighlightStyle::default(); - - if chunk.is_unnecessary { - diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); - } - - if let Some(severity) = chunk.diagnostic_severity { - // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. - if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { - let diagnostic_style = super::diagnostic_style(severity, true, style); - diagnostic_highlight.underline = Some(Underline { - color: Some(diagnostic_style.message.text.color), - thickness: 1.0.into(), - squiggly: true, - }); - } - } - - if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(diagnostic_highlight); - } else { - highlight_style = Some(diagnostic_highlight); - } - - (chunk.text, highlight_style) - }); + (chunk.text, highlight_style) + }); layout_highlighted_chunks( chunks, &style.text, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d6a4a431e9..ef6a73f5d7 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -573,6 +573,7 @@ pub struct Editor { pub line_number_active: Color, pub guest_selections: Vec, pub syntax: Arc, + pub suggestion: HighlightStyle, pub diagnostic_path_header: DiagnosticPathHeader, pub diagnostic_header: DiagnosticHeader, pub error_diagnostic: DiagnosticStyle, diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 4a7aae4c1b..e0c73109fc 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -43,6 +43,9 @@ export default function editor(colorScheme: ColorScheme) { background: background(layer), activeLineBackground: withOpacity(background(layer, "on"), 0.75), highlightedLineBackground: background(layer, "on"), + suggestion: { + color: foreground(layer, "disabled") + }, codeActions: { indicator: { color: foreground(layer, "variant"), From 1162615043d502be075ad2dafd4f772f41106447 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 27 Mar 2023 16:21:43 +0200 Subject: [PATCH 19/53] Reuse existing suggestion when inserting to avoid flickering --- crates/copilot/src/copilot.rs | 2 +- crates/editor/src/editor.rs | 168 +++++++++++++++++++++++----------- 2 files changed, 117 insertions(+), 53 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index f0d3ed7258..aa36991fac 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -81,7 +81,7 @@ impl Status { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct Completion { pub position: Anchor, pub text: String, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 40428e1261..cea0d0e8ec 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1026,8 +1026,45 @@ impl Default for CopilotState { } impl CopilotState { - fn active_completion(&self) -> Option<&copilot::Completion> { - self.completions.get(self.active_completion_index) + fn text_for_active_completion( + &self, + cursor: Anchor, + buffer: &MultiBufferSnapshot, + ) -> Option<&str> { + let completion = self.completions.get(self.active_completion_index)?; + if self.position.excerpt_id == cursor.excerpt_id + && self.position.buffer_id == cursor.buffer_id + && buffer.chars_at(cursor).next().map_or(true, |ch| ch == '\n') + { + let completion_position = Anchor { + excerpt_id: self.position.excerpt_id, + buffer_id: self.position.buffer_id, + text_anchor: completion.position, + }; + if completion_position.cmp(&cursor, buffer).is_le() { + let prefix = buffer + .text_for_range(completion_position..cursor) + .collect::(); + let suffix = completion.text.strip_prefix(&prefix)?; + if !suffix.is_empty() { + return Some(suffix); + } + } + } + None + } + + fn push_completion( + &mut self, + new_completion: copilot::Completion, + ) -> Option<&copilot::Completion> { + for completion in &self.completions { + if *completion == new_completion { + return None; + } + } + self.completions.push(new_completion); + self.completions.last() } } @@ -2713,30 +2750,46 @@ impl Editor { return None; } - self.copilot_state.completions.clear(); + let snapshot = self.buffer.read(cx).snapshot(cx); + let selection = self.selections.newest_anchor(); + let cursor = if selection.start == selection.end { + selection.start.bias_left(&snapshot) + } else { + self.clear_copilot_suggestions(cx); + return None; + }; + + if let Some(new_text) = self + .copilot_state + .text_for_active_completion(cursor, &snapshot) + { + self.display_map.update(cx, |map, cx| { + map.replace_suggestion( + Some(Suggestion { + position: cursor, + text: new_text.into(), + }), + cx, + ) + }); + self.copilot_state + .completions + .swap(0, self.copilot_state.active_completion_index); + self.copilot_state.completions.truncate(1); + } else { + self.clear_copilot_suggestions(cx); + } + self.copilot_state.position = cursor; self.copilot_state.active_completion_index = 0; - self.copilot_state.position = Anchor::min(); - self.display_map - .update(cx, |map, cx| map.replace_suggestion::(None, cx)); cx.notify(); if !copilot.read(cx).status().is_authorized() { return None; } - let selection = self.selections.newest_anchor(); - let position = if selection.start == selection.end { - selection.start - } else { - return None; - }; - let (buffer, buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(position, cx)?; - self.copilot_state.position = position; + let (buffer, buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move { - cx.background().timer(COPILOT_TIMEOUT).await; let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| { ( copilot.completion(&buffer, buffer_position, cx), @@ -2744,47 +2797,31 @@ impl Editor { ) }); - if let Some(completion) = completion.await.log_err() { - let this = this.upgrade(&cx)?; - this.update(&mut cx, |this, cx| { - if let Some(completion) = completion { - this.display_map.update(cx, |map, cx| { - map.replace_suggestion( - Some(Suggestion { - position, - text: completion.text.as_str().into(), - }), - cx, - ) - }); - this.copilot_state.completions.push(completion); - cx.notify(); - } - }); - } - - if let Some(completions) = completions_cycling.await.log_err() { - let this = this.upgrade(&cx)?; - this.update(&mut cx, |this, cx| { + let (completion, completions_cycling) = futures::join!(completion, completions_cycling); + let mut completions = Vec::new(); + completions.extend(completion.log_err().flatten()); + completions.extend(completions_cycling.log_err().into_iter().flatten()); + this.upgrade(&cx)?.update(&mut cx, |this, cx| { + this.copilot_state.completions.clear(); + this.copilot_state.active_completion_index = 0; + for completion in completions { let was_empty = this.copilot_state.completions.is_empty(); - if !completions.is_empty() { + if let Some(completion) = this.copilot_state.push_completion(completion) { if was_empty { - let completion = completions.first().unwrap(); this.display_map.update(cx, |map, cx| { map.replace_suggestion( Some(Suggestion { - position, + position: cursor, text: completion.text.as_str().into(), }), cx, ) }); - cx.notify(); } - this.copilot_state.completions.extend(completions); } - }); - } + } + cx.notify(); + }); Some(()) }); @@ -2797,21 +2834,49 @@ impl Editor { return; } + let snapshot = self.buffer.read(cx).snapshot(cx); + self.copilot_state.active_completion_index = (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len(); - if let Some(completion) = self.copilot_state.active_completion() { + if let Some(text) = self + .copilot_state + .text_for_active_completion(self.copilot_state.position, &snapshot) + { self.display_map.update(cx, |map, cx| { map.replace_suggestion( Some(Suggestion { position: self.copilot_state.position, - text: completion.text.as_str().into(), + text: text.into(), }), cx, ) }); + cx.notify(); } + } - cx.notify(); + fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { + let snapshot = self.buffer.read(cx).snapshot(cx); + if let Some(text) = self + .copilot_state + .text_for_active_completion(self.copilot_state.position, &snapshot) + .map(|text| text.to_string()) + { + self.copilot_state = Default::default(); + self.insert(&text, cx); + true + } else { + false + } + } + + fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext) { + self.display_map + .update(cx, |map, cx| map.replace_suggestion::(None, cx)); + self.copilot_state.completions.clear(); + self.copilot_state.active_completion_index = 0; + self.copilot_state.pending_refresh = Task::ready(None); + self.copilot_state.position = Anchor::min(); } pub fn render_code_actions_indicator( @@ -3121,8 +3186,7 @@ impl Editor { } pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { - if let Some(completion) = self.copilot_state.active_completion() { - self.insert(&completion.text.to_string(), cx); + if self.accept_copilot_suggestion(cx) { return; } From 2fede1c01fc4fca93ad8b9d9c6eae508842fe88c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 27 Mar 2023 10:35:34 -0400 Subject: [PATCH 20/53] Use `syntax.predictive.color` to style suggestions --- styles/src/styleTree/editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index e0c73109fc..304ba33b03 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -44,7 +44,7 @@ export default function editor(colorScheme: ColorScheme) { activeLineBackground: withOpacity(background(layer, "on"), 0.75), highlightedLineBackground: background(layer, "on"), suggestion: { - color: foreground(layer, "disabled") + color: syntax.predictive.color, }, codeActions: { indicator: { From ef6c28326d0a9a38efdd07a83a34c6f9f0b79970 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 27 Mar 2023 10:42:19 -0400 Subject: [PATCH 21/53] Update editor.ts --- styles/src/styleTree/editor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 304ba33b03..a1b791d00d 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -43,6 +43,7 @@ export default function editor(colorScheme: ColorScheme) { background: background(layer), activeLineBackground: withOpacity(background(layer, "on"), 0.75), highlightedLineBackground: background(layer, "on"), + // Inline autocomplete suggestions, Co-pilot suggestions, etc. suggestion: { color: syntax.predictive.color, }, From d236d9e8c991110cecc29ceea1692343f48e6acf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 27 Mar 2023 17:14:18 +0200 Subject: [PATCH 22/53] Clear copilot suggestions when hitting escape Co-Authored-By: Mikayla Maki --- crates/editor/src/editor.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cea0d0e8ec..cfe803adfc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -97,7 +97,6 @@ const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -pub const COPILOT_TIMEOUT: Duration = Duration::from_secs(1); #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { @@ -1825,6 +1824,10 @@ impl Editor { return; } + if self.clear_copilot_suggestions(cx) { + return; + } + if self.snippet_stack.pop().is_some() { return; } @@ -2870,13 +2873,16 @@ impl Editor { } } - fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext) { + fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext) -> bool { self.display_map .update(cx, |map, cx| map.replace_suggestion::(None, cx)); + let was_empty = self.copilot_state.completions.is_empty(); self.copilot_state.completions.clear(); self.copilot_state.active_completion_index = 0; self.copilot_state.pending_refresh = Task::ready(None); self.copilot_state.position = Anchor::min(); + cx.notify(); + !was_empty } pub fn render_code_actions_indicator( From 034bc75467f8c340f8901ae391509fd9362dedbe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 27 Mar 2023 17:16:10 +0200 Subject: [PATCH 23/53] Refresh copilot suggestions when hitting `alt-]` if none are showing Co-Authored-By: Mikayla Maki --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cfe803adfc..fca4321422 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2834,6 +2834,7 @@ impl Editor { fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { if self.copilot_state.completions.is_empty() { + self.refresh_copilot_suggestions(cx); return; } From da81ff32957291cb350b1f1936b32919db6cda2e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 27 Mar 2023 18:36:56 +0200 Subject: [PATCH 24/53] Optimize `CopilotState::text_for_active_completion` --- crates/editor/src/editor.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fca4321422..6b47034674 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1030,21 +1030,22 @@ impl CopilotState { cursor: Anchor, buffer: &MultiBufferSnapshot, ) -> Option<&str> { + let cursor_offset = cursor.to_offset(buffer); let completion = self.completions.get(self.active_completion_index)?; if self.position.excerpt_id == cursor.excerpt_id && self.position.buffer_id == cursor.buffer_id - && buffer.chars_at(cursor).next().map_or(true, |ch| ch == '\n') + && (cursor_offset == buffer.len() || buffer.contains_str_at(cursor_offset, "\n")) { - let completion_position = Anchor { + let completion_offset = buffer.summary_for_anchor(&Anchor { excerpt_id: self.position.excerpt_id, buffer_id: self.position.buffer_id, text_anchor: completion.position, - }; - if completion_position.cmp(&cursor, buffer).is_le() { - let prefix = buffer - .text_for_range(completion_position..cursor) - .collect::(); - let suffix = completion.text.strip_prefix(&prefix)?; + }); + let common_prefix_len = cursor_offset.saturating_sub(completion_offset); + if common_prefix_len <= completion.text.len() + && buffer.contains_str_at(completion_offset, &completion.text[..common_prefix_len]) + { + let suffix = &completion.text[common_prefix_len..]; if !suffix.is_empty() { return Some(suffix); } From 6ff09865eb1dbf3fd07ce307fc71489eb06d2bc9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 27 Mar 2023 14:25:11 -0700 Subject: [PATCH 25/53] Create copilot auth popup UI --- Cargo.lock | 1 + assets/icons/github-copilot-dummy.svg | 1 + crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 2 +- crates/copilot/src/sign_in.rs | 90 +++++++++++++++++++---- crates/theme/src/theme.rs | 23 ++++-- crates/theme/src/ui.rs | 82 +++++++++++++++++++-- crates/welcome/src/welcome.rs | 101 +++----------------------- styles/src/styleTree/components.ts | 12 +++ styles/src/styleTree/copilot.ts | 60 ++++++++++++--- styles/src/styleTree/welcome.ts | 19 +---- styles/src/styleTree/workspace.ts | 31 +++----- 12 files changed, 253 insertions(+), 170 deletions(-) create mode 100644 assets/icons/github-copilot-dummy.svg diff --git a/Cargo.lock b/Cargo.lock index 5c43455d54..65d68aa3a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,6 +1347,7 @@ dependencies = [ "serde_derive", "settings", "smol", + "theme", "util", "workspace", ] diff --git a/assets/icons/github-copilot-dummy.svg b/assets/icons/github-copilot-dummy.svg new file mode 100644 index 0000000000..4a7ded3976 --- /dev/null +++ b/assets/icons/github-copilot-dummy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index c17e7cac59..a7582a6ffc 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -12,6 +12,7 @@ doctest = false gpui = { path = "../gpui" } language = { path = "../language" } settings = { path = "../settings" } +theme = { path = "../theme" } lsp = { path = "../lsp" } util = { path = "../util" } client = { path = "../client" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index aa36991fac..2763eea0fd 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -475,7 +475,7 @@ mod tests { .update(cx, |copilot, cx| copilot.sign_in(cx)) .await .unwrap(); - dbg!(copilot.read_with(cx, |copilot, _| copilot.status())); + copilot.read_with(cx, |copilot, _| copilot.status()); let buffer = cx.add_model(|cx| language::Buffer::new(0, "fn foo() -> ", cx)); dbg!(copilot diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 67b93385ac..cdec0b8963 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,11 +1,18 @@ use crate::{request::PromptUserDeviceFlow, Copilot}; use gpui::{ - elements::*, - geometry::{rect::RectF, vector::vec2f}, - Axis, Element, Entity, MutableAppContext, View, WindowKind, WindowOptions, + elements::*, geometry::rect::RectF, impl_internal_actions, ClipboardItem, Element, Entity, + MutableAppContext, View, WindowKind, WindowOptions, }; use settings::Settings; +#[derive(PartialEq, Eq, Debug, Clone)] +struct CopyUserCode; + +#[derive(PartialEq, Eq, Debug, Clone)] +struct OpenGithub; + +impl_internal_actions!(copilot_sign_in, [CopyUserCode, OpenGithub]); + pub fn init(cx: &mut MutableAppContext) { let copilot = Copilot::global(cx).unwrap(); @@ -19,16 +26,24 @@ pub fn init(cx: &mut MutableAppContext) { cx.remove_window(window_id); } + let window_size = cx + .global::() + .theme + .copilot + .auth + .popup_dimensions + .to_vec(); + let (window_id, _) = cx.add_window( WindowOptions { bounds: gpui::WindowBounds::Fixed(RectF::new( Default::default(), - vec2f(600., 400.), + window_size, )), titlebar: None, center: true, focus: false, - kind: WindowKind::Normal, + kind: WindowKind::PopUp, is_movable: true, screen: None, }, @@ -62,23 +77,68 @@ impl View for CopilotCodeVerification { fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { let style = cx.global::().theme.copilot.clone(); - let auth_text = style.auth_text.clone(); - let prompt = self.prompt.clone(); - Flex::new(Axis::Vertical) - .with_child(Label::new(prompt.user_code.clone(), auth_text.clone()).boxed()) + let instruction_text = style.auth.instruction_text; + let user_code_text = style.auth.user_code; + let button = style.auth.button; + let button_width = style.auth.button_width; + let height = style.auth.popup_dimensions.height; + + let user_code = self.prompt.user_code.replace("-", " - "); + + Flex::column() .with_child( - MouseEventHandler::::new(1, cx, move |_state, _cx| { - Label::new("Click here to open GitHub!", auth_text.clone()).boxed() + MouseEventHandler::::new(0, cx, |state, _cx| { + let style = style.auth.close_icon.style_for(state, false); + theme::ui::icon(style).boxed() }) - .on_click(gpui::MouseButton::Left, move |_click, cx| { - cx.platform().open_url(&prompt.verification_uri) + .on_click(gpui::MouseButton::Left, move |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id); }) .with_cursor_style(gpui::CursorStyle::PointingHand) + .aligned() + .right() .boxed(), ) + .with_child( + Flex::column() + .align_children_center() + .with_children([ + theme::ui::svg(&style.auth.copilot_icon).boxed(), + Label::new( + "Here is your code to authenticate with github", + instruction_text.clone(), + ) + .boxed(), + Label::new(user_code, user_code_text.clone()).boxed(), + theme::ui::cta_button_with_click("Copy Code", button_width, &button, cx, { + let user_code = self.prompt.user_code.clone(); + move |_, cx| { + cx.platform() + .write_to_clipboard(ClipboardItem::new(user_code.clone())) + } + }), + Label::new("Copy it and enter it on GitHub", instruction_text.clone()) + .boxed(), + theme::ui::cta_button_with_click( + "Go to Github", + button_width, + &button, + cx, + { + let verification_uri = self.prompt.verification_uri.clone(); + move |_, cx| cx.platform().open_url(&verification_uri) + }, + ), + ]) + .aligned() + .boxed(), + ) .contained() - .with_style(style.auth_modal) - .named("Copilot Authentication status modal") + .with_style(style.auth.popup_container) + .constrained() + .with_height(height) + .boxed() } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ef6a73f5d7..ce4d8a04fb 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -9,7 +9,7 @@ use gpui::{ use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use std::{collections::HashMap, sync::Arc}; -use ui::{CheckboxStyle, IconStyle}; +use ui::{ButtonStyle, CheckboxStyle, Dimensions, IconStyle, SvgStyle}; pub mod ui; @@ -76,8 +76,8 @@ pub struct Workspace { #[derive(Clone, Deserialize, Default)] pub struct BlankPaneStyle { - pub logo: IconStyle, - pub logo_shadow: IconStyle, + pub logo: SvgStyle, + pub logo_shadow: SvgStyle, pub logo_container: ContainerStyle, pub keyboard_hints: ContainerStyle, pub keyboard_hint: Interactive, @@ -118,8 +118,19 @@ pub struct AvatarStyle { #[derive(Deserialize, Default, Clone)] pub struct Copilot { - pub auth_modal: ContainerStyle, - pub auth_text: TextStyle, + pub auth: CopilotAuth, +} + +#[derive(Deserialize, Default, Clone)] +pub struct CopilotAuth { + pub popup_container: ContainerStyle, + pub popup_dimensions: Dimensions, + pub instruction_text: TextStyle, + pub user_code: TextStyle, + pub button: ButtonStyle, + pub button_width: f32, + pub copilot_icon: SvgStyle, + pub close_icon: Interactive, } #[derive(Deserialize, Default)] @@ -876,7 +887,7 @@ pub struct FeedbackStyle { #[derive(Clone, Deserialize, Default)] pub struct WelcomeStyle { pub page_width: f32, - pub logo: IconStyle, + pub logo: SvgStyle, pub logo_subheading: ContainedText, pub usage_note: ContainedText, pub checkbox: CheckboxStyle, diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 5441e71168..392b1134a6 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -1,18 +1,22 @@ +use std::borrow::Cow; + use gpui::{ color::Color, elements::{ ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, MouseEventHandler, ParentElement, Svg, }, - Action, Element, ElementBox, EventContext, RenderContext, View, + geometry::vector::{vec2f, Vector2F}, + scene::MouseClick, + Action, Element, ElementBox, EventContext, MouseButton, MouseState, RenderContext, View, }; use serde::Deserialize; -use crate::ContainedText; +use crate::{ContainedText, Interactive}; #[derive(Clone, Deserialize, Default)] pub struct CheckboxStyle { - pub icon: IconStyle, + pub icon: SvgStyle, pub label: ContainedText, pub default: ContainerStyle, pub checked: ContainerStyle, @@ -44,7 +48,7 @@ pub fn checkbox_with_label( ) -> MouseEventHandler { MouseEventHandler::::new(0, cx, |state, _| { let indicator = if checked { - icon(&style.icon) + svg(&style.icon) } else { Empty::new() .constrained() @@ -80,9 +84,9 @@ pub fn checkbox_with_label( } #[derive(Clone, Deserialize, Default)] -pub struct IconStyle { +pub struct SvgStyle { pub color: Color, - pub icon: String, + pub asset: String, pub dimensions: Dimensions, } @@ -92,14 +96,30 @@ pub struct Dimensions { pub height: f32, } -pub fn icon(style: &IconStyle) -> ConstrainedBox { - Svg::new(style.icon.clone()) +impl Dimensions { + pub fn to_vec(&self) -> Vector2F { + vec2f(self.width, self.height) + } +} + +pub fn svg(style: &SvgStyle) -> ConstrainedBox { + Svg::new(style.asset.clone()) .with_color(style.color) .constrained() .with_width(style.dimensions.width) .with_height(style.dimensions.height) } +#[derive(Clone, Deserialize, Default)] +pub struct IconStyle { + icon: SvgStyle, + container: ContainerStyle, +} + +pub fn icon(style: &IconStyle) -> Container { + svg(&style.icon).contained().with_style(style.container) +} + pub fn keystroke_label( label_text: &'static str, label_style: &ContainedText, @@ -147,3 +167,49 @@ pub fn keystroke_label_for( .contained() .with_style(label_style.container) } + +pub type ButtonStyle = Interactive; + +pub fn cta_button( + label: L, + action: A, + max_width: f32, + style: &ButtonStyle, + cx: &mut RenderContext, +) -> ElementBox +where + L: Into>, + A: 'static + Action + Clone, + V: View, +{ + cta_button_with_click(label, max_width, style, cx, move |_, cx| { + cx.dispatch_action(action.clone()) + }) +} + +pub fn cta_button_with_click( + label: L, + max_width: f32, + style: &ButtonStyle, + cx: &mut RenderContext, + f: F, +) -> ElementBox +where + L: Into>, + V: View, + F: Fn(MouseClick, &mut EventContext) + 'static, +{ + MouseEventHandler::::new(0, cx, |state, _| { + let style = style.style_for(state, false); + Label::new(label, style.text.to_owned()) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_max_width(max_width) + .boxed() + }) + .on_click(MouseButton::Left, f) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed() +} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 3a35920b88..fb55c79a51 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,12 +1,11 @@ mod base_keymap_picker; -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; use db::kvp::KEY_VALUE_STORE; use gpui::{ - elements::{Flex, Label, MouseEventHandler, ParentElement}, - Action, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, - Subscription, View, ViewContext, + elements::{Flex, Label, ParentElement}, + Element, ElementBox, Entity, MutableAppContext, Subscription, View, ViewContext, }; use settings::{settings_file::SettingsFile, Settings}; @@ -77,7 +76,7 @@ impl View for WelcomePage { .with_children([ Flex::column() .with_children([ - theme::ui::icon(&theme.welcome.logo) + theme::ui::svg(&theme.welcome.logo) .aligned() .contained() .aligned() @@ -98,22 +97,25 @@ impl View for WelcomePage { .boxed(), Flex::column() .with_children([ - self.render_cta_button( + theme::ui::cta_button( "Choose a theme", theme_selector::Toggle, width, + &theme.welcome.button, cx, ), - self.render_cta_button( + theme::ui::cta_button( "Choose a keymap", ToggleBaseKeymapSelector, width, + &theme.welcome.button, cx, ), - self.render_cta_button( + theme::ui::cta_button( "Install the CLI", install_cli::Install, width, + &theme.welcome.button, cx, ), ]) @@ -201,89 +203,6 @@ impl WelcomePage { _settings_subscription: settings_subscription, } } - - fn render_cta_button( - &self, - label: L, - action: A, - width: f32, - cx: &mut RenderContext, - ) -> ElementBox - where - L: Into>, - A: 'static + Action + Clone, - { - let theme = cx.global::().theme.clone(); - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.welcome.button.style_for(state, false); - Label::new(label, style.text.clone()) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_max_width(width) - .boxed() - }) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(action.clone()) - }) - .with_cursor_style(gpui::CursorStyle::PointingHand) - .boxed() - } - - // fn render_settings_checkbox( - // &self, - // label: &'static str, - // style: &CheckboxStyle, - // checked: bool, - // cx: &mut RenderContext, - // set_value: fn(&mut SettingsFileContent, checked: bool) -> (), - // ) -> ElementBox { - // MouseEventHandler::::new(0, cx, |state, _| { - // let indicator = if checked { - // Svg::new(style.check_icon.clone()) - // .with_color(style.check_icon_color) - // .constrained() - // } else { - // Empty::new().constrained() - // }; - - // Flex::row() - // .with_children([ - // indicator - // .with_width(style.width) - // .with_height(style.height) - // .contained() - // .with_style(if checked { - // if state.hovered() { - // style.hovered_and_checked - // } else { - // style.checked - // } - // } else { - // if state.hovered() { - // style.hovered - // } else { - // style.default - // } - // }) - // .boxed(), - // Label::new(label, style.label.text.clone()) - // .contained() - // .with_style(style.label.container) - // .boxed(), - // ]) - // .align_children_center() - // .boxed() - // }) - // .on_click(gpui::MouseButton::Left, move |_, cx| { - // SettingsFile::update(cx, move |content| set_value(content, !checked)) - // }) - // .with_cursor_style(gpui::CursorStyle::PointingHand) - // .contained() - // .with_style(style.container) - // .boxed() - // } } impl Item for WelcomePage { diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index 33546c9978..6b21eec405 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/styleTree/components.ts @@ -280,3 +280,15 @@ export function border( ...properties, } } + + +export function svg(color: string, asset: String, width: Number, height: Number) { + return { + color, + asset, + dimensions: { + width, + height, + } + } +} diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index 66f5c63b4e..4772a2f673 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -1,21 +1,59 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { background, border, text } from "./components"; +import { background, border, foreground, svg, text } from "./components"; export default function copilot(colorScheme: ColorScheme) { let layer = colorScheme.highest; - return { - authModal: { - background: background(colorScheme.lowest), - border: border(colorScheme.lowest), - shadow: colorScheme.modalShadow, - cornerRadius: 12, - padding: { - bottom: 4, + auth: { + popupContainer: { + background: background(colorScheme.highest), }, - }, - authText: text(layer, "sans") + popupDimensions: { + width: 336, + height: 256, + }, + instructionText: text(layer, "sans"), + userCode: + text(layer, "sans", { size: "lg" }), + button: { // Copied from welcome screen. FIXME: Move this into a ZDS component + background: background(layer), + border: border(layer, "active"), + cornerRadius: 4, + margin: { + top: 4, + bottom: 4, + }, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + ...text(layer, "sans", "default", { size: "sm" }), + hover: { + ...text(layer, "sans", "default", { size: "sm" }), + background: background(layer, "hovered"), + border: border(layer, "active"), + }, + }, + buttonWidth: 320, + copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 64, 64), + closeIcon: { + icon: svg(background(layer, "on"), "icons/x_mark_16.svg", 16, 16), + container: { + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + } + }, + hover: { + icon: svg(foreground(layer, "on"), "icons/x_mark_16.svg", 16, 16), + } + }, + } } } diff --git a/styles/src/styleTree/welcome.ts b/styles/src/styleTree/welcome.ts index 252489ef1b..23e29c4a40 100644 --- a/styles/src/styleTree/welcome.ts +++ b/styles/src/styleTree/welcome.ts @@ -6,6 +6,7 @@ import { foreground, text, TextProperties, + svg, } from "./components" export default function welcome(colorScheme: ColorScheme) { @@ -32,14 +33,7 @@ export default function welcome(colorScheme: ColorScheme) { return { pageWidth: 320, - logo: { - color: foreground(layer, "default"), - icon: "icons/logo_96.svg", - dimensions: { - width: 64, - height: 64, - }, - }, + logo: svg(foreground(layer, "default"), "icons/logo_96.svg", 64, 64), logoSubheading: { ...text(layer, "sans", "variant", { size: "md" }), margin: { @@ -109,14 +103,7 @@ export default function welcome(colorScheme: ColorScheme) { ...text(layer, "sans", interactive_text_size), // Also supports margin, container, border, etc. }, - icon: { - color: foreground(layer, "on"), - icon: "icons/check_12.svg", - dimensions: { - width: 12, - height: 12, - }, - }, + icon: svg(foreground(layer, "on"), "icons/check_12.svg", 12, 12), default: { ...checkboxBase, background: background(layer, "default"), diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 1de2fe9502..11f6561bd3 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -1,6 +1,6 @@ import { ColorScheme } from "../themes/common/colorScheme" import { withOpacity } from "../utils/color" -import { background, border, borderColor, foreground, text } from "./components" +import { background, border, borderColor, foreground, svg, text } from "./components" import statusBar from "./statusBar" import tabBar from "./tabBar" @@ -46,27 +46,14 @@ export default function workspace(colorScheme: ColorScheme) { width: 256, height: 256, }, - logo: { - color: withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), - icon: "icons/logo_96.svg", - dimensions: { - width: 256, - height: 256, - }, - }, - logoShadow: { - color: withOpacity( - colorScheme.isLight - ? "#FFFFFF" - : colorScheme.lowest.base.default.background, - colorScheme.isLight ? 1 : 0.6 - ), - icon: "icons/logo_96.svg", - dimensions: { - width: 256, - height: 256, - }, - }, + logo: svg(withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), "icons/logo_96.svg", 256, 256), + + logoShadow: svg(withOpacity( + colorScheme.isLight + ? "#FFFFFF" + : colorScheme.lowest.base.default.background, + colorScheme.isLight ? 1 : 0.6 + ), "icons/logo_96.svg", 256, 256), keyboardHints: { margin: { top: 96, From 0ef9cefe0fd15f9093851fb36a9805dd93d21b73 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 27 Mar 2023 23:16:30 -0700 Subject: [PATCH 26/53] Finish shape of copilot auth UI --- crates/copilot/src/copilot.rs | 8 +- crates/copilot/src/sign_in.rs | 225 ++++++++++++++++++++++---------- crates/theme/src/theme.rs | 24 +++- crates/theme/src/ui.rs | 62 ++++++++- styles/src/styleTree/copilot.ts | 144 +++++++++++++++----- 5 files changed, 353 insertions(+), 110 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 2763eea0fd..5ad32ed3a5 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -51,10 +51,10 @@ enum CopilotServer { #[derive(Clone, Debug)] enum SignInStatus { Authorized { - user: String, + _user: String, }, Unauthorized { - user: String, + _user: String, }, SigningIn { prompt: Option, @@ -321,10 +321,10 @@ impl Copilot { if let CopilotServer::Started { status, .. } = &mut self.server { *status = match lsp_status { request::SignInStatus::Ok { user } | request::SignInStatus::MaybeOk { user } => { - SignInStatus::Authorized { user } + SignInStatus::Authorized { _user: user } } request::SignInStatus::NotAuthorized { user } => { - SignInStatus::Unauthorized { user } + SignInStatus::Unauthorized { _user: user } } _ => SignInStatus::SignedOut, }; diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index cdec0b8963..80411f18da 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -26,13 +26,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.remove_window(window_id); } - let window_size = cx - .global::() - .theme - .copilot - .auth - .popup_dimensions - .to_vec(); + let window_size = cx.global::().theme.copilot.modal.dimensions(); let (window_id, _) = cx.add_window( WindowOptions { @@ -43,13 +37,15 @@ pub fn init(cx: &mut MutableAppContext) { titlebar: None, center: true, focus: false, - kind: WindowKind::PopUp, + kind: WindowKind::Normal, is_movable: true, screen: None, }, |_| CopilotCodeVerification::new(prompt), ); code_verification_window_id = Some(window_id); + + cx.activate_window(window_id); } _ => { if let Some(window_id) = code_verification_window_id.take() { @@ -59,6 +55,26 @@ pub fn init(cx: &mut MutableAppContext) { } }) .detach(); + + // let window_size = cx.global::().theme.copilot.modal.dimensions(); + + // let (_window_id, _) = cx.add_window( + // WindowOptions { + // bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), + // titlebar: None, + // center: true, + // focus: false, + // kind: WindowKind::PopUp, + // is_movable: true, + // screen: None, + // }, + // |_| { + // CopilotCodeVerification::new(PromptUserDeviceFlow { + // user_code: "ABCD-1234".to_string(), + // verification_uri: "https://github.com/login/device".to_string(), + // }) + // }, + // ); } pub struct CopilotCodeVerification { @@ -74,71 +90,146 @@ impl View for CopilotCodeVerification { "CopilotCodeVerification" } + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext) { + cx.notify() + } + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { let style = cx.global::().theme.copilot.clone(); - let instruction_text = style.auth.instruction_text; - let user_code_text = style.auth.user_code; - let button = style.auth.button; - let button_width = style.auth.button_width; - let height = style.auth.popup_dimensions.height; + let copied = cx + .read_from_clipboard() + .map(|item| item.text() == &self.prompt.user_code) + .unwrap_or(false); - let user_code = self.prompt.user_code.replace("-", " - "); - - Flex::column() - .with_child( - MouseEventHandler::::new(0, cx, |state, _cx| { - let style = style.auth.close_icon.style_for(state, false); - theme::ui::icon(style).boxed() - }) - .on_click(gpui::MouseButton::Left, move |_, cx| { - let window_id = cx.window_id(); - cx.remove_window(window_id); - }) - .with_cursor_style(gpui::CursorStyle::PointingHand) - .aligned() - .right() - .boxed(), - ) - .with_child( - Flex::column() - .align_children_center() - .with_children([ - theme::ui::svg(&style.auth.copilot_icon).boxed(), - Label::new( - "Here is your code to authenticate with github", - instruction_text.clone(), - ) + theme::ui::modal("Authenticate Copilot", &style.modal, cx, |cx| { + Flex::column() + .align_children_center() + .with_children([ + Flex::column() + .with_children([ + Flex::row() + .with_children([ + theme::ui::svg(&style.auth.copilot_icon).boxed(), + theme::ui::svg(&style.auth.plus_icon).boxed(), + theme::ui::svg(&style.auth.zed_icon).boxed(), + ]) + .boxed(), + Label::new("Copilot for Zed", style.auth.header_text.clone()).boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.header_group) + .aligned() .boxed(), - Label::new(user_code, user_code_text.clone()).boxed(), - theme::ui::cta_button_with_click("Copy Code", button_width, &button, cx, { - let user_code = self.prompt.user_code.clone(); - move |_, cx| { - cx.platform() - .write_to_clipboard(ClipboardItem::new(user_code.clone())) - } - }), - Label::new("Copy it and enter it on GitHub", instruction_text.clone()) + Flex::column() + .with_children([ + Label::new( + "Here is your code to authenticate with github", + style.auth.instruction_text.clone(), + ) .boxed(), - theme::ui::cta_button_with_click( - "Go to Github", - button_width, - &button, - cx, - { - let verification_uri = self.prompt.verification_uri.clone(); - move |_, cx| cx.platform().open_url(&verification_uri) - }, - ), - ]) - .aligned() - .boxed(), - ) - .contained() - .with_style(style.auth.popup_container) - .constrained() - .with_height(height) - .boxed() + MouseEventHandler::::new(0, cx, |state, _cx| { + Flex::row() + .with_children([ + Label::new( + self.prompt.user_code.clone(), + style.auth.device_code.clone(), + ) + .aligned() + .contained() + .with_style(style.auth.device_code_left_container) + .constrained() + .with_width(style.auth.device_code_left) + .boxed(), + Empty::new() + .constrained() + .with_width(1.) + .with_height(style.auth.device_code_seperator_height) + .contained() + .with_background_color( + style + .auth + .cta_button + .style_for(state, false) + .container + .border + .color, + ) + .boxed(), + Label::new( + if copied { "Copied!" } else { "Copy" }, + style + .auth + .cta_button + .style_for(state, false) + .text + .clone(), + ) + .aligned() + .contained() + .with_style(style.auth.device_code_right_container) + .constrained() + .with_width(style.auth.device_code_right) + .boxed(), + ]) + .contained() + .with_style( + style + .auth + .device_code_cta + .style_for(state, false) + .container, + ) + .constrained() + .with_width(style.auth.content_width) + .boxed() + }) + .on_click(gpui::MouseButton::Left, { + let user_code = self.prompt.user_code.clone(); + move |_, cx| { + cx.platform() + .write_to_clipboard(ClipboardItem::new(user_code.clone())); + cx.notify(); + } + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.device_code_group) + .aligned() + .boxed(), + Flex::column() + .with_children([ + Label::new( + "Copy it and enter it on GitHub", + style.auth.instruction_text.clone(), + ) + .boxed(), + theme::ui::cta_button_with_click( + "Go to Github", + style.auth.content_width, + &style.auth.cta_button, + cx, + { + let verification_uri = self.prompt.verification_uri.clone(); + move |_, cx| cx.platform().open_url(&verification_uri) + }, + ), + ]) + .align_children_center() + .contained() + .with_style(style.auth.github_group) + .aligned() + .boxed(), + ]) + .constrained() + .with_width(style.auth.content_width) + .aligned() + .boxed() + }) } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ce4d8a04fb..ae072eca32 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -9,7 +9,7 @@ use gpui::{ use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use std::{collections::HashMap, sync::Arc}; -use ui::{ButtonStyle, CheckboxStyle, Dimensions, IconStyle, SvgStyle}; +use ui::{ButtonStyle, CheckboxStyle, ModalStyle, SvgStyle}; pub mod ui; @@ -118,19 +118,29 @@ pub struct AvatarStyle { #[derive(Deserialize, Default, Clone)] pub struct Copilot { + pub modal: ModalStyle, pub auth: CopilotAuth, } #[derive(Deserialize, Default, Clone)] pub struct CopilotAuth { - pub popup_container: ContainerStyle, - pub popup_dimensions: Dimensions, pub instruction_text: TextStyle, - pub user_code: TextStyle, - pub button: ButtonStyle, - pub button_width: f32, + pub cta_button: ButtonStyle, + pub content_width: f32, pub copilot_icon: SvgStyle, - pub close_icon: Interactive, + pub plus_icon: SvgStyle, + pub zed_icon: SvgStyle, + pub header_text: TextStyle, + pub device_code_group: ContainerStyle, + pub github_group: ContainerStyle, + pub header_group: ContainerStyle, + pub device_code: TextStyle, + pub device_code_cta: ButtonStyle, + pub device_code_left: f32, + pub device_code_left_container: ContainerStyle, + pub device_code_right: f32, + pub device_code_right_container: ContainerStyle, + pub device_code_seperator_height: f32, } #[derive(Deserialize, Default)] diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 392b1134a6..50239bdea5 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -4,11 +4,12 @@ use gpui::{ color::Color, elements::{ ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, - MouseEventHandler, ParentElement, Svg, + MouseEventHandler, ParentElement, Stack, Svg, }, + fonts::TextStyle, geometry::vector::{vec2f, Vector2F}, scene::MouseClick, - Action, Element, ElementBox, EventContext, MouseButton, MouseState, RenderContext, View, + Action, Element, ElementBox, EventContext, MouseButton, RenderContext, View, }; use serde::Deserialize; @@ -213,3 +214,60 @@ where .with_cursor_style(gpui::CursorStyle::PointingHand) .boxed() } + +#[derive(Clone, Deserialize, Default)] +pub struct ModalStyle { + close_icon: Interactive, + container: ContainerStyle, + titlebar: ContainerStyle, + title_text: TextStyle, + dimensions: Dimensions, +} + +impl ModalStyle { + pub fn dimensions(&self) -> Vector2F { + self.dimensions.to_vec() + } +} + +pub fn modal( + title: I, + style: &ModalStyle, + cx: &mut RenderContext, + build_modal: F, +) -> ElementBox +where + V: View, + I: Into>, + F: FnOnce(&mut gpui::RenderContext) -> ElementBox, +{ + Flex::column() + .with_child( + Stack::new() + .with_children([ + Label::new(title, style.title_text.clone()).boxed(), + // FIXME: Get a better tag type + MouseEventHandler::::new(999999, cx, |state, _cx| { + let style = style.close_icon.style_for(state, false); + icon(style).boxed() + }) + .on_click(gpui::MouseButton::Left, move |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id); + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .aligned() + .right() + .boxed(), + ]) + .contained() + .with_style(style.titlebar) + .boxed(), + ) + .with_child(build_modal(cx)) + .contained() + .with_style(style.container) + .constrained() + .with_height(style.dimensions().y()) + .boxed() +} diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index 4772a2f673..75fc99b591 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -5,41 +5,52 @@ import { background, border, foreground, svg, text } from "./components"; export default function copilot(colorScheme: ColorScheme) { let layer = colorScheme.highest; + let content_width = 304; + + let ctaButton = { // Copied from welcome screen. FIXME: Move this into a ZDS component + background: background(layer), + border: border(layer, "active"), + cornerRadius: 4, + margin: { + top: 4, + bottom: 4, + }, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + ...text(layer, "sans", "default", { size: "sm" }), + hover: { + ...text(layer, "sans", "default", { size: "sm" }), + background: background(layer, "hovered"), + border: border(layer, "active"), + }, + }; + return { - auth: { - popupContainer: { - background: background(colorScheme.highest), - }, - popupDimensions: { - width: 336, - height: 256, - }, - instructionText: text(layer, "sans"), - userCode: - text(layer, "sans", { size: "lg" }), - button: { // Copied from welcome screen. FIXME: Move this into a ZDS component - background: background(layer), + modal: { + titleText: text(layer, "sans", { size: "md" }), + titlebar: { border: border(layer, "active"), - cornerRadius: 4, - margin: { + padding: { top: 4, bottom: 4, + left: 8, + right: 8, }, - padding: { - top: 3, - bottom: 3, - left: 7, - right: 7, - }, - ...text(layer, "sans", "default", { size: "sm" }), - hover: { - ...text(layer, "sans", "default", { size: "sm" }), - background: background(layer, "hovered"), - border: border(layer, "active"), - }, + margin: { + top: 0, + left: 0, + right: 0, + bottom: 8 + } + }, + container: { + background: background(colorScheme.highest), + }, - buttonWidth: 320, - copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 64, 64), closeIcon: { icon: svg(background(layer, "on"), "icons/x_mark_16.svg", 16, 16), container: { @@ -47,13 +58,86 @@ export default function copilot(colorScheme: ColorScheme) { top: 3, bottom: 3, left: 7, - right: 7, + right: 0, } }, hover: { icon: svg(foreground(layer, "on"), "icons/x_mark_16.svg", 16, 16), } }, + dimensions: { + width: 400, + height: 500, + }, + }, + auth: { + content_width, + + headerGroup: { + margin: { + top: 5, + bottom: 5, + left: 0, + right: 0 + } + }, + headerText: text(layer, "sans", { size: "lg" }), + copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 36, 36), + plusIcon: svg(foreground(layer, "default"), "icons/plus_16.svg", 36, 36), + zedIcon: svg(foreground(layer, "default"), "icons/logo_96.svg", 36, 36), + + instructionText: text(layer, "sans"), + + deviceCodeGroup: { + margin: { + top: 5, + bottom: 5, + left: 0, + right: 0 + } + }, + deviceCode: + text(layer, "mono", { size: "md" }), + deviceCodeCta: { + ...ctaButton, + padding: { + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + }, + deviceCodeLeft: content_width * 2 / 3, + deviceCodeLeftContainer: { + padding: { + top: 3, + bottom: 3, + left: 0, + right: 0, + }, + }, + deviceCodeRight: content_width * 1 / 3, + deviceCodeRightContainer: { + border: border(layer, "active", { bottom: false, right: false, top: false, left: true }), + padding: { + top: 3, + bottom: 5, + left: 0, + right: 0, + }, + }, + deviceCodeSeperatorHeight: 0, + + githubGroup: { + margin: { + top: 3, + bottom: 3, + left: 0, + right: 0 + } + }, + + ctaButton } } } From 941da24f7375170605d6662a69251db8b86b90c8 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Mar 2023 10:27:31 -0700 Subject: [PATCH 27/53] Refactor out the node runtime crate and hook up all related imports --- Cargo.lock | 19 ++++++++ Cargo.toml | 1 + crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 14 ++++-- crates/node_runtime/Cargo.toml | 22 +++++++++ .../src}/node_runtime.rs | 0 crates/util/src/github.rs | 5 +++ crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 9 +--- crates/zed/src/languages/c.rs | 2 +- crates/zed/src/languages/elixir.rs | 2 +- crates/zed/src/languages/github.rs | 45 ------------------- crates/zed/src/languages/html.rs | 2 +- crates/zed/src/languages/json.rs | 2 +- crates/zed/src/languages/lua.rs | 2 +- crates/zed/src/languages/python.rs | 2 +- crates/zed/src/languages/rust.rs | 2 +- crates/zed/src/languages/typescript.rs | 2 +- crates/zed/src/languages/yaml.rs | 2 +- crates/zed/src/main.rs | 12 +++-- crates/zed/src/zed.rs | 10 ++--- 21 files changed, 78 insertions(+), 79 deletions(-) create mode 100644 crates/node_runtime/Cargo.toml rename crates/{zed/src/languages => node_runtime/src}/node_runtime.rs (100%) delete mode 100644 crates/zed/src/languages/github.rs diff --git a/Cargo.lock b/Cargo.lock index 65d68aa3a8..7654a1ac47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1343,6 +1343,7 @@ dependencies = [ "language", "log", "lsp", + "node_runtime", "serde", "serde_derive", "settings", @@ -3931,6 +3932,23 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "node_runtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-compression", + "async-tar", + "futures 0.3.25", + "gpui", + "parking_lot 0.11.2", + "serde", + "serde_derive", + "serde_json", + "smol", + "util", +] + [[package]] name = "nom" version = "7.1.1" @@ -8512,6 +8530,7 @@ dependencies = [ "libc", "log", "lsp", + "node_runtime", "num_cpus", "outline", "parking_lot 0.11.2", diff --git a/Cargo.toml b/Cargo.toml index bf9214f49e..f097b5b2c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ members = [ "crates/lsp", "crates/media", "crates/menu", + "crates/node_runtime", "crates/outline", "crates/picker", "crates/plugin", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index a7582a6ffc..74dd73df0b 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -14,6 +14,7 @@ language = { path = "../language" } settings = { path = "../settings" } theme = { path = "../theme" } lsp = { path = "../lsp" } +node_runtime = { path = "../node_runtime"} util = { path = "../util" } client = { path = "../client" } workspace = { path = "../workspace" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 5ad32ed3a5..d12e5995d0 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -8,6 +8,7 @@ use futures::{future::Shared, FutureExt, TryFutureExt}; use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, ToPointUtf16}; use lsp::LanguageServer; +use node_runtime::NodeRuntime; use settings::Settings; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ @@ -21,8 +22,8 @@ use util::{ actions!(copilot, [SignIn, SignOut, NextSuggestion]); -pub fn init(client: Arc, cx: &mut MutableAppContext) { - let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); +pub fn init(client: Arc, node_runtime: Arc, cx: &mut MutableAppContext) { + let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx)); cx.set_global(copilot.clone()); cx.add_global_action(|_: &SignIn, cx| { let copilot = Copilot::global(cx).unwrap(); @@ -104,7 +105,11 @@ impl Copilot { } } - fn start(http: Arc, cx: &mut ModelContext) -> Self { + fn start( + http: Arc, + node_runtime: Arc, + cx: &mut ModelContext, + ) -> Self { // TODO: Don't eagerly download the LSP cx.spawn(|this, mut cx| async move { let start_language_server = async { @@ -469,7 +474,8 @@ mod tests { async fn test_smoke(cx: &mut TestAppContext) { Settings::test_async(cx); let http = http::client(); - let copilot = cx.add_model(|cx| Copilot::start(http, cx)); + let node_runtime = NodeRuntime::new(http.clone(), cx.background()); + let copilot = cx.add_model(|cx| Copilot::start(http, node_runtime, cx)); smol::Timer::after(std::time::Duration::from_secs(2)).await; copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml new file mode 100644 index 0000000000..32ab6abbb3 --- /dev/null +++ b/crates/node_runtime/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "node_runtime" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/node_runtime.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } +util = { path = "../util" } +async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-tar = "0.4.2" +futures = "0.3" +anyhow = "1.0.38" +parking_lot = "0.11.1" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +smol = "1.2.5" diff --git a/crates/zed/src/languages/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs similarity index 100% rename from crates/zed/src/languages/node_runtime.rs rename to crates/node_runtime/src/node_runtime.rs diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index 02082108e1..5170bd6f4f 100644 --- a/crates/util/src/github.rs +++ b/crates/util/src/github.rs @@ -4,6 +4,11 @@ use futures::AsyncReadExt; use serde::Deserialize; use std::sync::Arc; +pub struct GitHubLspBinaryVersion { + pub name: String, + pub url: String, +} + #[derive(Deserialize)] pub struct GithubRelease { pub name: String, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a9cbfbc737..72470e216b 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -45,6 +45,7 @@ journal = { path = "../journal" } language = { path = "../language" } language_selector = { path = "../language_selector" } lsp = { path = "../lsp" } +node_runtime = { path = "../node_runtime" } outline = { path = "../outline" } plugin_runtime = { path = "../plugin_runtime" } project = { path = "../project" } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 3a23afb970..12e6c1b1f2 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,21 +1,17 @@ use anyhow::Context; -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; -mod github; mod go; mod html; mod json; mod language_plugin; mod lua; -mod node_runtime; mod python; mod ruby; mod rust; @@ -37,13 +33,10 @@ mod yaml; struct LanguageDir; pub fn init( - http: Arc, - background: Arc, languages: Arc, themes: Arc, + node_runtime: Arc, ) { - let node_runtime = NodeRuntime::new(http, background); - for (name, grammar, lsp_adapter) in [ ( "c", diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 88f5c4553b..e142028196 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -9,7 +9,7 @@ use util::github::latest_github_release; use util::http::HttpClient; use util::ResultExt; -use super::github::GitHubLspBinaryVersion; +use util::github::GitHubLspBinaryVersion; pub struct CLspAdapter; diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index ecd4028fe0..a2debcdb2d 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -10,7 +10,7 @@ use util::github::latest_github_release; use util::http::HttpClient; use util::ResultExt; -use super::github::GitHubLspBinaryVersion; +use util::github::GitHubLspBinaryVersion; pub struct ElixirLspAdapter; diff --git a/crates/zed/src/languages/github.rs b/crates/zed/src/languages/github.rs deleted file mode 100644 index 9e0dd9b582..0000000000 --- a/crates/zed/src/languages/github.rs +++ /dev/null @@ -1,45 +0,0 @@ -use anyhow::{Context, Result}; -use serde::Deserialize; -use smol::io::AsyncReadExt; -use std::sync::Arc; -use util::http::HttpClient; - -pub struct GitHubLspBinaryVersion { - pub name: String, - pub url: String, -} - -#[derive(Deserialize)] -pub(crate) struct GithubRelease { - pub name: String, - pub assets: Vec, -} - -#[derive(Deserialize)] -pub(crate) struct GithubReleaseAsset { - pub name: String, - pub browser_download_url: String, -} - -pub(crate) 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/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index f77b264fbf..db743f02af 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -1,4 +1,4 @@ -use super::node_runtime::NodeRuntime; +use node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 97c158fd1f..4da1056685 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,4 +1,4 @@ -use super::node_runtime::NodeRuntime; +use node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::HashMap; diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index f16761d870..2a18138cb7 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -8,7 +8,7 @@ use smol::fs; 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::GitHubLspBinaryVersion; +use util::github::GitHubLspBinaryVersion; #[derive(Copy, Clone)] pub struct LuaLspAdapter; diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 3a671c60f6..6227b8336d 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -1,4 +1,4 @@ -use super::node_runtime::NodeRuntime; +use node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index b95a64fa1e..d5a6773129 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -1,4 +1,3 @@ -use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; @@ -9,6 +8,7 @@ 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::github::{latest_github_release, GitHubLspBinaryVersion}; use util::http::HttpClient; use util::ResultExt; diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index d3704c84c8..69a135e6ec 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,8 +1,8 @@ -use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; use std::{ diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 6028ecd134..7339512f1a 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -1,9 +1,9 @@ -use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::MutableAppContext; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use serde_json::Value; use settings::Settings; use smol::fs; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c88f2e94f9..8f7b858dfd 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -18,6 +18,7 @@ use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, V use isahc::{config::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; +use node_runtime::NodeRuntime; use parking_lot::Mutex; use project::Fs; use serde_json::json; @@ -136,12 +137,9 @@ fn main() { languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - languages::init( - http.clone(), - cx.background().clone(), - languages.clone(), - themes.clone(), - ); + let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned()); + + languages::init(languages.clone(), themes.clone(), node_runtime.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); cx.set_global(client.clone()); @@ -162,7 +160,7 @@ fn main() { terminal_view::init(cx); theme_testbench::init(cx); recent_projects::init(cx); - copilot::init(client.clone(), cx); + copilot::init(client.clone(), node_runtime, 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 32706cb47f..43172d07fa 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -657,6 +657,7 @@ mod tests { executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle, }; use language::LanguageRegistry; + use node_runtime::NodeRuntime; use project::{Project, ProjectPath}; use serde_json::json; use std::{ @@ -1851,12 +1852,9 @@ mod tests { languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); let themes = ThemeRegistry::new((), cx.font_cache().clone()); - languages::init( - FakeHttpClient::with_404_response(), - cx.background().clone(), - languages.clone(), - themes, - ); + let http = FakeHttpClient::with_404_response(); + let node_runtime = NodeRuntime::new(http, cx.background().to_owned()); + languages::init(languages.clone(), themes, node_runtime); for name in languages.language_names() { languages.language_for_name(&name); } From 9d8d2bb8f4d1aa3178bfcab85924d8e4b4abcfec Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Mar 2023 18:00:09 -0700 Subject: [PATCH 28/53] Add rough versions of all 3 modals --- crates/copilot/src/copilot.rs | 37 +-- crates/copilot/src/sign_in.rs | 516 ++++++++++++++++++++++---------- crates/gpui/src/app.rs | 6 + crates/theme/src/theme.rs | 31 +- crates/theme/src/ui.rs | 17 +- styles/src/styleTree/copilot.ts | 79 ++++- 6 files changed, 480 insertions(+), 206 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index d12e5995d0..71277cd67a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -155,7 +155,10 @@ impl Copilot { SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { Task::ready(Ok(())).shared() } - SignInStatus::SigningIn { task, .. } => task.clone(), + SignInStatus::SigningIn { task, .. } => { + cx.notify(); // To re-show the prompt, just in case. + task.clone() + } SignInStatus::SignedOut => { let server = server.clone(); let task = cx @@ -463,35 +466,3 @@ async fn get_lsp_binary(http: Arc) -> anyhow::Result { } } } - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use util::http; - - #[gpui::test] - async fn test_smoke(cx: &mut TestAppContext) { - Settings::test_async(cx); - let http = http::client(); - let node_runtime = NodeRuntime::new(http.clone(), cx.background()); - let copilot = cx.add_model(|cx| Copilot::start(http, node_runtime, cx)); - smol::Timer::after(std::time::Duration::from_secs(2)).await; - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .await - .unwrap(); - copilot.read_with(cx, |copilot, _| copilot.status()); - - let buffer = cx.add_model(|cx| language::Buffer::new(0, "fn foo() -> ", cx)); - dbg!(copilot - .update(cx, |copilot, cx| copilot.completion(&buffer, 12, cx)) - .await - .unwrap()); - dbg!(copilot - .update(cx, |copilot, cx| copilot - .completions_cycling(&buffer, 12, cx)) - .await - .unwrap()); - } -} diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 80411f18da..9b4bc78d7c 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -13,72 +13,318 @@ struct OpenGithub; impl_internal_actions!(copilot_sign_in, [CopyUserCode, OpenGithub]); +const _COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; + +enum SignInContents { + PromptingUser(PromptUserDeviceFlow), + Unauthorized, + Enabled, +} + pub fn init(cx: &mut MutableAppContext) { let copilot = Copilot::global(cx).unwrap(); - let mut code_verification_window_id = None; + let mut code_verification_window_id: Option<(usize, SignInContents)> = None; cx.observe(&copilot, move |copilot, cx| { match copilot.read(cx).status() { crate::Status::SigningIn { prompt: Some(prompt), } => { - if let Some(window_id) = code_verification_window_id.take() { - cx.remove_window(window_id); - } + let window_id = match code_verification_window_id.take() { + Some((window_id, SignInContents::PromptingUser(current_prompt))) + if current_prompt == prompt => + { + if cx.window_ids().find(|item| item == &window_id).is_some() { + window_id + } else { + CopilotCodeVerification::prompting(prompt.clone(), cx) + } + } + Some((window_id, _)) => { + cx.remove_window(window_id); + CopilotCodeVerification::prompting(prompt.clone(), cx) + } + None => CopilotCodeVerification::prompting(prompt.clone(), cx), + }; - let window_size = cx.global::().theme.copilot.modal.dimensions(); - - let (window_id, _) = cx.add_window( - WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new( - Default::default(), - window_size, - )), - titlebar: None, - center: true, - focus: false, - kind: WindowKind::Normal, - is_movable: true, - screen: None, - }, - |_| CopilotCodeVerification::new(prompt), - ); - code_verification_window_id = Some(window_id); + code_verification_window_id = + Some((window_id, SignInContents::PromptingUser(prompt))); cx.activate_window(window_id); } + crate::Status::Authorized => match code_verification_window_id.take() { + Some((window_id, sign_in_contents)) => { + match sign_in_contents { + SignInContents::PromptingUser(_) => cx.remove_window(window_id), + SignInContents::Unauthorized => cx.remove_window(window_id), + SignInContents::Enabled => { + if cx.has_window(window_id) { + code_verification_window_id = + Some((window_id, SignInContents::Enabled)) + } + return; + } + } + let window_id = CopilotCodeVerification::enabled(cx); + code_verification_window_id = Some((window_id, SignInContents::Enabled)); + cx.activate_window(window_id); + } + None => return, + }, + crate::Status::Unauthorized => match code_verification_window_id.take() { + Some((window_id, sign_in_contents)) => { + match sign_in_contents { + SignInContents::PromptingUser(_) => cx.remove_window(window_id), // Show prompt + SignInContents::Unauthorized => { + if cx.has_window(window_id) { + code_verification_window_id = + Some((window_id, SignInContents::Unauthorized)) + } + return; + } //Do nothing + SignInContents::Enabled => cx.remove_window(window_id), // + } + + let window_id = CopilotCodeVerification::unauthorized(cx); + code_verification_window_id = Some((window_id, SignInContents::Unauthorized)); + cx.activate_window(window_id); + } + None => return, + }, _ => { - if let Some(window_id) = code_verification_window_id.take() { + if let Some((window_id, _)) = code_verification_window_id.take() { cx.remove_window(window_id); } } } }) .detach(); - - // let window_size = cx.global::().theme.copilot.modal.dimensions(); - - // let (_window_id, _) = cx.add_window( - // WindowOptions { - // bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), - // titlebar: None, - // center: true, - // focus: false, - // kind: WindowKind::PopUp, - // is_movable: true, - // screen: None, - // }, - // |_| { - // CopilotCodeVerification::new(PromptUserDeviceFlow { - // user_code: "ABCD-1234".to_string(), - // verification_uri: "https://github.com/login/device".to_string(), - // }) - // }, - // ); } pub struct CopilotCodeVerification { - prompt: PromptUserDeviceFlow, + prompt: SignInContents, +} + +impl CopilotCodeVerification { + pub fn prompting(prompt: PromptUserDeviceFlow, cx: &mut MutableAppContext) -> usize { + let window_size = cx.global::().theme.copilot.modal.dimensions(); + + let (window_id, _) = cx.add_window( + WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), + titlebar: None, + center: true, + focus: false, + kind: WindowKind::Normal, + is_movable: true, + screen: None, + }, + |_| CopilotCodeVerification { + prompt: SignInContents::PromptingUser(prompt), + }, + ); + + window_id + } + + pub fn unauthorized(cx: &mut MutableAppContext) -> usize { + let window_size = cx.global::().theme.copilot.modal.dimensions(); + + let (window_id, _) = cx.add_window( + WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), + titlebar: None, + center: true, + focus: false, + kind: WindowKind::Normal, + is_movable: true, + screen: None, + }, + |_| CopilotCodeVerification { + prompt: SignInContents::Unauthorized, + }, + ); + + window_id + } + + pub fn enabled(cx: &mut MutableAppContext) -> usize { + let window_size = cx.global::().theme.copilot.modal.dimensions(); + + let (window_id, _) = cx.add_window( + WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), + titlebar: None, + center: true, + focus: false, + kind: WindowKind::Normal, + is_movable: true, + screen: None, + }, + |_| CopilotCodeVerification { + prompt: SignInContents::Enabled, + }, + ); + + window_id + } + + fn render_device_code( + data: &PromptUserDeviceFlow, + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { + let copied = cx + .read_from_clipboard() + .map(|item| item.text() == &data.user_code) + .unwrap_or(false); + + Flex::column() + .with_children([ + MouseEventHandler::::new(0, cx, |state, _cx| { + Flex::row() + .with_children([ + Label::new(data.user_code.clone(), style.auth.device_code.clone()) + .aligned() + .contained() + .with_style(style.auth.device_code_left_container) + .constrained() + .with_width(style.auth.device_code_left) + .boxed(), + Empty::new() + .constrained() + .with_width(1.) + .with_height(style.auth.device_code_seperator_height) + .contained() + .with_background_color( + style + .auth + .cta_button + .style_for(state, false) + .container + .border + .color, + ) + .boxed(), + Label::new( + if copied { "Copied!" } else { "Copy" }, + style.auth.cta_button.style_for(state, false).text.clone(), + ) + .aligned() + .contained() + .with_style(style.auth.device_code_right_container) + .constrained() + .with_width(style.auth.device_code_right) + .boxed(), + ]) + .contained() + .with_style(style.auth.device_code_cta.style_for(state, false).container) + .constrained() + .with_width(style.auth.content_width) + .boxed() + }) + .on_click(gpui::MouseButton::Left, { + let user_code = data.user_code.clone(); + move |_, cx| { + cx.platform() + .write_to_clipboard(ClipboardItem::new(user_code.clone())); + cx.notify(); + } + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed(), + Flex::column() + .with_children([ + Label::new( + "Paste this code into GitHub after", + style.auth.hint.text.clone(), + ) + .boxed(), + Label::new("clicking the button below.", style.auth.hint.text.clone()) + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.hint.container.clone()) + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.device_code_group) + .aligned() + .boxed() + } + + fn render_not_authorized_warning(style: &theme::Copilot) -> ElementBox { + Flex::column() + .with_children([ + Flex::column() + .with_children([ + Label::new( + "You must have an active copilot", + style.auth.warning.text.to_owned(), + ) + .aligned() + .boxed(), + Label::new( + "license to use it in Zed.", + style.auth.warning.text.to_owned(), + ) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.warning.container) + .boxed(), + Flex::column() + .with_children([ + Label::new( + "Try connecting again once you", + style.auth.hint.text.to_owned(), + ) + .aligned() + .boxed(), + Label::new( + "have activated a Copilot license.", + style.auth.hint.text.to_owned(), + ) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.not_authorized_hint) + .boxed(), + ]) + .align_children_center() + .boxed() + } + + fn render_copilot_enabled(style: &theme::Copilot) -> ElementBox { + Flex::column() + .with_children([ + Label::new( + "You can update your settings or", + style.auth.hint.text.clone(), + ) + .aligned() + .boxed(), + Label::new( + "sign out from the Copilot menu in", + style.auth.hint.text.clone(), + ) + .aligned() + .boxed(), + Label::new("the status bar.", style.auth.hint.text.clone()) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.enabled_hint) + .boxed() + } } impl Entity for CopilotCodeVerification { @@ -94,137 +340,107 @@ impl View for CopilotCodeVerification { cx.notify() } + fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext) { + cx.notify() + } + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { let style = cx.global::().theme.copilot.clone(); - let copied = cx - .read_from_clipboard() - .map(|item| item.text() == &self.prompt.user_code) - .unwrap_or(false); - - theme::ui::modal("Authenticate Copilot", &style.modal, cx, |cx| { + theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| { Flex::column() - .align_children_center() .with_children([ Flex::column() .with_children([ Flex::row() .with_children([ theme::ui::svg(&style.auth.copilot_icon).boxed(), - theme::ui::svg(&style.auth.plus_icon).boxed(), + theme::ui::icon(&style.auth.plus_icon).boxed(), theme::ui::svg(&style.auth.zed_icon).boxed(), ]) .boxed(), - Label::new("Copilot for Zed", style.auth.header_text.clone()).boxed(), + match self.prompt { + SignInContents::PromptingUser(_) | SignInContents::Unauthorized => { + Flex::column() + .with_children([ + Label::new( + "Enable Copilot by connecting", + style.auth.enable_text.clone(), + ) + .boxed(), + Label::new( + "your existing license.", + style.auth.enable_text.clone(), + ) + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.enable_group.clone()) + .boxed() + } + SignInContents::Enabled => { + Label::new("Copilot Enabled!", style.auth.enable_text.clone()) + .boxed() + } + }, ]) .align_children_center() .contained() .with_style(style.auth.header_group) .aligned() .boxed(), + match &self.prompt { + SignInContents::PromptingUser(data) => { + Self::render_device_code(data, &style, cx) + } + SignInContents::Unauthorized => Self::render_not_authorized_warning(&style), + SignInContents::Enabled => Self::render_copilot_enabled(&style), + }, Flex::column() - .with_children([ - Label::new( - "Here is your code to authenticate with github", - style.auth.instruction_text.clone(), - ) - .boxed(), - MouseEventHandler::::new(0, cx, |state, _cx| { - Flex::row() - .with_children([ - Label::new( - self.prompt.user_code.clone(), - style.auth.device_code.clone(), - ) - .aligned() - .contained() - .with_style(style.auth.device_code_left_container) - .constrained() - .with_width(style.auth.device_code_left) - .boxed(), - Empty::new() - .constrained() - .with_width(1.) - .with_height(style.auth.device_code_seperator_height) - .contained() - .with_background_color( - style - .auth - .cta_button - .style_for(state, false) - .container - .border - .color, - ) - .boxed(), - Label::new( - if copied { "Copied!" } else { "Copy" }, - style - .auth - .cta_button - .style_for(state, false) - .text - .clone(), - ) - .aligned() - .contained() - .with_style(style.auth.device_code_right_container) - .constrained() - .with_width(style.auth.device_code_right) - .boxed(), - ]) - .contained() - .with_style( - style - .auth - .device_code_cta - .style_for(state, false) - .container, + .with_child({ + match &self.prompt { + SignInContents::PromptingUser(data) => { + theme::ui::cta_button_with_click( + "Connect to GitHub", + style.auth.content_width, + &style.auth.cta_button, + cx, + { + let verification_uri = data.verification_uri.clone(); + move |_, cx| cx.platform().open_url(&verification_uri) + }, ) - .constrained() - .with_width(style.auth.content_width) - .boxed() - }) - .on_click(gpui::MouseButton::Left, { - let user_code = self.prompt.user_code.clone(); - move |_, cx| { - cx.platform() - .write_to_clipboard(ClipboardItem::new(user_code.clone())); - cx.notify(); } - }) - .with_cursor_style(gpui::CursorStyle::PointingHand) - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.device_code_group) - .aligned() - .boxed(), - Flex::column() - .with_children([ - Label::new( - "Copy it and enter it on GitHub", - style.auth.instruction_text.clone(), - ) - .boxed(), - theme::ui::cta_button_with_click( - "Go to Github", - style.auth.content_width, - &style.auth.cta_button, - cx, - { - let verification_uri = self.prompt.verification_uri.clone(); - move |_, cx| cx.platform().open_url(&verification_uri) - }, - ), - ]) + SignInContents::Unauthorized => theme::ui::cta_button_with_click( + "Close", + style.auth.content_width, + &style.auth.cta_button, + cx, + |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id) + }, + ), + SignInContents::Enabled => theme::ui::cta_button_with_click( + "Done", + style.auth.content_width, + &style.auth.cta_button, + cx, + |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id) + }, + ), + } + }) .align_children_center() .contained() .with_style(style.auth.github_group) .aligned() .boxed(), ]) + .align_children_center() .constrained() .with_width(style.auth.content_width) .aligned() @@ -232,9 +448,3 @@ impl View for CopilotCodeVerification { }) } } - -impl CopilotCodeVerification { - pub fn new(prompt: PromptUserDeviceFlow) -> Self { - CopilotCodeVerification { prompt } - } -} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0c8256fefb..a744018e1f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -765,6 +765,12 @@ impl MutableAppContext { }) } + pub fn has_window(&self, window_id: usize) -> bool { + self.window_ids() + .find(|window| window == &window_id) + .is_some() + } + pub fn window_ids(&self) -> impl Iterator + '_ { self.cx.windows.keys().copied() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ae072eca32..29acdd92fe 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -9,7 +9,7 @@ use gpui::{ use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use std::{collections::HashMap, sync::Arc}; -use ui::{ButtonStyle, CheckboxStyle, ModalStyle, SvgStyle}; +use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle}; pub mod ui; @@ -124,13 +124,14 @@ pub struct Copilot { #[derive(Deserialize, Default, Clone)] pub struct CopilotAuth { + pub enable_group: ContainerStyle, + pub enable_text: TextStyle, pub instruction_text: TextStyle, pub cta_button: ButtonStyle, pub content_width: f32, pub copilot_icon: SvgStyle, - pub plus_icon: SvgStyle, + pub plus_icon: IconStyle, pub zed_icon: SvgStyle, - pub header_text: TextStyle, pub device_code_group: ContainerStyle, pub github_group: ContainerStyle, pub header_group: ContainerStyle, @@ -141,6 +142,10 @@ pub struct CopilotAuth { pub device_code_right: f32, pub device_code_right_container: ContainerStyle, pub device_code_seperator_height: f32, + pub hint: ContainedText, + pub enabled_hint: ContainerStyle, + pub not_authorized_hint: ContainerStyle, + pub warning: ContainedText, } #[derive(Deserialize, Default)] @@ -720,7 +725,9 @@ pub struct DiffStyle { pub struct Interactive { pub default: T, pub hover: Option, + pub hover_and_active: Option, pub clicked: Option, + pub click_and_active: Option, pub active: Option, pub disabled: Option, } @@ -728,7 +735,17 @@ pub struct Interactive { impl Interactive { pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T { if active { - self.active.as_ref().unwrap_or(&self.default) + if state.hovered() { + self.hover_and_active + .as_ref() + .unwrap_or(self.active.as_ref().unwrap_or(&self.default)) + } else if state.clicked() == Some(gpui::MouseButton::Left) && self.clicked.is_some() { + self.click_and_active + .as_ref() + .unwrap_or(self.active.as_ref().unwrap_or(&self.default)) + } else { + self.active.as_ref().unwrap_or(&self.default) + } } else if state.clicked() == Some(gpui::MouseButton::Left) && self.clicked.is_some() { self.clicked.as_ref().unwrap() } else if state.hovered() { @@ -753,7 +770,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { #[serde(flatten)] default: Value, hover: Option, + hover_and_active: Option, clicked: Option, + click_and_active: Option, active: Option, disabled: Option, } @@ -780,7 +799,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { }; let hover = deserialize_state(json.hover)?; + let hover_and_active = deserialize_state(json.hover_and_active)?; let clicked = deserialize_state(json.clicked)?; + let click_and_active = deserialize_state(json.click_and_active)?; let active = deserialize_state(json.active)?; let disabled = deserialize_state(json.disabled)?; let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?; @@ -788,7 +809,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { Ok(Interactive { default, hover, + hover_and_active, clicked, + click_and_active, active, disabled, }) diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 50239bdea5..7518d4c304 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -9,7 +9,7 @@ use gpui::{ fonts::TextStyle, geometry::vector::{vec2f, Vector2F}, scene::MouseClick, - Action, Element, ElementBox, EventContext, MouseButton, RenderContext, View, + Action, Element, ElementBox, EventContext, MouseButton, MouseState, RenderContext, View, }; use serde::Deserialize; @@ -220,7 +220,7 @@ pub struct ModalStyle { close_icon: Interactive, container: ContainerStyle, titlebar: ContainerStyle, - title_text: TextStyle, + title_text: Interactive, dimensions: Dimensions, } @@ -241,14 +241,23 @@ where I: Into>, F: FnOnce(&mut gpui::RenderContext) -> ElementBox, { + let active = cx.window_is_active(cx.window_id()); + Flex::column() .with_child( Stack::new() .with_children([ - Label::new(title, style.title_text.clone()).boxed(), + Label::new( + title, + style + .title_text + .style_for(&mut MouseState::default(), active) + .clone(), + ) + .boxed(), // FIXME: Get a better tag type MouseEventHandler::::new(999999, cx, |state, _cx| { - let style = style.close_icon.style_for(state, false); + let style = style.close_icon.style_for(state, active); icon(style).boxed() }) .on_click(gpui::MouseButton::Left, move |_, cx| { diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index 75fc99b591..fe77cab8dc 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -31,12 +31,17 @@ export default function copilot(colorScheme: ColorScheme) { return { modal: { - titleText: text(layer, "sans", { size: "md" }), + titleText: { + ...text(layer, "sans", { size: "md", color: background(layer, "default") }), + active: { + ...text(layer, "sans", { size: "md" }), + } + }, titlebar: { border: border(layer, "active"), padding: { - top: 4, - bottom: 4, + top: 8, + bottom: 8, left: 8, right: 8, }, @@ -44,7 +49,7 @@ export default function copilot(colorScheme: ColorScheme) { top: 0, left: 0, right: 0, - bottom: 8 + bottom: 16 } }, container: { @@ -54,6 +59,7 @@ export default function copilot(colorScheme: ColorScheme) { closeIcon: { icon: svg(background(layer, "on"), "icons/x_mark_16.svg", 16, 16), container: { + cornerRadius: 2, padding: { top: 3, bottom: 3, @@ -61,8 +67,14 @@ export default function copilot(colorScheme: ColorScheme) { right: 0, } }, - hover: { - icon: svg(foreground(layer, "on"), "icons/x_mark_16.svg", 16, 16), + active: { + icon: svg(foreground(colorScheme.lowest, "warning"), "icons/x_mark_16.svg", 16, 16), + }, + hoverAndActive: { + icon: svg(foreground(layer, "on", "hovered"), "icons/x_mark_16.svg", 16, 16), + }, + clickedAndactive: { + icon: svg(foreground(layer, "on", "pressed"), "icons/x_mark_16.svg", 16, 16), } }, dimensions: { @@ -81,17 +93,35 @@ export default function copilot(colorScheme: ColorScheme) { right: 0 } }, - headerText: text(layer, "sans", { size: "lg" }), - copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 36, 36), - plusIcon: svg(foreground(layer, "default"), "icons/plus_16.svg", 36, 36), - zedIcon: svg(foreground(layer, "default"), "icons/logo_96.svg", 36, 36), + copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 32, 32), + plusIcon: { + icon: svg(foreground(layer, "default"), "icons/plus_12.svg", 12, 12), + container: { + padding: { + top: 12, + bottom: 12, + left: 12, + right: 12, + } + } + }, + zedIcon: svg(foreground(layer, "default"), "icons/logo_96.svg", 32, 32), + enableText: text(layer, "sans", { size: "md" }), + enableGroup: { + margin: { + top: 5, + bottom: 5, + left: 0, + right: 0 + } + }, instructionText: text(layer, "sans"), deviceCodeGroup: { margin: { - top: 5, - bottom: 5, + top: 20, + bottom: 20, left: 0, right: 0 } @@ -127,6 +157,31 @@ export default function copilot(colorScheme: ColorScheme) { }, }, deviceCodeSeperatorHeight: 0, + hint: { + ...text(layer, "sans", { size: "xs" }), + margin: { + top: -5, + } + }, + enabledHint: { + margin: { + top: 10, + bottom: 10 + } + }, + notAuthorizedHint: { + margin: { + top: 10, + bottom: 10 + } + }, + + warning: { + ...text(layer, "sans", { size: "md", color: foreground(layer, "warning") }), + border: border(layer, "warning"), + background_color: background(layer, "warning"), + cornerRadius: 2, + }, githubGroup: { margin: { From ce9774be530abfd692dafa7d84b4af15967b39d9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 28 Mar 2023 09:50:03 +0200 Subject: [PATCH 29/53] Improve detection of common prefix in `text_for_active_completion` --- crates/editor/src/editor.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6b47034674..9c1b0913aa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1034,19 +1034,16 @@ impl CopilotState { let completion = self.completions.get(self.active_completion_index)?; if self.position.excerpt_id == cursor.excerpt_id && self.position.buffer_id == cursor.buffer_id - && (cursor_offset == buffer.len() || buffer.contains_str_at(cursor_offset, "\n")) { - let completion_offset = buffer.summary_for_anchor(&Anchor { + let completion_offset: usize = buffer.summary_for_anchor(&Anchor { excerpt_id: self.position.excerpt_id, buffer_id: self.position.buffer_id, text_anchor: completion.position, }); - let common_prefix_len = cursor_offset.saturating_sub(completion_offset); - if common_prefix_len <= completion.text.len() - && buffer.contains_str_at(completion_offset, &completion.text[..common_prefix_len]) - { - let suffix = &completion.text[common_prefix_len..]; - if !suffix.is_empty() { + let prefix_len = cursor_offset.saturating_sub(completion_offset); + if completion_offset <= cursor_offset && prefix_len <= completion.text.len() { + let (prefix, suffix) = completion.text.split_at(prefix_len); + if buffer.contains_str_at(completion_offset, prefix) && !suffix.is_empty() { return Some(suffix); } } @@ -2867,8 +2864,8 @@ impl Editor { .text_for_active_completion(self.copilot_state.position, &snapshot) .map(|text| text.to_string()) { - self.copilot_state = Default::default(); self.insert(&text, cx); + self.clear_copilot_suggestions(cx); true } else { false From 12370f120e189d58954b6ea7640a33702603ca38 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 28 Mar 2023 10:23:35 +0200 Subject: [PATCH 30/53] :art: --- crates/editor/src/editor.rs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9c1b0913aa..ccd090c409 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1007,7 +1007,7 @@ impl CodeActionsMenu { } struct CopilotState { - position: Anchor, + excerpt_id: Option, pending_refresh: Task>, completions: Vec, active_completion_index: usize, @@ -1016,7 +1016,7 @@ struct CopilotState { impl Default for CopilotState { fn default() -> Self { Self { - position: Anchor::min(), + excerpt_id: None, pending_refresh: Task::ready(Some(())), completions: Default::default(), active_completion_index: 0, @@ -1032,12 +1032,10 @@ impl CopilotState { ) -> Option<&str> { let cursor_offset = cursor.to_offset(buffer); let completion = self.completions.get(self.active_completion_index)?; - if self.position.excerpt_id == cursor.excerpt_id - && self.position.buffer_id == cursor.buffer_id - { + if self.excerpt_id == Some(cursor.excerpt_id) { let completion_offset: usize = buffer.summary_for_anchor(&Anchor { - excerpt_id: self.position.excerpt_id, - buffer_id: self.position.buffer_id, + excerpt_id: cursor.excerpt_id, + buffer_id: cursor.buffer_id, text_anchor: completion.position, }); let prefix_len = cursor_offset.saturating_sub(completion_offset); @@ -2777,12 +2775,11 @@ impl Editor { .completions .swap(0, self.copilot_state.active_completion_index); self.copilot_state.completions.truncate(1); + self.copilot_state.active_completion_index = 0; + cx.notify(); } else { self.clear_copilot_suggestions(cx); } - self.copilot_state.position = cursor; - self.copilot_state.active_completion_index = 0; - cx.notify(); if !copilot.read(cx).status().is_authorized() { return None; @@ -2805,6 +2802,7 @@ impl Editor { this.upgrade(&cx)?.update(&mut cx, |this, cx| { this.copilot_state.completions.clear(); this.copilot_state.active_completion_index = 0; + this.copilot_state.excerpt_id = Some(cursor.excerpt_id); for completion in completions { let was_empty = this.copilot_state.completions.is_empty(); if let Some(completion) = this.copilot_state.push_completion(completion) { @@ -2837,17 +2835,18 @@ impl Editor { } let snapshot = self.buffer.read(cx).snapshot(cx); + let cursor = self.selections.newest_anchor().head(); self.copilot_state.active_completion_index = (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len(); if let Some(text) = self .copilot_state - .text_for_active_completion(self.copilot_state.position, &snapshot) + .text_for_active_completion(cursor, &snapshot) { self.display_map.update(cx, |map, cx| { map.replace_suggestion( Some(Suggestion { - position: self.copilot_state.position, + position: cursor, text: text.into(), }), cx, @@ -2859,12 +2858,12 @@ impl Editor { fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { let snapshot = self.buffer.read(cx).snapshot(cx); + let cursor = self.selections.newest_anchor().head(); if let Some(text) = self .copilot_state - .text_for_active_completion(self.copilot_state.position, &snapshot) - .map(|text| text.to_string()) + .text_for_active_completion(cursor, &snapshot) { - self.insert(&text, cx); + self.insert(&text.to_string(), cx); self.clear_copilot_suggestions(cx); true } else { @@ -2879,7 +2878,7 @@ impl Editor { self.copilot_state.completions.clear(); self.copilot_state.active_completion_index = 0; self.copilot_state.pending_refresh = Task::ready(None); - self.copilot_state.position = Anchor::min(); + self.copilot_state.excerpt_id = None; cx.notify(); !was_empty } From a5cec18775fa61c8af75a1561c053d66815f9397 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 28 Mar 2023 10:23:49 +0200 Subject: [PATCH 31/53] Remove copilot smoke test From aea8475d30c4a79fc5a1422b2e78ca63fccb8f0d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Mar 2023 18:45:37 -0700 Subject: [PATCH 32/53] Apply cargo fmt --- crates/zed/src/languages/html.rs | 2 +- crates/zed/src/languages/json.rs | 2 +- crates/zed/src/languages/python.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index db743f02af..20f097ba7f 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -1,8 +1,8 @@ -use node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; use std::ffi::OsString; diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 4da1056685..7919f7510d 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,10 +1,10 @@ -use node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::HashMap; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::MutableAppContext; use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use serde_json::json; use settings::{keymap_file_json_schema, settings_file_json_schema}; use smol::fs; diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 6227b8336d..d5fd865221 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -1,8 +1,8 @@ -use node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use smol::fs; use std::{ any::Any, From 0b0c7e4ce9a949a624593345bd21bb0adb32d457 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Mar 2023 20:13:17 -0700 Subject: [PATCH 33/53] Move command palette filter into collections crate Filter out copilot commands from command palette when not active --- Cargo.lock | 1 + crates/collections/src/collections.rs | 7 ++ crates/command_palette/src/command_palette.rs | 7 +- crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 103 +++++++++++------- crates/copilot/src/sign_in.rs | 6 +- crates/vim/src/vim.rs | 2 +- 7 files changed, 76 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7654a1ac47..ade31e169e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,6 +1338,7 @@ dependencies = [ "anyhow", "async-compression", "client", + "collections", "futures 0.3.25", "gpui", "language", diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs index acef74dbd6..eb4e4d8462 100644 --- a/crates/collections/src/collections.rs +++ b/crates/collections/src/collections.rs @@ -24,3 +24,10 @@ pub type HashMap = std::collections::HashMap; pub type HashSet = std::collections::HashSet; pub use std::collections::*; + +// NEW TYPES + +#[derive(Default)] +pub struct CommandPaletteFilter { + pub filtered_namespaces: HashSet<&'static str>, +} diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 52a0e1cdc0..229e4a04e5 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,4 +1,4 @@ -use collections::HashSet; +use collections::CommandPaletteFilter; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, @@ -12,11 +12,6 @@ use settings::Settings; use std::cmp; use workspace::Workspace; -#[derive(Default)] -pub struct CommandPaletteFilter { - pub filtered_namespaces: HashSet<&'static str>, -} - pub fn init(cx: &mut MutableAppContext) { cx.add_action(CommandPalette::toggle); Picker::::init(cx); diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 74dd73df0b..f39ab604e2 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -9,6 +9,7 @@ path = "src/copilot.rs" doctest = false [dependencies] +collections = { path = "../collections" } gpui = { path = "../gpui" } language = { path = "../language" } settings = { path = "../settings" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 71277cd67a..98107ec8cf 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -2,7 +2,6 @@ mod request; mod sign_in; use anyhow::{anyhow, Result}; -use async_compression::futures::bufread::GzipDecoder; use client::Client; use futures::{future::Shared, FutureExt, TryFutureExt}; use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; @@ -10,17 +9,18 @@ use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapsho use lsp::LanguageServer; use node_runtime::NodeRuntime; use settings::Settings; -use smol::{fs, io::BufReader, stream::StreamExt}; +use smol::{fs, stream::StreamExt}; use std::{ - env::consts, + ffi::OsString, path::{Path, PathBuf}, sync::Arc, }; -use util::{ - fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, -}; +use util::{fs::remove_matching, http::HttpClient, paths, ResultExt}; -actions!(copilot, [SignIn, SignOut, NextSuggestion]); +actions!(copilot_auth, [SignIn, SignOut]); + +const COPILOT_NAMESPACE: &'static str = "copilot"; +actions!(copilot, [NextSuggestion]); pub fn init(client: Arc, node_runtime: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx)); @@ -37,6 +37,18 @@ pub fn init(client: Arc, node_runtime: Arc, cx: &mut Mutabl .update(cx, |copilot, cx| copilot.sign_out(cx)) .detach_and_log_err(cx); }); + + cx.observe(&copilot, |handle, cx| { + let status = handle.read(cx).status(); + cx.update_global::( + move |filter, _cx| match status { + Status::Authorized => filter.filtered_namespaces.remove(COPILOT_NAMESPACE), + _ => filter.filtered_namespaces.insert(COPILOT_NAMESPACE), + }, + ); + }) + .detach(); + sign_in::init(cx); } @@ -113,9 +125,12 @@ impl Copilot { // TODO: Don't eagerly download the LSP cx.spawn(|this, mut cx| async move { let start_language_server = async { - let server_path = get_lsp_binary(http).await?; + let server_path = get_copilot_lsp(http, node_runtime.clone()).await?; + let node_path = node_runtime.binary_path().await?; + let arguments: &[OsString] = &[server_path.into(), "--stdio".into()]; let server = - LanguageServer::new(0, &server_path, &["--stdio"], Path::new("/"), cx.clone())?; + LanguageServer::new(0, &node_path, arguments, Path::new("/"), cx.clone())?; + let server = server.initialize(Default::default()).await?; let status = server .request::(request::CheckStatusParams { @@ -414,53 +429,61 @@ fn completion_from_lsp(completion: request::Completion, buffer: &BufferSnapshot) } } -async fn get_lsp_binary(http: Arc) -> anyhow::Result { +async fn get_copilot_lsp( + http: Arc, + node: Arc, +) -> anyhow::Result { + const SERVER_PATH: &'static str = "node_modules/copilot-node-server/copilot/dist/agent.js"; + ///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/copilot", 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))?; + async fn fetch_latest( + _http: Arc, + node: Arc, + ) -> anyhow::Result { + const COPILOT_NPM_PACKAGE: &'static str = "copilot-node-server"; - fs::create_dir_all(&*paths::COPILOT_DIR).await?; - let destination_path = - paths::COPILOT_DIR.join(format!("copilot-{}-{}", release.name, consts::ARCH)); + let release = node.npm_package_latest_version(COPILOT_NPM_PACKAGE).await?; - 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?; + let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.clone())); - remove_matching(&paths::COPILOT_DIR, |entry| entry != destination_path).await; + fs::create_dir_all(version_dir).await?; + let server_path = version_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + node.npm_install_packages([(COPILOT_NPM_PACKAGE, release.as_str())], version_dir) + .await?; + + remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await; } - Ok(destination_path) + Ok(server_path) } - match fetch_latest(http).await { + match fetch_latest(http, node).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 last_version_dir = None; let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?; while let Some(entry) = entries.next().await { - last = Some(entry?.path()); + 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 + )) } - last.ok_or_else(|| anyhow!("no cached binary")) })() .await } diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 9b4bc78d7c..6f32347aaa 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,7 +1,7 @@ use crate::{request::PromptUserDeviceFlow, Copilot}; use gpui::{ - elements::*, geometry::rect::RectF, impl_internal_actions, ClipboardItem, Element, Entity, - MutableAppContext, View, WindowKind, WindowOptions, + elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View, + WindowKind, WindowOptions, }; use settings::Settings; @@ -11,8 +11,6 @@ struct CopyUserCode; #[derive(PartialEq, Eq, Debug, Clone)] struct OpenGithub; -impl_internal_actions!(copilot_sign_in, [CopyUserCode, OpenGithub]); - const _COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; enum SignInContents { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 33f142c21e..34afcb5f84 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -12,7 +12,7 @@ mod visual; use std::sync::Arc; -use command_palette::CommandPaletteFilter; +use collections::CommandPaletteFilter; use editor::{Bias, Cancel, Editor, EditorMode}; use gpui::{ actions, impl_actions, MutableAppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, From d60ef03d66157649b08594de7a1a58df2f5a3091 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 28 Mar 2023 20:52:50 -0700 Subject: [PATCH 34/53] WIP Add copilot disabled setting --- crates/copilot/src/copilot.rs | 82 ++++++++++++++++++++++++++++----- crates/settings/src/settings.rs | 28 +++++++++++ 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 98107ec8cf..3da2e1de71 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -3,8 +3,11 @@ mod sign_in; use anyhow::{anyhow, Result}; use client::Client; -use futures::{future::Shared, FutureExt, TryFutureExt}; -use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; +use futures::{future::Shared, Future, FutureExt, TryFutureExt}; +use gpui::{ + actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, + Task, +}; use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, ToPointUtf16}; use lsp::LanguageServer; use node_runtime::NodeRuntime; @@ -17,6 +20,7 @@ use std::{ }; use util::{fs::remove_matching, http::HttpClient, paths, ResultExt}; +const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; actions!(copilot_auth, [SignIn, SignOut]); const COPILOT_NAMESPACE: &'static str = "copilot"; @@ -42,8 +46,18 @@ pub fn init(client: Arc, node_runtime: Arc, cx: &mut Mutabl let status = handle.read(cx).status(); cx.update_global::( move |filter, _cx| match status { - Status::Authorized => filter.filtered_namespaces.remove(COPILOT_NAMESPACE), - _ => filter.filtered_namespaces.insert(COPILOT_NAMESPACE), + Status::Disabled => { + filter.filtered_namespaces.insert(COPILOT_NAMESPACE); + filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE); + } + Status::Authorized => { + filter.filtered_namespaces.remove(COPILOT_NAMESPACE); + filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + } + _ => { + filter.filtered_namespaces.insert(COPILOT_NAMESPACE); + filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + } }, ); }) @@ -55,6 +69,7 @@ pub fn init(client: Arc, node_runtime: Arc, cx: &mut Mutabl enum CopilotServer { Downloading, Error(Arc), + Disabled, Started { server: Arc, status: SignInStatus, @@ -80,6 +95,7 @@ enum SignInStatus { pub enum Status { Downloading, Error(Arc), + Disabled, SignedOut, SigningIn { prompt: Option, @@ -122,8 +138,55 @@ impl Copilot { node_runtime: Arc, cx: &mut ModelContext, ) -> Self { - // TODO: Don't eagerly download the LSP - cx.spawn(|this, mut cx| async move { + // TODO: Make this task resilient to users thrashing the copilot setting + cx.observe_global::({ + let http = http.clone(); + let node_runtime = node_runtime.clone(); + move |this, cx| { + if cx.global::().copilot.as_bool() { + if matches!(this.server, CopilotServer::Disabled) { + cx.spawn({ + let http = http.clone(); + let node_runtime = node_runtime.clone(); + move |this, cx| { + Self::start_language_server(http, node_runtime, this, cx) + } + }) + .detach(); + } + } else { + // TODO: What else needs to be turned off here? + this.server = CopilotServer::Disabled + } + } + }) + .detach(); + + if !cx.global::().copilot.as_bool() { + return Self { + server: CopilotServer::Disabled, + }; + } + + cx.spawn({ + let http = http.clone(); + let node_runtime = node_runtime.clone(); + move |this, cx| Self::start_language_server(http, node_runtime, this, cx) + }) + .detach(); + + Self { + server: CopilotServer::Downloading, + } + } + + fn start_language_server( + http: Arc, + node_runtime: Arc, + this: ModelHandle, + mut cx: AsyncAppContext, + ) -> impl Future { + async move { let start_language_server = async { let server_path = get_copilot_lsp(http, node_runtime.clone()).await?; let node_path = node_runtime.binary_path().await?; @@ -156,11 +219,6 @@ impl Copilot { } } }) - }) - .detach(); - - Self { - server: CopilotServer::Downloading, } } @@ -324,6 +382,7 @@ impl Copilot { pub fn status(&self) -> Status { match &self.server { CopilotServer::Downloading => Status::Downloading, + CopilotServer::Disabled => Status::Disabled, CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Started { status, .. } => match status { SignInStatus::Authorized { .. } => Status::Authorized, @@ -358,6 +417,7 @@ impl Copilot { fn authorized_server(&self) -> Result> { match &self.server { CopilotServer::Downloading => Err(anyhow!("copilot is still downloading")), + CopilotServer::Disabled => Err(anyhow!("copilot is disabled")), CopilotServer::Error(error) => Err(anyhow!( "copilot was not started because of an error: {}", error diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 4566776a34..f56364cfa8 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -58,6 +58,29 @@ pub struct Settings { pub telemetry_overrides: TelemetrySettings, pub auto_update: bool, pub base_keymap: BaseKeymap, + pub copilot: CopilotSettings, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +pub enum CopilotSettings { + #[default] + On, + Off, +} + +impl From for bool { + fn from(value: CopilotSettings) -> Self { + match value { + CopilotSettings::On => true, + CopilotSettings::Off => false, + } + } +} + +impl CopilotSettings { + pub fn as_bool(&self) -> bool { + >::into(*self) + } } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] @@ -375,6 +398,8 @@ pub struct SettingsFileContent { pub auto_update: Option, #[serde(default)] pub base_keymap: Option, + #[serde(default)] + pub copilot: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -452,6 +477,7 @@ impl Settings { telemetry_overrides: Default::default(), auto_update: defaults.auto_update.unwrap(), base_keymap: Default::default(), + copilot: Default::default(), } } @@ -503,6 +529,7 @@ impl Settings { merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.base_keymap, data.base_keymap); + merge(&mut self.copilot, data.copilot); self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); @@ -681,6 +708,7 @@ impl Settings { telemetry_overrides: Default::default(), auto_update: true, base_keymap: Default::default(), + copilot: Default::default(), } } From f8127962839b1e77ae86700a1b83e391d1bc9828 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Mar 2023 12:45:53 -0700 Subject: [PATCH 35/53] Make modal behavior stateless Co-authored-by: antonio --- crates/copilot/src/copilot.rs | 1 + crates/copilot/src/sign_in.rs | 550 ++++++++++++++++++++-------------- 2 files changed, 318 insertions(+), 233 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 3da2e1de71..61bb408de4 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -226,6 +226,7 @@ impl Copilot { if let CopilotServer::Started { server, status } = &mut self.server { let task = match status { SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { + cx.notify(); Task::ready(Ok(())).shared() } SignInStatus::SigningIn { task, .. } => { diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 6f32347aaa..fb31f9a8e8 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,7 +1,9 @@ -use crate::{request::PromptUserDeviceFlow, Copilot}; +use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View, - WindowKind, WindowOptions, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + ClipboardItem, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, WindowKind, + WindowOptions, }; use settings::Settings; @@ -13,158 +15,119 @@ struct OpenGithub; const _COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; -enum SignInContents { - PromptingUser(PromptUserDeviceFlow), - Unauthorized, - Enabled, -} - pub fn init(cx: &mut MutableAppContext) { let copilot = Copilot::global(cx).unwrap(); - let mut code_verification_window_id: Option<(usize, SignInContents)> = None; + let mut code_verification: Option> = None; cx.observe(&copilot, move |copilot, cx| { - match copilot.read(cx).status() { - crate::Status::SigningIn { - prompt: Some(prompt), - } => { - let window_id = match code_verification_window_id.take() { - Some((window_id, SignInContents::PromptingUser(current_prompt))) - if current_prompt == prompt => - { - if cx.window_ids().find(|item| item == &window_id).is_some() { - window_id - } else { - CopilotCodeVerification::prompting(prompt.clone(), cx) - } - } - Some((window_id, _)) => { - cx.remove_window(window_id); - CopilotCodeVerification::prompting(prompt.clone(), cx) - } - None => CopilotCodeVerification::prompting(prompt.clone(), cx), - }; + let status = copilot.read(cx).status(); - code_verification_window_id = - Some((window_id, SignInContents::PromptingUser(prompt))); - - cx.activate_window(window_id); + match &status { + crate::Status::SigningIn { prompt } => { + if let Some(code_verification) = code_verification.as_ref() { + code_verification.update(cx, |code_verification, cx| { + code_verification.set_status(status, cx) + }); + cx.activate_window(code_verification.window_id()); + } else if let Some(_prompt) = prompt { + let window_size = cx.global::().theme.copilot.modal.dimensions(); + let window_options = WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new( + Default::default(), + window_size, + )), + titlebar: None, + center: true, + focus: true, + kind: WindowKind::Normal, + is_movable: true, + screen: None, + }; + let (_, view) = + cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status)); + code_verification = Some(view); + } } - crate::Status::Authorized => match code_verification_window_id.take() { - Some((window_id, sign_in_contents)) => { - match sign_in_contents { - SignInContents::PromptingUser(_) => cx.remove_window(window_id), - SignInContents::Unauthorized => cx.remove_window(window_id), - SignInContents::Enabled => { - if cx.has_window(window_id) { - code_verification_window_id = - Some((window_id, SignInContents::Enabled)) - } - return; - } - } - let window_id = CopilotCodeVerification::enabled(cx); - code_verification_window_id = Some((window_id, SignInContents::Enabled)); - cx.activate_window(window_id); - } - None => return, - }, - crate::Status::Unauthorized => match code_verification_window_id.take() { - Some((window_id, sign_in_contents)) => { - match sign_in_contents { - SignInContents::PromptingUser(_) => cx.remove_window(window_id), // Show prompt - SignInContents::Unauthorized => { - if cx.has_window(window_id) { - code_verification_window_id = - Some((window_id, SignInContents::Unauthorized)) - } - return; - } //Do nothing - SignInContents::Enabled => cx.remove_window(window_id), // - } + Status::Authorized | Status::Unauthorized => { + if let Some(code_verification) = code_verification.as_ref() { + code_verification.update(cx, |code_verification, cx| { + code_verification.set_status(status, cx) + }); - let window_id = CopilotCodeVerification::unauthorized(cx); - code_verification_window_id = Some((window_id, SignInContents::Unauthorized)); - cx.activate_window(window_id); + cx.platform().activate(true); + cx.activate_window(code_verification.window_id()); } - None => return, - }, + } _ => { - if let Some((window_id, _)) = code_verification_window_id.take() { - cx.remove_window(window_id); + if let Some(code_verification) = code_verification.take() { + cx.remove_window(code_verification.window_id()); } } } }) .detach(); + + // Modal theming test: + // let window_size = cx.global::().theme.copilot.modal.dimensions(); + // let window_options = WindowOptions { + // bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), + // titlebar: None, + // center: false, + // focus: false, + // kind: WindowKind::PopUp, + // is_movable: true, + // screen: None, + // }; + // let (_, _view) = cx.add_window(window_options, |_cx| { + // CopilotCodeVerification::new(Status::SigningIn { + // prompt: Some(PromptUserDeviceFlow { + // user_code: "ABCD-1234".to_string(), + // verification_uri: "https://github.com/login/device".to_string(), + // }), + // }) + // }); + + // let window_size = cx.global::().theme.copilot.modal.dimensions(); + // let window_options = WindowOptions { + // bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(window_size.x(), 0.), window_size)), + // titlebar: None, + // center: false, + // focus: false, + // kind: WindowKind::PopUp, + // is_movable: true, + // screen: None, + // }; + // let (_, _view) = cx.add_window(window_options, |_cx| { + // CopilotCodeVerification::new(Status::Authorized) + // }); + + // let window_size = cx.global::().theme.copilot.modal.dimensions(); + // let window_options = WindowOptions { + // bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(0., window_size.y()), window_size)), + // titlebar: None, + // center: false, + // focus: false, + // kind: WindowKind::PopUp, + // is_movable: true, + // screen: None, + // }; + // let (_, _view) = cx.add_window(window_options, |_cx| { + // CopilotCodeVerification::new(Status::Unauthorized) + // }); } pub struct CopilotCodeVerification { - prompt: SignInContents, + status: Status, } impl CopilotCodeVerification { - pub fn prompting(prompt: PromptUserDeviceFlow, cx: &mut MutableAppContext) -> usize { - let window_size = cx.global::().theme.copilot.modal.dimensions(); - - let (window_id, _) = cx.add_window( - WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), - titlebar: None, - center: true, - focus: false, - kind: WindowKind::Normal, - is_movable: true, - screen: None, - }, - |_| CopilotCodeVerification { - prompt: SignInContents::PromptingUser(prompt), - }, - ); - - window_id + pub fn new(status: Status) -> Self { + Self { status } } - pub fn unauthorized(cx: &mut MutableAppContext) -> usize { - let window_size = cx.global::().theme.copilot.modal.dimensions(); - - let (window_id, _) = cx.add_window( - WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), - titlebar: None, - center: true, - focus: false, - kind: WindowKind::Normal, - is_movable: true, - screen: None, - }, - |_| CopilotCodeVerification { - prompt: SignInContents::Unauthorized, - }, - ); - - window_id - } - - pub fn enabled(cx: &mut MutableAppContext) -> usize { - let window_size = cx.global::().theme.copilot.modal.dimensions(); - - let (window_id, _) = cx.add_window( - WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), - titlebar: None, - center: true, - focus: false, - kind: WindowKind::Normal, - is_movable: true, - screen: None, - }, - |_| CopilotCodeVerification { - prompt: SignInContents::Enabled, - }, - ); - - window_id + pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { + self.status = status; + cx.notify(); } fn render_device_code( @@ -323,6 +286,219 @@ impl CopilotCodeVerification { .with_style(style.auth.enabled_hint) .boxed() } + + fn render_prompting_modal( + data: &PromptUserDeviceFlow, + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { + theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| { + Flex::column() + .with_children([ + Flex::column() + .with_children([ + Flex::row() + .with_children([ + theme::ui::svg(&style.auth.copilot_icon).boxed(), + theme::ui::icon(&style.auth.plus_icon).boxed(), + theme::ui::svg(&style.auth.zed_icon).boxed(), + ]) + .boxed(), + Flex::column() + .with_children([ + Label::new( + "Enable Copilot by connecting", + style.auth.enable_text.clone(), + ) + .boxed(), + Label::new( + "your existing license.", + style.auth.enable_text.clone(), + ) + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.enable_group.clone()) + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.header_group) + .aligned() + .boxed(), + Self::render_device_code(data, &style, cx), + // match &self.prompt { + // SignInContents::PromptingUser(data) => { + + // } + // SignInContents::Unauthorized => Self::render_not_authorized_warning(&style), + // SignInContents::Enabled => Self::render_copilot_enabled(&style), + // }, + Flex::column() + .with_child( + theme::ui::cta_button_with_click( + "Connect to GitHub", + style.auth.content_width, + &style.auth.cta_button, + cx, + { + let verification_uri = data.verification_uri.clone(); + move |_, cx| cx.platform().open_url(&verification_uri) + }, + ), + // { + // match &self.prompt { + // SignInContents::PromptingUser(data) => { + + // } + // // SignInContents::Unauthorized => theme::ui::cta_button_with_click( + // // "Close", + // // style.auth.content_width, + // // &style.auth.cta_button, + // // cx, + // // |_, cx| { + // // let window_id = cx.window_id(); + // // cx.remove_window(window_id) + // // }, + // // ), + // // SignInContents::Enabled => theme::ui::cta_button_with_click( + // // "Done", + // // style.auth.content_width, + // // &style.auth.cta_button, + // // cx, + // // |_, cx| { + // // let window_id = cx.window_id(); + // // cx.remove_window(window_id) + // // }, + // // ), + // } + ) + .align_children_center() + .contained() + .with_style(style.auth.github_group) + .aligned() + .boxed(), + ]) + .align_children_center() + .constrained() + .with_width(style.auth.content_width) + .aligned() + .boxed() + }) + } + fn render_enabled_modal( + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { + theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| { + Flex::column() + .with_children([ + Flex::column() + .with_children([ + Flex::row() + .with_children([ + theme::ui::svg(&style.auth.copilot_icon).boxed(), + theme::ui::icon(&style.auth.plus_icon).boxed(), + theme::ui::svg(&style.auth.zed_icon).boxed(), + ]) + .boxed(), + Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.header_group) + .aligned() + .boxed(), + Self::render_copilot_enabled(&style), + Flex::column() + .with_child(theme::ui::cta_button_with_click( + "Close", + style.auth.content_width, + &style.auth.cta_button, + cx, + |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id) + }, + )) + .align_children_center() + .contained() + .with_style(style.auth.github_group) + .aligned() + .boxed(), + ]) + .align_children_center() + .constrained() + .with_width(style.auth.content_width) + .aligned() + .boxed() + }) + } + fn render_unauthorized_modal( + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { + theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| { + Flex::column() + .with_children([ + Flex::column() + .with_children([ + Flex::row() + .with_children([ + theme::ui::svg(&style.auth.copilot_icon).boxed(), + theme::ui::icon(&style.auth.plus_icon).boxed(), + theme::ui::svg(&style.auth.zed_icon).boxed(), + ]) + .boxed(), + Flex::column() + .with_children([ + Label::new( + "Enable Copilot by connecting", + style.auth.enable_text.clone(), + ) + .boxed(), + Label::new( + "your existing license.", + style.auth.enable_text.clone(), + ) + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.enable_group.clone()) + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.header_group) + .aligned() + .boxed(), + Self::render_not_authorized_warning(&style), + Flex::column() + .with_child(theme::ui::cta_button_with_click( + "Close", + style.auth.content_width, + &style.auth.cta_button, + cx, + |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id) + }, + )) + .align_children_center() + .contained() + .with_style(style.auth.github_group) + .aligned() + .boxed(), + ]) + .align_children_center() + .constrained() + .with_width(style.auth.content_width) + .aligned() + .boxed() + }) + } } impl Entity for CopilotCodeVerification { @@ -344,105 +520,13 @@ impl View for CopilotCodeVerification { fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { let style = cx.global::().theme.copilot.clone(); - - theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| { - Flex::column() - .with_children([ - Flex::column() - .with_children([ - Flex::row() - .with_children([ - theme::ui::svg(&style.auth.copilot_icon).boxed(), - theme::ui::icon(&style.auth.plus_icon).boxed(), - theme::ui::svg(&style.auth.zed_icon).boxed(), - ]) - .boxed(), - match self.prompt { - SignInContents::PromptingUser(_) | SignInContents::Unauthorized => { - Flex::column() - .with_children([ - Label::new( - "Enable Copilot by connecting", - style.auth.enable_text.clone(), - ) - .boxed(), - Label::new( - "your existing license.", - style.auth.enable_text.clone(), - ) - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.enable_group.clone()) - .boxed() - } - SignInContents::Enabled => { - Label::new("Copilot Enabled!", style.auth.enable_text.clone()) - .boxed() - } - }, - ]) - .align_children_center() - .contained() - .with_style(style.auth.header_group) - .aligned() - .boxed(), - match &self.prompt { - SignInContents::PromptingUser(data) => { - Self::render_device_code(data, &style, cx) - } - SignInContents::Unauthorized => Self::render_not_authorized_warning(&style), - SignInContents::Enabled => Self::render_copilot_enabled(&style), - }, - Flex::column() - .with_child({ - match &self.prompt { - SignInContents::PromptingUser(data) => { - theme::ui::cta_button_with_click( - "Connect to GitHub", - style.auth.content_width, - &style.auth.cta_button, - cx, - { - let verification_uri = data.verification_uri.clone(); - move |_, cx| cx.platform().open_url(&verification_uri) - }, - ) - } - SignInContents::Unauthorized => theme::ui::cta_button_with_click( - "Close", - style.auth.content_width, - &style.auth.cta_button, - cx, - |_, cx| { - let window_id = cx.window_id(); - cx.remove_window(window_id) - }, - ), - SignInContents::Enabled => theme::ui::cta_button_with_click( - "Done", - style.auth.content_width, - &style.auth.cta_button, - cx, - |_, cx| { - let window_id = cx.window_id(); - cx.remove_window(window_id) - }, - ), - } - }) - .align_children_center() - .contained() - .with_style(style.auth.github_group) - .aligned() - .boxed(), - ]) - .align_children_center() - .constrained() - .with_width(style.auth.content_width) - .aligned() - .boxed() - }) + match &self.status { + Status::SigningIn { + prompt: Some(prompt), + } => Self::render_prompting_modal(&prompt, &style, cx), + Status::Unauthorized => Self::render_unauthorized_modal(&style, cx), + Status::Authorized => Self::render_enabled_modal(&style, cx), + _ => Empty::new().boxed(), + } } } From ebd06b43f5deda4b4a09d11514e0baac4c0f9673 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Mar 2023 16:40:52 -0700 Subject: [PATCH 36/53] Add copilot settings Add copilot pause / resume Add copilot statusbar item stub --- Cargo.lock | 1 + assets/keymaps/default.json | 4 +- assets/settings/default.json | 18 ++- crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 76 ++++++------ crates/copilot/src/copilot_button.rs | 172 +++++++++++++++++++++++++++ crates/copilot/src/sign_in.rs | 6 +- crates/editor/src/editor.rs | 96 ++++++++++++++- crates/settings/src/settings.rs | 45 ++++++- 9 files changed, 368 insertions(+), 51 deletions(-) create mode 100644 crates/copilot/src/copilot_button.rs diff --git a/Cargo.lock b/Cargo.lock index ade31e169e..36c32924c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1339,6 +1339,7 @@ dependencies = [ "async-compression", "client", "collections", + "context_menu", "futures 0.3.25", "gpui", "language", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 03e24c8bc3..1a8350bb53 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -177,7 +177,9 @@ "focus": false } ], - "alt-]": "copilot::NextSuggestion" + "alt-]": "copilot::NextSuggestion", + "alt-[": "copilot::PreviousSuggestion", + "alt-\\": "copilot::Toggle" } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 7b775d6309..fbb52e00dc 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -13,6 +13,11 @@ // The factor to grow the active pane by. Defaults to 1.0 // which gives the same size as all other panes. "active_pane_magnification": 1.0, + // Enable / disable copilot integration. + "enable_copilot_integration": true, + // Controls whether copilot provides suggestion immediately + // or waits for a `copilot::Toggle` + "copilot": "on", // Whether to enable vim modes and key bindings "vim_mode": false, // Whether to show the informational hover box when moving the mouse @@ -120,7 +125,7 @@ // Settings specific to the terminal "terminal": { // What shell to use when opening a terminal. May take 3 values: - // 1. Use the system's default terminal configuration (e.g. $TERM). + // 1. Use the system's default terminal configuration in /etc/passwd // "shell": "system" // 2. A program: // "shell": { @@ -200,7 +205,9 @@ // Different settings for specific languages. "languages": { "Plain Text": { - "soft_wrap": "preferred_line_length" + "soft_wrap": "preferred_line_length", + // Copilot can be a little strange on non-code files + "copilot": "off" }, "Elixir": { "tab_size": 2 @@ -210,7 +217,9 @@ "hard_tabs": true }, "Markdown": { - "soft_wrap": "preferred_line_length" + "soft_wrap": "preferred_line_length", + // Copilot can be a little strange on non-code files + "copilot": "off" }, "JavaScript": { "tab_size": 2 @@ -223,6 +232,9 @@ }, "YAML": { "tab_size": 2 + }, + "JSON": { + "copilot": "off" } }, // LSP Specific settings. diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index f39ab604e2..47f49f9910 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] collections = { path = "../collections" } +context_menu = { path = "../context_menu" } gpui = { path = "../gpui" } language = { path = "../language" } settings = { path = "../settings" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 61bb408de4..efa693278e 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1,3 +1,4 @@ +pub mod copilot_button; mod request; mod sign_in; @@ -24,7 +25,7 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; actions!(copilot_auth, [SignIn, SignOut]); const COPILOT_NAMESPACE: &'static str = "copilot"; -actions!(copilot, [NextSuggestion]); +actions!(copilot, [NextSuggestion, PreviousSuggestion, Toggle]); pub fn init(client: Arc, node_runtime: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx)); @@ -67,9 +68,11 @@ pub fn init(client: Arc, node_runtime: Arc, cx: &mut Mutabl } enum CopilotServer { - Downloading, - Error(Arc), Disabled, + Starting { + _task: Shared>, + }, + Error(Arc), Started { server: Arc, status: SignInStatus, @@ -93,7 +96,7 @@ enum SignInStatus { #[derive(Debug, PartialEq, Eq)] pub enum Status { - Downloading, + Starting, Error(Arc), Disabled, SignedOut, @@ -138,45 +141,46 @@ impl Copilot { node_runtime: Arc, cx: &mut ModelContext, ) -> Self { - // TODO: Make this task resilient to users thrashing the copilot setting cx.observe_global::({ let http = http.clone(); let node_runtime = node_runtime.clone(); move |this, cx| { - if cx.global::().copilot.as_bool() { + if cx.global::().enable_copilot_integration { if matches!(this.server, CopilotServer::Disabled) { - cx.spawn({ - let http = http.clone(); - let node_runtime = node_runtime.clone(); - move |this, cx| { - Self::start_language_server(http, node_runtime, this, cx) - } - }) - .detach(); + let start_task = cx + .spawn({ + let http = http.clone(); + let node_runtime = node_runtime.clone(); + move |this, cx| { + Self::start_language_server(http, node_runtime, this, cx) + } + }) + .shared(); + this.server = CopilotServer::Starting { _task: start_task } } } else { - // TODO: What else needs to be turned off here? this.server = CopilotServer::Disabled } } }) .detach(); - if !cx.global::().copilot.as_bool() { - return Self { + if cx.global::().enable_copilot_integration { + let start_task = cx + .spawn({ + let http = http.clone(); + let node_runtime = node_runtime.clone(); + move |this, cx| Self::start_language_server(http, node_runtime, this, cx) + }) + .shared(); + + Self { + server: CopilotServer::Starting { _task: start_task }, + } + } else { + Self { server: CopilotServer::Disabled, - }; - } - - cx.spawn({ - let http = http.clone(); - let node_runtime = node_runtime.clone(); - move |this, cx| Self::start_language_server(http, node_runtime, this, cx) - }) - .detach(); - - Self { - server: CopilotServer::Downloading, + } } } @@ -216,6 +220,7 @@ impl Copilot { } Err(error) => { this.server = CopilotServer::Error(error.to_string().into()); + cx.notify() } } }) @@ -226,11 +231,10 @@ impl Copilot { if let CopilotServer::Started { server, status } = &mut self.server { let task = match status { SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { - cx.notify(); Task::ready(Ok(())).shared() } SignInStatus::SigningIn { task, .. } => { - cx.notify(); // To re-show the prompt, just in case. + cx.notify(); task.clone() } SignInStatus::SignedOut => { @@ -382,7 +386,7 @@ impl Copilot { pub fn status(&self) -> Status { match &self.server { - CopilotServer::Downloading => Status::Downloading, + CopilotServer::Starting { .. } => Status::Starting, CopilotServer::Disabled => Status::Disabled, CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Started { status, .. } => match status { @@ -403,13 +407,15 @@ impl Copilot { ) { if let CopilotServer::Started { status, .. } = &mut self.server { *status = match lsp_status { - request::SignInStatus::Ok { user } | request::SignInStatus::MaybeOk { user } => { + request::SignInStatus::Ok { user } + | request::SignInStatus::MaybeOk { user } + | request::SignInStatus::AlreadySignedIn { user } => { SignInStatus::Authorized { _user: user } } request::SignInStatus::NotAuthorized { user } => { SignInStatus::Unauthorized { _user: user } } - _ => SignInStatus::SignedOut, + request::SignInStatus::NotSignedIn => SignInStatus::SignedOut, }; cx.notify(); } @@ -417,7 +423,7 @@ impl Copilot { fn authorized_server(&self) -> Result> { match &self.server { - CopilotServer::Downloading => Err(anyhow!("copilot is still downloading")), + CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")), CopilotServer::Disabled => Err(anyhow!("copilot is disabled")), CopilotServer::Error(error) => Err(anyhow!( "copilot was not started because of an error: {}", diff --git a/crates/copilot/src/copilot_button.rs b/crates/copilot/src/copilot_button.rs new file mode 100644 index 0000000000..9c8a8c4d6e --- /dev/null +++ b/crates/copilot/src/copilot_button.rs @@ -0,0 +1,172 @@ +// use context_menu::{ContextMenu, ContextMenuItem}; +// use gpui::{ +// elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton, +// MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakModelHandle, +// WeakViewHandle, +// }; +// use settings::Settings; +// use std::any::TypeId; +// use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace}; + +// #[derive(Clone, PartialEq)] +// pub struct DeployTerminalMenu; + +// impl_internal_actions!(terminal, [DeployTerminalMenu]); + +// pub fn init(cx: &mut MutableAppContext) { +// cx.add_action(CopilotButton::deploy_terminal_menu); +// } + +// pub struct CopilotButton { +// workspace: WeakViewHandle, +// popup_menu: ViewHandle, +// } + +// impl Entity for CopilotButton { +// type Event = (); +// } + +// impl View for CopilotButton { +// fn ui_name() -> &'static str { +// "TerminalButton" +// } + +// fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { +// let workspace = self.workspace.upgrade(cx); +// let project = match workspace { +// Some(workspace) => workspace.read(cx).project().read(cx), +// None => return Empty::new().boxed(), +// }; + +// let focused_view = cx.focused_view_id(cx.window_id()); +// let active = focused_view +// .map(|view_id| { +// cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::()) +// }) +// .unwrap_or(false); + +// let has_terminals = !project.local_terminal_handles().is_empty(); +// let terminal_count = project.local_terminal_handles().len() as i32; +// let theme = cx.global::().theme.clone(); + +// Stack::new() +// .with_child( +// MouseEventHandler::::new(0, cx, { +// let theme = theme.clone(); +// move |state, _cx| { +// let style = theme +// .workspace +// .status_bar +// .sidebar_buttons +// .item +// .style_for(state, active); + +// Flex::row() +// .with_child( +// Svg::new("icons/terminal_12.svg") +// .with_color(style.icon_color) +// .constrained() +// .with_width(style.icon_size) +// .aligned() +// .named("terminals-icon"), +// ) +// .with_children(has_terminals.then(|| { +// Label::new(terminal_count.to_string(), style.label.text.clone()) +// .contained() +// .with_style(style.label.container) +// .aligned() +// .boxed() +// })) +// .constrained() +// .with_height(style.icon_size) +// .contained() +// .with_style(style.container) +// .boxed() +// } +// }) +// .with_cursor_style(CursorStyle::PointingHand) +// .on_click(MouseButton::Left, move |_, cx| { +// if has_terminals { +// cx.dispatch_action(DeployTerminalMenu); +// } else { +// if !active { +// cx.dispatch_action(FocusDock); +// } +// }; +// }) +// .with_tooltip::( +// 0, +// "Show Terminal".into(), +// Some(Box::new(FocusDock)), +// theme.tooltip.clone(), +// cx, +// ) +// .boxed(), +// ) +// .with_child( +// ChildView::new(&self.popup_menu, cx) +// .aligned() +// .top() +// .right() +// .boxed(), +// ) +// .boxed() +// } +// } + +// impl CopilotButton { +// pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { +// cx.observe(&workspace, |_, _, cx| cx.notify()).detach(); +// Self { +// workspace: workspace.downgrade(), +// popup_menu: cx.add_view(|cx| { +// let mut menu = ContextMenu::new(cx); +// menu.set_position_mode(OverlayPositionMode::Local); +// menu +// }), +// } +// } + +// pub fn deploy_terminal_menu( +// &mut self, +// _action: &DeployTerminalMenu, +// cx: &mut ViewContext, +// ) { +// let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)]; + +// if let Some(workspace) = self.workspace.upgrade(cx) { +// let project = workspace.read(cx).project().read(cx); +// let local_terminal_handles = project.local_terminal_handles(); + +// if !local_terminal_handles.is_empty() { +// menu_options.push(ContextMenuItem::Separator) +// } + +// for local_terminal_handle in local_terminal_handles { +// if let Some(terminal) = local_terminal_handle.upgrade(cx) { +// menu_options.push(ContextMenuItem::item( +// terminal.read(cx).title(), +// // FocusTerminal { +// // terminal_handle: local_terminal_handle.clone(), +// // }, +// )) +// } +// } +// } + +// self.popup_menu.update(cx, |menu, cx| { +// menu.show( +// Default::default(), +// AnchorCorner::BottomRight, +// menu_options, +// cx, +// ); +// }); +// } +// } + +// impl StatusItemView for CopilotButton { +// fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext) { +// cx.notify(); +// } +// } diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index fb31f9a8e8..0a9299f512 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,9 +1,7 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - elements::*, - geometry::{rect::RectF, vector::vec2f}, - ClipboardItem, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, WindowKind, - WindowOptions, + elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View, + ViewContext, ViewHandle, WindowKind, WindowOptions, }; use settings::Settings; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ccd090c409..4dbbf66a84 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -390,6 +390,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::confirm_rename); cx.add_async_action(Editor::find_all_references); cx.add_action(Editor::next_copilot_suggestion); + cx.add_action(Editor::previous_copilot_suggestion); + cx.add_action(Editor::toggle_copilot_suggestions); hover_popover::init(cx); link_go_to_definition::init(cx); @@ -1011,6 +1013,7 @@ struct CopilotState { pending_refresh: Task>, completions: Vec, active_completion_index: usize, + user_enabled: Option, } impl Default for CopilotState { @@ -1020,6 +1023,7 @@ impl Default for CopilotState { pending_refresh: Task::ready(Some(())), completions: Default::default(), active_completion_index: 0, + user_enabled: None, } } } @@ -2745,12 +2749,40 @@ impl Editor { fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext) -> Option<()> { let copilot = Copilot::global(cx)?; + if self.mode != EditorMode::Full { return None; } + let settings = cx.global::(); + + dbg!(self.copilot_state.user_enabled); + + if !self + .copilot_state + .user_enabled + .unwrap_or_else(|| settings.copilot_on(None)) + { + return None; + } + let snapshot = self.buffer.read(cx).snapshot(cx); let selection = self.selections.newest_anchor(); + + if !self.copilot_state.user_enabled.is_some() { + let language_name = snapshot + .language_at(selection.start) + .map(|language| language.name()); + + let copilot_enabled = settings.copilot_on(language_name.as_deref()); + + dbg!(language_name, copilot_enabled); + + if !copilot_enabled { + return None; + } + } + let cursor = if selection.start == selection.end { selection.start.bias_left(&snapshot) } else { @@ -2829,16 +2861,76 @@ impl Editor { } fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { + // Auto re-enable copilot if you're asking for a suggestion + if self.copilot_state.user_enabled == Some(false) { + self.copilot_state.user_enabled = Some(true); + } + if self.copilot_state.completions.is_empty() { self.refresh_copilot_suggestions(cx); return; } + self.copilot_state.active_completion_index = + (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len(); + + self.sync_suggestion(cx); + } + + fn previous_copilot_suggestion( + &mut self, + _: &copilot::PreviousSuggestion, + cx: &mut ViewContext, + ) { + // Auto re-enable copilot if you're asking for a suggestion + if self.copilot_state.user_enabled == Some(false) { + self.copilot_state.user_enabled = Some(true); + } + + if self.copilot_state.completions.is_empty() { + self.refresh_copilot_suggestions(cx); + return; + } + + self.copilot_state.active_completion_index = + if self.copilot_state.active_completion_index == 0 { + self.copilot_state.completions.len() - 1 + } else { + self.copilot_state.active_completion_index - 1 + }; + + self.sync_suggestion(cx); + } + + fn toggle_copilot_suggestions(&mut self, _: &copilot::Toggle, cx: &mut ViewContext) { + self.copilot_state.user_enabled = match self.copilot_state.user_enabled { + Some(enabled) => Some(!enabled), + None => { + let selection = self.selections.newest_anchor().start; + + let language_name = self + .snapshot(cx) + .language_at(selection) + .map(|language| language.name()); + + let copilot_enabled = cx.global::().copilot_on(language_name.as_deref()); + + Some(!copilot_enabled) + } + }; + + // We know this can't be None, as we just set it to Some above + if self.copilot_state.user_enabled == Some(true) { + self.refresh_copilot_suggestions(cx); + } else { + self.clear_copilot_suggestions(cx); + } + } + + fn sync_suggestion(&mut self, cx: &mut ViewContext) { let snapshot = self.buffer.read(cx).snapshot(cx); let cursor = self.selections.newest_anchor().head(); - self.copilot_state.active_completion_index = - (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len(); if let Some(text) = self .copilot_state .text_for_active_completion(cursor, &snapshot) diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index f56364cfa8..6688b3c4d4 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -32,6 +32,7 @@ pub struct Settings { pub buffer_font_features: fonts::Features, pub buffer_font_family: FamilyId, pub default_buffer_font_size: f32, + pub enable_copilot_integration: bool, pub buffer_font_size: f32, pub active_pane_magnification: f32, pub cursor_blink: bool, @@ -58,10 +59,10 @@ pub struct Settings { pub telemetry_overrides: TelemetrySettings, pub auto_update: bool, pub base_keymap: BaseKeymap, - pub copilot: CopilotSettings, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] pub enum CopilotSettings { #[default] On, @@ -78,7 +79,7 @@ impl From for bool { } impl CopilotSettings { - pub fn as_bool(&self) -> bool { + pub fn is_on(&self) -> bool { >::into(*self) } } @@ -176,6 +177,29 @@ pub struct EditorSettings { pub ensure_final_newline_on_save: Option, pub formatter: Option, pub enable_language_server: Option, + pub copilot: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum OnOff { + On, + Off, +} + +impl OnOff { + fn as_bool(&self) -> bool { + match self { + OnOff::On => true, + OnOff::Off => false, + } + } +} + +impl Into for OnOff { + fn into(self) -> bool { + self.as_bool() + } } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -399,7 +423,7 @@ pub struct SettingsFileContent { #[serde(default)] pub base_keymap: Option, #[serde(default)] - pub copilot: Option, + pub enable_copilot_integration: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -461,6 +485,7 @@ impl Settings { format_on_save: required(defaults.editor.format_on_save), formatter: required(defaults.editor.formatter), enable_language_server: required(defaults.editor.enable_language_server), + copilot: required(defaults.editor.copilot), }, editor_overrides: Default::default(), git: defaults.git.unwrap(), @@ -477,7 +502,7 @@ impl Settings { telemetry_overrides: Default::default(), auto_update: defaults.auto_update.unwrap(), base_keymap: Default::default(), - copilot: Default::default(), + enable_copilot_integration: defaults.enable_copilot_integration.unwrap(), } } @@ -529,7 +554,6 @@ impl Settings { merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.base_keymap, data.base_keymap); - merge(&mut self.copilot, data.copilot); self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); @@ -553,6 +577,14 @@ impl Settings { self } + pub fn copilot_on(&self, language: Option<&str>) -> bool { + if self.enable_copilot_integration { + self.language_setting(language, |settings| settings.copilot.map(Into::into)) + } else { + false + } + } + pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 { self.language_setting(language, |settings| settings.tab_size) } @@ -689,6 +721,7 @@ impl Settings { format_on_save: Some(FormatOnSave::On), formatter: Some(Formatter::LanguageServer), enable_language_server: Some(true), + copilot: Some(OnOff::On), }, editor_overrides: Default::default(), journal_defaults: Default::default(), @@ -708,7 +741,7 @@ impl Settings { telemetry_overrides: Default::default(), auto_update: true, base_keymap: Default::default(), - copilot: Default::default(), + enable_copilot_integration: true, } } From e3822a5b5c7bb7a2abce1e05aecaba19239550b2 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 29 Mar 2023 20:23:32 -0400 Subject: [PATCH 37/53] Add copilot icons --- assets/icons/copilot_16.svg | 12 ++++++++++++ assets/icons/copilot_disabled_16.svg | 10 ++++++++++ assets/icons/copilot_init_16.svg | 4 ++++ 3 files changed, 26 insertions(+) create mode 100644 assets/icons/copilot_16.svg create mode 100644 assets/icons/copilot_disabled_16.svg create mode 100644 assets/icons/copilot_init_16.svg diff --git a/assets/icons/copilot_16.svg b/assets/icons/copilot_16.svg new file mode 100644 index 0000000000..35e84a6d7a --- /dev/null +++ b/assets/icons/copilot_16.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/copilot_disabled_16.svg b/assets/icons/copilot_disabled_16.svg new file mode 100644 index 0000000000..91bd009508 --- /dev/null +++ b/assets/icons/copilot_disabled_16.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/copilot_init_16.svg b/assets/icons/copilot_init_16.svg new file mode 100644 index 0000000000..68478e62aa --- /dev/null +++ b/assets/icons/copilot_init_16.svg @@ -0,0 +1,4 @@ + + + + From 76efab005ffd7d90645c511a8705cbb0ae0010a4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Mar 2023 17:25:27 -0700 Subject: [PATCH 38/53] WIP --- crates/copilot/src/copilot_button.rs | 268 +++++++++++---------------- crates/editor/src/editor.rs | 4 - crates/zed/src/zed.rs | 3 + 3 files changed, 114 insertions(+), 161 deletions(-) diff --git a/crates/copilot/src/copilot_button.rs b/crates/copilot/src/copilot_button.rs index 9c8a8c4d6e..b563d7e34e 100644 --- a/crates/copilot/src/copilot_button.rs +++ b/crates/copilot/src/copilot_button.rs @@ -1,172 +1,126 @@ -// use context_menu::{ContextMenu, ContextMenuItem}; -// use gpui::{ -// elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton, -// MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakModelHandle, -// WeakViewHandle, -// }; -// use settings::Settings; -// use std::any::TypeId; -// use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace}; +use context_menu::{ContextMenu, ContextMenuItem}; +use gpui::{ + elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, +}; +use settings::Settings; +use workspace::{item::ItemHandle, NewTerminal, StatusItemView}; -// #[derive(Clone, PartialEq)] -// pub struct DeployTerminalMenu; +const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; -// impl_internal_actions!(terminal, [DeployTerminalMenu]); +#[derive(Clone, PartialEq)] +pub struct DeployCopilotMenu; -// pub fn init(cx: &mut MutableAppContext) { -// cx.add_action(CopilotButton::deploy_terminal_menu); -// } +impl_internal_actions!(copilot, [DeployCopilotMenu]); -// pub struct CopilotButton { -// workspace: WeakViewHandle, -// popup_menu: ViewHandle, -// } +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(CopilotButton::deploy_copilot_menu); +} -// impl Entity for CopilotButton { -// type Event = (); -// } +pub struct CopilotButton { + popup_menu: ViewHandle, +} -// impl View for CopilotButton { -// fn ui_name() -> &'static str { -// "TerminalButton" -// } +impl Entity for CopilotButton { + type Event = (); +} -// fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { -// let workspace = self.workspace.upgrade(cx); -// let project = match workspace { -// Some(workspace) => workspace.read(cx).project().read(cx), -// None => return Empty::new().boxed(), -// }; +impl View for CopilotButton { + fn ui_name() -> &'static str { + "CopilotButton" + } -// let focused_view = cx.focused_view_id(cx.window_id()); -// let active = focused_view -// .map(|view_id| { -// cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::()) -// }) -// .unwrap_or(false); + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + let theme = cx.global::().theme.clone(); -// let has_terminals = !project.local_terminal_handles().is_empty(); -// let terminal_count = project.local_terminal_handles().len() as i32; -// let theme = cx.global::().theme.clone(); + let visible = self.popup_menu.read(cx).visible(); -// Stack::new() -// .with_child( -// MouseEventHandler::::new(0, cx, { -// let theme = theme.clone(); -// move |state, _cx| { -// let style = theme -// .workspace -// .status_bar -// .sidebar_buttons -// .item -// .style_for(state, active); + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, { + let theme = theme.clone(); + move |state, _cx| { + let style = theme + .workspace + .status_bar + .sidebar_buttons + .item + .style_for(state, visible); -// Flex::row() -// .with_child( -// Svg::new("icons/terminal_12.svg") -// .with_color(style.icon_color) -// .constrained() -// .with_width(style.icon_size) -// .aligned() -// .named("terminals-icon"), -// ) -// .with_children(has_terminals.then(|| { -// Label::new(terminal_count.to_string(), style.label.text.clone()) -// .contained() -// .with_style(style.label.container) -// .aligned() -// .boxed() -// })) -// .constrained() -// .with_height(style.icon_size) -// .contained() -// .with_style(style.container) -// .boxed() -// } -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, cx| { -// if has_terminals { -// cx.dispatch_action(DeployTerminalMenu); -// } else { -// if !active { -// cx.dispatch_action(FocusDock); -// } -// }; -// }) -// .with_tooltip::( -// 0, -// "Show Terminal".into(), -// Some(Box::new(FocusDock)), -// theme.tooltip.clone(), -// cx, -// ) -// .boxed(), -// ) -// .with_child( -// ChildView::new(&self.popup_menu, cx) -// .aligned() -// .top() -// .right() -// .boxed(), -// ) -// .boxed() -// } -// } + Flex::row() + .with_child( + Svg::new("icons/maybe_copilot.svg") + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) + .aligned() + .named("copilot-icon"), + ) + .constrained() + .with_height(style.icon_size) + .contained() + .with_style(style.container) + .boxed() + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, _cx| { + // TODO: Behavior of this + // if has_terminals { + // cx.dispatch_action(DeployCopilotMenu); + // } else { + // if !active { + // cx.dispatch_action(FocusDock); + // } + // }; + }) + .with_tooltip::( + 0, + "GitHub Copilot".into(), + None, + theme.tooltip.clone(), + cx, + ) + .boxed(), + ) + .with_child( + ChildView::new(&self.popup_menu, cx) + .aligned() + .top() + .right() + .boxed(), + ) + .boxed() + } +} -// impl CopilotButton { -// pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { -// cx.observe(&workspace, |_, _, cx| cx.notify()).detach(); -// Self { -// workspace: workspace.downgrade(), -// popup_menu: cx.add_view(|cx| { -// let mut menu = ContextMenu::new(cx); -// menu.set_position_mode(OverlayPositionMode::Local); -// menu -// }), -// } -// } +impl CopilotButton { + pub fn new(cx: &mut ViewContext) -> Self { + Self { + popup_menu: cx.add_view(|cx| { + let mut menu = ContextMenu::new(cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }), + } + } -// pub fn deploy_terminal_menu( -// &mut self, -// _action: &DeployTerminalMenu, -// cx: &mut ViewContext, -// ) { -// let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)]; + pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext) { + let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)]; -// if let Some(workspace) = self.workspace.upgrade(cx) { -// let project = workspace.read(cx).project().read(cx); -// let local_terminal_handles = project.local_terminal_handles(); + self.popup_menu.update(cx, |menu, cx| { + menu.show( + Default::default(), + AnchorCorner::BottomRight, + menu_options, + cx, + ); + }); + } +} -// if !local_terminal_handles.is_empty() { -// menu_options.push(ContextMenuItem::Separator) -// } - -// for local_terminal_handle in local_terminal_handles { -// if let Some(terminal) = local_terminal_handle.upgrade(cx) { -// menu_options.push(ContextMenuItem::item( -// terminal.read(cx).title(), -// // FocusTerminal { -// // terminal_handle: local_terminal_handle.clone(), -// // }, -// )) -// } -// } -// } - -// self.popup_menu.update(cx, |menu, cx| { -// menu.show( -// Default::default(), -// AnchorCorner::BottomRight, -// menu_options, -// cx, -// ); -// }); -// } -// } - -// impl StatusItemView for CopilotButton { -// fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext) { -// cx.notify(); -// } -// } +impl StatusItemView for CopilotButton { + fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + cx.notify(); + } +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4dbbf66a84..f8f83dc101 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2756,8 +2756,6 @@ impl Editor { let settings = cx.global::(); - dbg!(self.copilot_state.user_enabled); - if !self .copilot_state .user_enabled @@ -2776,8 +2774,6 @@ impl Editor { let copilot_enabled = settings.copilot_on(language_name.as_deref()); - dbg!(language_name, copilot_enabled); - if !copilot_enabled { return None; } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 43172d07fa..d9c91225c2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -8,6 +8,7 @@ use breadcrumbs::Breadcrumbs; pub use client; use collab_ui::{CollabTitlebarItem, ToggleContactsMenu}; use collections::VecDeque; +use copilot::copilot_button::CopilotButton; pub use editor; use editor::{Editor, MultiBuffer}; @@ -311,6 +312,7 @@ pub fn initialize_workspace( }); let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx)); + let copilot = cx.add_view(|cx| CopilotButton::new(cx)); let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); let activity_indicator = @@ -324,6 +326,7 @@ pub fn initialize_workspace( status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(toggle_terminal, cx); status_bar.add_right_item(feedback_button, cx); + status_bar.add_right_item(copilot, cx); status_bar.add_right_item(active_buffer_language, cx); status_bar.add_right_item(cursor_position, cx); }); From 8fac32e1ebe1f536e9daddd89e500e6b39ebbc2d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Mar 2023 17:59:21 -0700 Subject: [PATCH 39/53] WIP, not compiling --- crates/copilot/src/copilot_button.rs | 64 +++++++++++++++++++--------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/crates/copilot/src/copilot_button.rs b/crates/copilot/src/copilot_button.rs index b563d7e34e..fdc5dc776f 100644 --- a/crates/copilot/src/copilot_button.rs +++ b/crates/copilot/src/copilot_button.rs @@ -1,17 +1,24 @@ use context_menu::{ContextMenu, ContextMenuItem}; use gpui::{ elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton, - MutableAppContext, RenderContext, View, ViewContext, ViewHandle, + MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; +use theme::Editor; use workspace::{item::ItemHandle, NewTerminal, StatusItemView}; +use crate::{Copilot, Status}; + const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; #[derive(Clone, PartialEq)] pub struct DeployCopilotMenu; -impl_internal_actions!(copilot, [DeployCopilotMenu]); +// TODO: Make the other code path use `get_or_insert` logic for this modal +#[derive(Clone, PartialEq)] +pub struct DeployCopilotModal; + +impl_internal_actions!(copilot, [DeployCopilotMenu, DeployCopilotModal]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(CopilotButton::deploy_copilot_menu); @@ -19,6 +26,7 @@ pub fn init(cx: &mut MutableAppContext) { pub struct CopilotButton { popup_menu: ViewHandle, + editor: Option>, } impl Entity for CopilotButton { @@ -31,9 +39,16 @@ impl View for CopilotButton { } fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { - let theme = cx.global::().theme.clone(); + let settings = cx.global::(); - let visible = self.popup_menu.read(cx).visible(); + if !settings.enable_copilot_integration { + return Empty::new().boxed(); + } + + let theme = settings.theme.clone(); + let active = self.popup_menu.read(cx).visible() /* || modal.is_shown */; + let authorized = Copilot::global(cx).unwrap().read(cx).status() == Status::Authorized; + let enabled = true; Stack::new() .with_child( @@ -45,16 +60,26 @@ impl View for CopilotButton { .status_bar .sidebar_buttons .item - .style_for(state, visible); + .style_for(state, active); Flex::row() .with_child( - Svg::new("icons/maybe_copilot.svg") - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_size) - .aligned() - .named("copilot-icon"), + Svg::new({ + if authorized { + if enabled { + "icons/copilot_16.svg" + } else { + "icons/copilot_disabled_16.svg" + } + } else { + "icons/copilot_init_16.svg" + } + }) + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) + .aligned() + .named("copilot-icon"), ) .constrained() .with_height(style.icon_size) @@ -64,15 +89,12 @@ impl View for CopilotButton { } }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, _cx| { - // TODO: Behavior of this - // if has_terminals { - // cx.dispatch_action(DeployCopilotMenu); - // } else { - // if !active { - // cx.dispatch_action(FocusDock); - // } - // }; + .on_click(MouseButton::Left, move |_, cx| { + if authorized { + cx.dispatch_action(DeployCopilotMenu); + } else { + cx.dispatch_action(DeployCopilotModal); + } }) .with_tooltip::( 0, @@ -102,6 +124,7 @@ impl CopilotButton { menu.set_position_mode(OverlayPositionMode::Local); menu }), + editor: None, } } @@ -121,6 +144,7 @@ impl CopilotButton { impl StatusItemView for CopilotButton { fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + if let Some(editor) = item.map(|item| item.act_as::(cx)) {} cx.notify(); } } From cc7c5b416c5bb5bc34aa527026a57d0b5da9a422 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Mar 2023 21:31:33 -0700 Subject: [PATCH 40/53] Add status bar icon reflecting copilot state to Zed status bar --- Cargo.lock | 19 ++ Cargo.toml | 1 + assets/icons/maybe_link_out.svg | 5 + crates/collab_ui/src/collab_titlebar_item.rs | 20 +- crates/context_menu/src/context_menu.rs | 128 ++++++-- crates/copilot/src/copilot.rs | 1 - crates/copilot/src/copilot_button.rs | 150 --------- crates/copilot/src/editor.rs | 3 + crates/copilot_button/Cargo.toml | 22 ++ crates/copilot_button/src/copilot_button.rs | 301 +++++++++++++++++++ crates/editor/src/editor.rs | 10 +- crates/gpui/src/elements.rs | 6 + crates/settings/Cargo.toml | 1 + crates/settings/src/settings.rs | 110 ++++++- crates/theme/src/theme.rs | 1 + crates/workspace/src/notifications.rs | 10 +- crates/workspace/src/workspace.rs | 4 +- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 4 +- styles/src/styleTree/copilot.ts | 10 + 20 files changed, 606 insertions(+), 201 deletions(-) create mode 100644 assets/icons/maybe_link_out.svg delete mode 100644 crates/copilot/src/copilot_button.rs create mode 100644 crates/copilot/src/editor.rs create mode 100644 crates/copilot_button/Cargo.toml create mode 100644 crates/copilot_button/src/copilot_button.rs diff --git a/Cargo.lock b/Cargo.lock index 1f7c9bc814..84abc6e101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1356,6 +1356,23 @@ dependencies = [ "workspace", ] +[[package]] +name = "copilot_button" +version = "0.1.0" +dependencies = [ + "anyhow", + "context_menu", + "copilot", + "editor", + "futures 0.3.25", + "gpui", + "settings", + "smol", + "theme", + "util", + "workspace", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -5924,6 +5941,7 @@ dependencies = [ "gpui", "json_comments", "postage", + "pretty_assertions", "schemars", "serde", "serde_derive", @@ -8507,6 +8525,7 @@ dependencies = [ "command_palette", "context_menu", "copilot", + "copilot_button", "ctor", "db", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index f097b5b2c7..8fad52c8f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/command_palette", "crates/context_menu", "crates/copilot", + "crates/copilot_button", "crates/db", "crates/diagnostics", "crates/drag_and_drop", diff --git a/assets/icons/maybe_link_out.svg b/assets/icons/maybe_link_out.svg new file mode 100644 index 0000000000..561f012452 --- /dev/null +++ b/assets/icons/maybe_link_out.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 3228f7d5a6..b5e8696ec7 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -301,25 +301,13 @@ impl CollabTitlebarItem { .with_style(item_style.container) .boxed() })), - ContextMenuItem::Item { - label: "Sign out".into(), - action: Box::new(SignOut), - }, - ContextMenuItem::Item { - label: "Send Feedback".into(), - action: Box::new(feedback::feedback_editor::GiveFeedback), - }, + ContextMenuItem::item("Sign out", SignOut), + ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback), ] } else { vec![ - ContextMenuItem::Item { - label: "Sign in".into(), - action: Box::new(SignIn), - }, - ContextMenuItem::Item { - label: "Send Feedback".into(), - action: Box::new(feedback::feedback_editor::GiveFeedback), - }, + ContextMenuItem::item("Sign in", SignIn), + ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback), ] }; diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index e1b9f81c1a..ffc121576e 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,7 +1,7 @@ use gpui::{ elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext, platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, - MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext, + MouseState, MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext, }; use menu::*; use settings::Settings; @@ -24,20 +24,71 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContextMenu::cancel); } +type ContextMenuItemBuilder = Box ElementBox>; + +pub enum ContextMenuItemLabel { + String(Cow<'static, str>), + Element(ContextMenuItemBuilder), +} + +pub enum ContextMenuAction { + ParentAction { + action: Box, + }, + ViewAction { + action: Box, + for_view: usize, + }, +} + +impl ContextMenuAction { + fn id(&self) -> TypeId { + match self { + ContextMenuAction::ParentAction { action } => action.id(), + ContextMenuAction::ViewAction { action, .. } => action.id(), + } + } +} + pub enum ContextMenuItem { Item { - label: Cow<'static, str>, - action: Box, + label: ContextMenuItemLabel, + action: ContextMenuAction, }, Static(StaticItem), Separator, } impl ContextMenuItem { + pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self { + Self::Item { + label: ContextMenuItemLabel::Element(label), + action: ContextMenuAction::ParentAction { + action: Box::new(action), + }, + } + } + pub fn item(label: impl Into>, action: impl 'static + Action) -> Self { Self::Item { - label: label.into(), - action: Box::new(action), + label: ContextMenuItemLabel::String(label.into()), + action: ContextMenuAction::ParentAction { + action: Box::new(action), + }, + } + } + + pub fn item_for_view( + label: impl Into>, + view_id: usize, + action: impl 'static + Action, + ) -> Self { + Self::Item { + label: ContextMenuItemLabel::String(label.into()), + action: ContextMenuAction::ViewAction { + action: Box::new(action), + for_view: view_id, + }, } } @@ -168,7 +219,15 @@ impl ContextMenu { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(ix) = self.selected_index { if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) { - cx.dispatch_any_action(action.boxed_clone()); + match action { + ContextMenuAction::ParentAction { action } => { + cx.dispatch_any_action(action.boxed_clone()) + } + ContextMenuAction::ViewAction { action, for_view } => { + let window_id = cx.window_id(); + cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone()) + } + }; self.reset(cx); } } @@ -278,10 +337,17 @@ impl ContextMenu { Some(ix) == self.selected_index, ); - Label::new(label.to_string(), style.label.clone()) - .contained() - .with_style(style.container) - .boxed() + match label { + ContextMenuItemLabel::String(label) => { + Label::new(label.to_string(), style.label.clone()) + .contained() + .with_style(style.container) + .boxed() + } + ContextMenuItemLabel::Element(element) => { + element(&mut Default::default(), style) + } + } } ContextMenuItem::Static(f) => f(cx), @@ -306,9 +372,18 @@ impl ContextMenu { &mut Default::default(), Some(ix) == self.selected_index, ); + let (action, view_id) = match action { + ContextMenuAction::ParentAction { action } => { + (action.boxed_clone(), self.parent_view_id) + } + ContextMenuAction::ViewAction { action, for_view } => { + (action.boxed_clone(), *for_view) + } + }; + KeystrokeLabel::new( window_id, - self.parent_view_id, + view_id, action.boxed_clone(), style.keystroke.container, style.keystroke.text.clone(), @@ -347,22 +422,34 @@ impl ContextMenu { .with_children(self.items.iter().enumerate().map(|(ix, item)| { match item { ContextMenuItem::Item { label, action } => { - let action = action.boxed_clone(); + let (action, view_id) = match action { + ContextMenuAction::ParentAction { action } => { + (action.boxed_clone(), self.parent_view_id) + } + ContextMenuAction::ViewAction { action, for_view } => { + (action.boxed_clone(), *for_view) + } + }; MouseEventHandler::::new(ix, cx, |state, _| { let style = style.item.style_for(state, Some(ix) == self.selected_index); Flex::row() - .with_child( - Label::new(label.clone(), style.label.clone()) - .contained() - .boxed(), - ) + .with_child(match label { + ContextMenuItemLabel::String(label) => { + Label::new(label.clone(), style.label.clone()) + .contained() + .boxed() + } + ContextMenuItemLabel::Element(element) => { + element(state, style) + } + }) .with_child({ KeystrokeLabel::new( window_id, - self.parent_view_id, + view_id, action.boxed_clone(), style.keystroke.container, style.keystroke.text.clone(), @@ -375,9 +462,12 @@ impl ContextMenu { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) + .on_up(MouseButton::Left, |_, _| {}) // Capture these events + .on_down(MouseButton::Left, |_, _| {}) // Capture these events .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(Clicked); - cx.dispatch_any_action(action.boxed_clone()); + let window_id = cx.window_id(); + cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone()); }) .on_drag(MouseButton::Left, |_, _| {}) .boxed() diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index efa693278e..6dd2f7518b 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1,4 +1,3 @@ -pub mod copilot_button; mod request; mod sign_in; diff --git a/crates/copilot/src/copilot_button.rs b/crates/copilot/src/copilot_button.rs deleted file mode 100644 index fdc5dc776f..0000000000 --- a/crates/copilot/src/copilot_button.rs +++ /dev/null @@ -1,150 +0,0 @@ -use context_menu::{ContextMenu, ContextMenuItem}; -use gpui::{ - elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton, - MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle, -}; -use settings::Settings; -use theme::Editor; -use workspace::{item::ItemHandle, NewTerminal, StatusItemView}; - -use crate::{Copilot, Status}; - -const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; - -#[derive(Clone, PartialEq)] -pub struct DeployCopilotMenu; - -// TODO: Make the other code path use `get_or_insert` logic for this modal -#[derive(Clone, PartialEq)] -pub struct DeployCopilotModal; - -impl_internal_actions!(copilot, [DeployCopilotMenu, DeployCopilotModal]); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_action(CopilotButton::deploy_copilot_menu); -} - -pub struct CopilotButton { - popup_menu: ViewHandle, - editor: Option>, -} - -impl Entity for CopilotButton { - type Event = (); -} - -impl View for CopilotButton { - fn ui_name() -> &'static str { - "CopilotButton" - } - - fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { - let settings = cx.global::(); - - if !settings.enable_copilot_integration { - return Empty::new().boxed(); - } - - let theme = settings.theme.clone(); - let active = self.popup_menu.read(cx).visible() /* || modal.is_shown */; - let authorized = Copilot::global(cx).unwrap().read(cx).status() == Status::Authorized; - let enabled = true; - - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, { - let theme = theme.clone(); - move |state, _cx| { - let style = theme - .workspace - .status_bar - .sidebar_buttons - .item - .style_for(state, active); - - Flex::row() - .with_child( - Svg::new({ - if authorized { - if enabled { - "icons/copilot_16.svg" - } else { - "icons/copilot_disabled_16.svg" - } - } else { - "icons/copilot_init_16.svg" - } - }) - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_size) - .aligned() - .named("copilot-icon"), - ) - .constrained() - .with_height(style.icon_size) - .contained() - .with_style(style.container) - .boxed() - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - if authorized { - cx.dispatch_action(DeployCopilotMenu); - } else { - cx.dispatch_action(DeployCopilotModal); - } - }) - .with_tooltip::( - 0, - "GitHub Copilot".into(), - None, - theme.tooltip.clone(), - cx, - ) - .boxed(), - ) - .with_child( - ChildView::new(&self.popup_menu, cx) - .aligned() - .top() - .right() - .boxed(), - ) - .boxed() - } -} - -impl CopilotButton { - pub fn new(cx: &mut ViewContext) -> Self { - Self { - popup_menu: cx.add_view(|cx| { - let mut menu = ContextMenu::new(cx); - menu.set_position_mode(OverlayPositionMode::Local); - menu - }), - editor: None, - } - } - - pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext) { - let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)]; - - self.popup_menu.update(cx, |menu, cx| { - menu.show( - Default::default(), - AnchorCorner::BottomRight, - menu_options, - cx, - ); - }); - } -} - -impl StatusItemView for CopilotButton { - fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { - if let Some(editor) = item.map(|item| item.act_as::(cx)) {} - cx.notify(); - } -} diff --git a/crates/copilot/src/editor.rs b/crates/copilot/src/editor.rs new file mode 100644 index 0000000000..7fc4204449 --- /dev/null +++ b/crates/copilot/src/editor.rs @@ -0,0 +1,3 @@ +use gpui::MutableAppContext; + +fn init(cx: &mut MutableAppContext) {} diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml new file mode 100644 index 0000000000..f44493b323 --- /dev/null +++ b/crates/copilot_button/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "copilot_button" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/copilot_button.rs" +doctest = false + +[dependencies] +copilot = { path = "../copilot" } +editor = { path = "../editor" } +context_menu = { path = "../context_menu" } +gpui = { path = "../gpui" } +settings = { path = "../settings" } +theme = { path = "../theme" } +util = { path = "../util" } +workspace = { path = "../workspace" } +anyhow = "1.0" +smol = "1.2.5" +futures = "0.3" diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs new file mode 100644 index 0000000000..45255b4f65 --- /dev/null +++ b/crates/copilot_button/src/copilot_button.rs @@ -0,0 +1,301 @@ +use std::sync::Arc; + +use context_menu::{ContextMenu, ContextMenuItem}; +use editor::Editor; +use gpui::{ + elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton, + MouseState, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, +}; +use settings::{settings_file::SettingsFile, Settings}; +use workspace::{ + item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView, +}; + +use copilot::{Copilot, SignOut, Status}; + +const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; + +#[derive(Clone, PartialEq)] +pub struct DeployCopilotMenu; + +#[derive(Clone, PartialEq)] +pub struct ToggleCopilotForLanguage { + language: Arc, +} + +#[derive(Clone, PartialEq)] +pub struct ToggleCopilotGlobally; + +// TODO: Make the other code path use `get_or_insert` logic for this modal +#[derive(Clone, PartialEq)] +pub struct DeployCopilotModal; + +impl_internal_actions!( + copilot, + [ + DeployCopilotMenu, + DeployCopilotModal, + ToggleCopilotForLanguage, + ToggleCopilotGlobally + ] +); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(CopilotButton::deploy_copilot_menu); + cx.add_action( + |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| { + let language = action.language.to_owned(); + + let current_langauge = cx.global::().copilot_on(Some(&language)); + + SettingsFile::update(cx, move |file_contents| { + file_contents.languages.insert( + language.to_owned(), + settings::EditorSettings { + copilot: Some((!current_langauge).into()), + ..Default::default() + }, + ); + }) + }, + ); + + cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| { + let copilot_on = cx.global::().copilot_on(None); + + SettingsFile::update(cx, move |file_contents| { + file_contents.editor.copilot = Some((!copilot_on).into()) + }) + }); +} + +pub struct CopilotButton { + popup_menu: ViewHandle, + editor_subscription: Option<(Subscription, usize)>, + editor_enabled: Option, + language: Option>, +} + +impl Entity for CopilotButton { + type Event = (); +} + +impl View for CopilotButton { + fn ui_name() -> &'static str { + "CopilotButton" + } + + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + let settings = cx.global::(); + + if !settings.enable_copilot_integration { + return Empty::new().boxed(); + } + + let theme = settings.theme.clone(); + let active = self.popup_menu.read(cx).visible() /* || modal.is_shown */; + let authorized = Copilot::global(cx).unwrap().read(cx).status() == Status::Authorized; + let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); + + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, { + let theme = theme.clone(); + move |state, _cx| { + let style = theme + .workspace + .status_bar + .sidebar_buttons + .item + .style_for(state, active); + + Flex::row() + .with_child( + Svg::new({ + if authorized { + if enabled { + "icons/copilot_16.svg" + } else { + "icons/copilot_disabled_16.svg" + } + } else { + "icons/copilot_init_16.svg" + } + }) + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) + .aligned() + .named("copilot-icon"), + ) + .constrained() + .with_height(style.icon_size) + .contained() + .with_style(style.container) + .boxed() + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + if authorized { + cx.dispatch_action(DeployCopilotMenu); + } else { + cx.dispatch_action(DeployCopilotModal); + } + }) + .with_tooltip::( + 0, + "GitHub Copilot".into(), + None, + theme.tooltip.clone(), + cx, + ) + .boxed(), + ) + .with_child( + ChildView::new(&self.popup_menu, cx) + .aligned() + .top() + .right() + .boxed(), + ) + .boxed() + } +} + +impl CopilotButton { + pub fn new(cx: &mut ViewContext) -> Self { + let menu = cx.add_view(|cx| { + let mut menu = ContextMenu::new(cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }); + + cx.observe(&menu, |_, _, cx| cx.notify()).detach(); + cx.observe(&Copilot::global(cx).unwrap(), |_, _, cx| cx.notify()) + .detach(); + let this_handle = cx.handle(); + cx.observe_global::(move |cx| this_handle.update(cx, |_, cx| cx.notify())) + .detach(); + + Self { + popup_menu: menu, + editor_subscription: None, + editor_enabled: None, + language: None, + } + } + + pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext) { + let settings = cx.global::(); + + let mut menu_options = Vec::with_capacity(6); + + if let Some((_, view_id)) = self.editor_subscription.as_ref() { + let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); + menu_options.push(ContextMenuItem::item_for_view( + if locally_enabled { + "Pause Copilot for file" + } else { + "Resume Copilot for file" + }, + *view_id, + copilot::Toggle, + )); + } + + if let Some(language) = &self.language { + let language_enabled = settings.copilot_on(Some(language.as_ref())); + + menu_options.push(ContextMenuItem::item( + format!( + "{} Copilot for {}", + if language_enabled { + "Disable" + } else { + "Enable" + }, + language + ), + ToggleCopilotForLanguage { + language: language.to_owned(), + }, + )); + } + + let globally_enabled = cx.global::().copilot_on(None); + menu_options.push(ContextMenuItem::item( + if globally_enabled { + "Disable Copilot Globally" + } else { + "Enable Copilot Locally" + }, + ToggleCopilotGlobally, + )); + + menu_options.push(ContextMenuItem::Separator); + + let icon_style = settings.theme.copilot.out_link_icon.clone(); + menu_options.push(ContextMenuItem::element_item( + Box::new( + move |state: &mut MouseState, style: &theme::ContextMenuItem| { + Flex::row() + .with_children([ + Label::new("Copilot Settings", style.label.clone()).boxed(), + theme::ui::icon(icon_style.style_for(state, false)).boxed(), + ]) + .boxed() + }, + ), + OsOpen::new(COPILOT_SETTINGS_URL), + )); + + menu_options.push(ContextMenuItem::item("Sign Out", SignOut)); + + self.popup_menu.update(cx, |menu, cx| { + menu.show( + Default::default(), + AnchorCorner::BottomRight, + menu_options, + cx, + ); + }); + } + + pub fn update_enabled(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + let editor = editor.read(cx); + + if let Some(enabled) = editor.copilot_state.user_enabled { + self.editor_enabled = Some(enabled); + cx.notify(); + return; + } + + let snapshot = editor.buffer().read(cx).snapshot(cx); + let settings = cx.global::(); + let suggestion_anchor = editor.selections.newest_anchor().start; + + let language_name = snapshot + .language_at(suggestion_anchor) + .map(|language| language.name()); + + self.language = language_name.clone(); + self.editor_enabled = Some(settings.copilot_on(language_name.as_deref())); + cx.notify() + } +} + +impl StatusItemView for CopilotButton { + fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + if let Some(editor) = item.map(|item| item.act_as::(cx)).flatten() { + self.editor_subscription = + Some((cx.observe(&editor, Self::update_enabled), editor.id())); + self.update_enabled(editor, cx); + } else { + self.language = None; + self.editor_subscription = None; + self.editor_enabled = None; + } + cx.notify(); + } +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f8f83dc101..e0ab8d84b4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -510,7 +510,7 @@ pub struct Editor { hover_state: HoverState, gutter_hovered: bool, link_go_to_definition_state: LinkGoToDefinitionState, - copilot_state: CopilotState, + pub copilot_state: CopilotState, _subscriptions: Vec, } @@ -1008,12 +1008,12 @@ impl CodeActionsMenu { } } -struct CopilotState { +pub struct CopilotState { excerpt_id: Option, pending_refresh: Task>, completions: Vec, active_completion_index: usize, - user_enabled: Option, + pub user_enabled: Option, } impl Default for CopilotState { @@ -2859,6 +2859,7 @@ impl Editor { fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { // Auto re-enable copilot if you're asking for a suggestion if self.copilot_state.user_enabled == Some(false) { + cx.notify(); self.copilot_state.user_enabled = Some(true); } @@ -2880,6 +2881,7 @@ impl Editor { ) { // Auto re-enable copilot if you're asking for a suggestion if self.copilot_state.user_enabled == Some(false) { + cx.notify(); self.copilot_state.user_enabled = Some(true); } @@ -2921,6 +2923,8 @@ impl Editor { } else { self.clear_copilot_suggestions(cx); } + + cx.notify(); } fn sync_suggestion(&mut self, cx: &mut ViewContext) { diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index a42dc1cfa8..bf3e17e1f1 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -389,6 +389,12 @@ impl ElementBox { } } +impl Clone for ElementBox { + fn clone(&self) -> Self { + ElementBox(self.0.clone()) + } +} + impl From for ElementRc { fn from(val: ElementBox) -> Self { val.0 diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 6eeab7d7d9..5972808396 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -36,3 +36,4 @@ tree-sitter-json = "*" unindent = "0.1" gpui = { path = "../gpui", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } +pretty_assertions = "1.3.0" diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 6688b3c4d4..e28ce180b1 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -188,17 +188,30 @@ pub enum OnOff { } impl OnOff { - fn as_bool(&self) -> bool { + pub fn as_bool(&self) -> bool { match self { OnOff::On => true, OnOff::Off => false, } } + + pub fn from_bool(value: bool) -> OnOff { + match value { + true => OnOff::On, + false => OnOff::Off, + } + } } -impl Into for OnOff { - fn into(self) -> bool { - self.as_bool() +impl From for bool { + fn from(value: OnOff) -> bool { + value.as_bool() + } +} + +impl From for OnOff { + fn from(value: bool) -> OnOff { + OnOff::from_bool(value) } } @@ -928,6 +941,7 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu settings_content.insert_str(first_key_start, &content); } } else { + dbg!("here???"); new_value = serde_json::json!({ new_key.to_string(): new_value }); let indent_prefix_len = 4 * depth; let new_val = to_pretty_json(&new_value, 4, indent_prefix_len); @@ -973,13 +987,28 @@ fn to_pretty_json( pub fn update_settings_file( mut text: String, - old_file_content: SettingsFileContent, + mut old_file_content: SettingsFileContent, update: impl FnOnce(&mut SettingsFileContent), ) -> String { let mut new_file_content = old_file_content.clone(); update(&mut new_file_content); + if new_file_content.languages.len() != old_file_content.languages.len() { + for language in new_file_content.languages.keys() { + old_file_content + .languages + .entry(language.clone()) + .or_default(); + } + for language in old_file_content.languages.keys() { + new_file_content + .languages + .entry(language.clone()) + .or_default(); + } + } + let old_object = to_json_object(old_file_content); let new_object = to_json_object(new_file_content); @@ -992,6 +1021,7 @@ pub fn update_settings_file( for (key, old_value) in old_object.iter() { // We know that these two are from the same shape of object, so we can just unwrap let new_value = new_object.get(key).unwrap(); + if old_value != new_value { match new_value { Value::Bool(_) | Value::Number(_) | Value::String(_) => { @@ -1047,7 +1077,75 @@ mod tests { let old_json = old_json.into(); let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default(); let new_json = update_settings_file(old_json, old_content, update); - assert_eq!(new_json, expected_new_json.into()); + pretty_assertions::assert_eq!(new_json, expected_new_json.into()); + } + + #[test] + fn test_update_copilot() { + assert_new_settings( + r#" + { + "languages": { + "JSON": { + "copilot": "off" + } + } + } + "# + .unindent(), + |settings| { + settings.editor.copilot = Some(OnOff::On); + }, + r#" + { + "copilot": "on", + "languages": { + "JSON": { + "copilot": "off" + } + } + } + "# + .unindent(), + ); + } + + #[test] + fn test_update_langauge_copilot() { + assert_new_settings( + r#" + { + "languages": { + "JSON": { + "copilot": "off" + } + } + } + "# + .unindent(), + |settings| { + settings.languages.insert( + "Rust".into(), + EditorSettings { + copilot: Some(OnOff::On), + ..Default::default() + }, + ); + }, + r#" + { + "languages": { + "Rust": { + "copilot": "on" + }, + "JSON": { + "copilot": "off" + } + } + } + "# + .unindent(), + ); } #[test] diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 042249c265..7c9f42c2f5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -119,6 +119,7 @@ pub struct AvatarStyle { #[derive(Deserialize, Default, Clone)] pub struct Copilot { + pub out_link_icon: Interactive, pub modal: ModalStyle, pub auth: CopilotAuth, } diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 76f46f83c5..f19f876be5 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -141,7 +141,13 @@ pub mod simple_message_notification { actions!(message_notifications, [CancelMessageNotification]); #[derive(Clone, Default, Deserialize, PartialEq)] - pub struct OsOpen(pub String); + pub struct OsOpen(pub Cow<'static, str>); + + impl OsOpen { + pub fn new>>(url: I) -> Self { + OsOpen(url.into()) + } + } impl_actions!(message_notifications, [OsOpen]); @@ -149,7 +155,7 @@ pub mod simple_message_notification { cx.add_action(MessageNotification::dismiss); cx.add_action( |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext| { - cx.platform().open_url(open_action.0.as_str()); + cx.platform().open_url(open_action.0.as_ref()); }, ) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index eb04e05286..83b87b9221 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2690,7 +2690,7 @@ fn notify_if_database_failed(workspace: &ViewHandle, cx: &mut AsyncAp indoc::indoc! {" Failed to load any database file :( "}, - OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()), + OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()), "Click to let us know about this error" ) }) @@ -2712,7 +2712,7 @@ fn notify_if_database_failed(workspace: &ViewHandle, cx: &mut AsyncAp "}, backup_path ), - OsOpen(backup_path.to_string()), + OsOpen::new(backup_path.to_string()), "Click to show old database in finder", ) }) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c13ae2411c..2d59d8f309 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -29,6 +29,7 @@ context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } copilot = { path = "../copilot" } +copilot_button = { path = "../copilot_button" } diagnostics = { path = "../diagnostics" } db = { path = "../db" } editor = { path = "../editor" } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d9c91225c2..01b493bf7d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -8,7 +8,6 @@ use breadcrumbs::Breadcrumbs; pub use client; use collab_ui::{CollabTitlebarItem, ToggleContactsMenu}; use collections::VecDeque; -use copilot::copilot_button::CopilotButton; pub use editor; use editor::{Editor, MultiBuffer}; @@ -262,6 +261,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }, ); activity_indicator::init(cx); + copilot_button::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); settings::KeymapFileContent::load_defaults(cx); } @@ -312,7 +312,7 @@ pub fn initialize_workspace( }); let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx)); - let copilot = cx.add_view(|cx| CopilotButton::new(cx)); + let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx)); let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); let activity_indicator = diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index fe77cab8dc..106fed298f 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -30,6 +30,16 @@ export default function copilot(colorScheme: ColorScheme) { }; return { + outLinkIcon: { + icon: svg(foreground(layer, "variant"), "icons/maybe_link_out.svg", 12, 12), + container: { + cornerRadius: 6, + padding: { top: 6, bottom: 6, left: 6, right: 6 }, + }, + hover: { + icon: svg(foreground(layer, "hovered"), "icons/maybe_link_out.svg", 12, 12) + }, + }, modal: { titleText: { ...text(layer, "sans", { size: "md", color: background(layer, "default") }), From a8033b266d9bf67a8b40aadc492cc3bf3fa2ad9f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Mar 2023 21:51:07 -0700 Subject: [PATCH 41/53] Fix bug with enable setting, clean up sign in UIs --- crates/copilot/src/sign_in.rs | 56 ++++----------------- crates/copilot_button/src/copilot_button.rs | 4 +- crates/settings/src/settings.rs | 4 ++ 3 files changed, 17 insertions(+), 47 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 0a9299f512..cce064160e 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -66,6 +66,7 @@ pub fn init(cx: &mut MutableAppContext) { .detach(); // Modal theming test: + // use gpui::geometry::vector::vec2f; // let window_size = cx.global::().theme.copilot.modal.dimensions(); // let window_options = WindowOptions { // bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), @@ -326,52 +327,17 @@ impl CopilotCodeVerification { .aligned() .boxed(), Self::render_device_code(data, &style, cx), - // match &self.prompt { - // SignInContents::PromptingUser(data) => { - - // } - // SignInContents::Unauthorized => Self::render_not_authorized_warning(&style), - // SignInContents::Enabled => Self::render_copilot_enabled(&style), - // }, Flex::column() - .with_child( - theme::ui::cta_button_with_click( - "Connect to GitHub", - style.auth.content_width, - &style.auth.cta_button, - cx, - { - let verification_uri = data.verification_uri.clone(); - move |_, cx| cx.platform().open_url(&verification_uri) - }, - ), - // { - // match &self.prompt { - // SignInContents::PromptingUser(data) => { - - // } - // // SignInContents::Unauthorized => theme::ui::cta_button_with_click( - // // "Close", - // // style.auth.content_width, - // // &style.auth.cta_button, - // // cx, - // // |_, cx| { - // // let window_id = cx.window_id(); - // // cx.remove_window(window_id) - // // }, - // // ), - // // SignInContents::Enabled => theme::ui::cta_button_with_click( - // // "Done", - // // style.auth.content_width, - // // &style.auth.cta_button, - // // cx, - // // |_, cx| { - // // let window_id = cx.window_id(); - // // cx.remove_window(window_id) - // // }, - // // ), - // } - ) + .with_child(theme::ui::cta_button_with_click( + "Connect to GitHub", + style.auth.content_width, + &style.auth.cta_button, + cx, + { + let verification_uri = data.verification_uri.clone(); + move |_, cx| cx.platform().open_url(&verification_uri) + }, + )) .align_children_center() .contained() .with_style(style.auth.github_group) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 45255b4f65..e61a8814af 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -11,7 +11,7 @@ use workspace::{ item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView, }; -use copilot::{Copilot, SignOut, Status}; +use copilot::{Copilot, SignIn, SignOut, Status}; const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; @@ -140,7 +140,7 @@ impl View for CopilotButton { if authorized { cx.dispatch_action(DeployCopilotMenu); } else { - cx.dispatch_action(DeployCopilotModal); + cx.dispatch_action(SignIn); } }) .with_tooltip::( diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index e28ce180b1..2e087dd260 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -567,6 +567,10 @@ impl Settings { merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.base_keymap, data.base_keymap); + merge( + &mut self.enable_copilot_integration, + data.enable_copilot_integration, + ); self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); From 5b3b74345dcc2237b593d189fb7e739a65b088a2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 29 Mar 2023 22:22:02 -0700 Subject: [PATCH 42/53] Remove some strays --- crates/copilot/readme.md | 21 --------------------- crates/settings/src/settings.rs | 1 - 2 files changed, 22 deletions(-) delete mode 100644 crates/copilot/readme.md diff --git a/crates/copilot/readme.md b/crates/copilot/readme.md deleted file mode 100644 index a916081970..0000000000 --- a/crates/copilot/readme.md +++ /dev/null @@ -1,21 +0,0 @@ -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/settings/src/settings.rs b/crates/settings/src/settings.rs index 2e087dd260..5825e48d5a 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -945,7 +945,6 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu settings_content.insert_str(first_key_start, &content); } } else { - dbg!("here???"); new_value = serde_json::json!({ new_key.to_string(): new_value }); let indent_prefix_len = 4 * depth; let new_val = to_pretty_json(&new_value, 4, indent_prefix_len); From e46cd2def3047381cf99417c6dc833e5bb4b1504 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 09:29:44 -0700 Subject: [PATCH 43/53] Switch to using zed hosted copilot LSP (again) co-authored-by: antonio --- Cargo.lock | 1 + crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 49 +++++++++++++++++++++-------------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84abc6e101..403dffadfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,6 +1338,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", + "async-tar", "client", "collections", "context_menu", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 47f49f9910..5c9ef2d7c4 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -21,6 +21,7 @@ util = { path = "../util" } client = { path = "../client" } workspace = { path = "../workspace" } async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-tar = "0.4.2" anyhow = "1.0" log = "0.4" serde = { workspace = true } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 6dd2f7518b..8a72f57b95 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1,7 +1,9 @@ mod request; mod sign_in; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; use client::Client; use futures::{future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ @@ -12,13 +14,15 @@ use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapsho use lsp::LanguageServer; use node_runtime::NodeRuntime; use settings::Settings; -use smol::{fs, stream::StreamExt}; +use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ ffi::OsString, path::{Path, PathBuf}, sync::Arc, }; -use util::{fs::remove_matching, http::HttpClient, paths, ResultExt}; +use util::{ + fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, +}; const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; actions!(copilot_auth, [SignIn, SignOut]); @@ -191,7 +195,7 @@ impl Copilot { ) -> impl Future { async move { let start_language_server = async { - let server_path = get_copilot_lsp(http, node_runtime.clone()).await?; + let server_path = get_copilot_lsp(http).await?; let node_path = node_runtime.binary_path().await?; let arguments: &[OsString] = &[server_path.into(), "--stdio".into()]; let server = @@ -305,6 +309,8 @@ impl Copilot { cx.foreground() .spawn(task.map_err(|err| anyhow!("{:?}", err))) } else { + // If we're downloading, wait until download is finished + // If we're in a stuck state, display to the user Task::ready(Err(anyhow!("copilot hasn't started yet"))) } } @@ -495,29 +501,32 @@ fn completion_from_lsp(completion: request::Completion, buffer: &BufferSnapshot) } } -async fn get_copilot_lsp( - http: Arc, - node: Arc, -) -> anyhow::Result { - const SERVER_PATH: &'static str = "node_modules/copilot-node-server/copilot/dist/agent.js"; +async fn get_copilot_lsp(http: Arc) -> anyhow::Result { + const SERVER_PATH: &'static str = "agent.js"; ///Check for the latest copilot language server and download it if we haven't already - async fn fetch_latest( - _http: Arc, - node: Arc, - ) -> anyhow::Result { - const COPILOT_NPM_PACKAGE: &'static str = "copilot-node-server"; + async fn fetch_latest(http: Arc) -> anyhow::Result { + let release = latest_github_release("zed-industries/copilot", http.clone()).await?; - let release = node.npm_package_latest_version(COPILOT_NPM_PACKAGE).await?; - - let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.clone())); + let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name)); fs::create_dir_all(version_dir).await?; let server_path = version_dir.join(SERVER_PATH); if fs::metadata(&server_path).await.is_err() { - node.npm_install_packages([(COPILOT_NPM_PACKAGE, release.as_str())], version_dir) - .await?; + let url = &release + .assets + .get(0) + .context("Github release for copilot contained no assets")? + .browser_download_url; + + let mut response = http + .get(&url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading copilot release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(version_dir).await?; remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await; } @@ -525,7 +534,7 @@ async fn get_copilot_lsp( Ok(server_path) } - match fetch_latest(http, node).await { + match fetch_latest(http).await { ok @ Result::Ok(..) => ok, e @ Err(..) => { e.log_err(); From b7461c32dd28e936a4a5224834bbbcbcb9be5bd9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 10:40:53 -0700 Subject: [PATCH 44/53] Improve settings writing for more cases --- crates/settings/src/settings.rs | 16 ++++++++++++++-- crates/util/src/util.rs | 9 +++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 5825e48d5a..1c796ad5c3 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -21,7 +21,7 @@ use sqlez::{ use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc}; use theme::{Theme, ThemeRegistry}; use tree_sitter::Query; -use util::ResultExt as _; +use util::{RangeExt, ResultExt as _}; pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; pub use watched_json::watch_files; @@ -865,6 +865,7 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu .unwrap(); let mut depth = 0; + let mut last_value_range = 0..0; let mut first_key_start = None; let mut existing_value_range = 0..settings_content.len(); let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes()); @@ -876,6 +877,14 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu let key_range = mat.captures[0].node.byte_range(); let value_range = mat.captures[1].node.byte_range(); + // Don't enter sub objects until we find an exact + // match for the current keypath + if last_value_range.contains_inclusive(&value_range) { + continue; + } + + last_value_range = value_range.clone(); + if key_range.start > existing_value_range.end { break; } @@ -889,6 +898,8 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu if found_key { existing_value_range = value_range; + // Reset last value range when increasing in depth + last_value_range = existing_value_range.start..existing_value_range.start; depth += 1; if depth == key_path.len() { @@ -930,7 +941,8 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu } if row > 0 { - let new_val = to_pretty_json(&new_value, column, column); + // depth is 0 based, but division needs to be 1 based. + let new_val = to_pretty_json(&new_value, column / (depth + 1), column); let content = format!(r#""{new_key}": {new_val},"#); settings_content.insert_str(first_key_start, &content); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 07b2ffd0da..d9db47c2bb 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -301,6 +301,7 @@ pub trait RangeExt { fn sorted(&self) -> Self; fn to_inclusive(&self) -> RangeInclusive; fn overlaps(&self, other: &Range) -> bool; + fn contains_inclusive(&self, other: &Range) -> bool; } impl RangeExt for Range { @@ -315,6 +316,10 @@ impl RangeExt for Range { fn overlaps(&self, other: &Range) -> bool { self.start < other.end && other.start < self.end } + + fn contains_inclusive(&self, other: &Range) -> bool { + self.start <= other.start && other.end <= self.end + } } impl RangeExt for RangeInclusive { @@ -329,6 +334,10 @@ impl RangeExt for RangeInclusive { fn overlaps(&self, other: &Range) -> bool { self.start() < &other.end && &other.start <= self.end() } + + fn contains_inclusive(&self, other: &Range) -> bool { + self.start() <= &other.start && &other.end <= self.end() + } } #[cfg(test)] From 58b453ad6e3dbab56e7b7f968f4ecd245e2017a6 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Mar 2023 14:18:03 -0400 Subject: [PATCH 45/53] Add new copilot state icons --- assets/icons/copilot_16.svg | 22 +++++++++++----------- assets/icons/copilot__disabled_16.svg | 4 ++++ assets/icons/copilot_error_16.svg | 4 ++++ assets/icons/copilot_init_16.svg | 4 ++-- 4 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 assets/icons/copilot__disabled_16.svg create mode 100644 assets/icons/copilot_error_16.svg diff --git a/assets/icons/copilot_16.svg b/assets/icons/copilot_16.svg index 35e84a6d7a..e14b61ce8b 100644 --- a/assets/icons/copilot_16.svg +++ b/assets/icons/copilot_16.svg @@ -1,12 +1,12 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/assets/icons/copilot__disabled_16.svg b/assets/icons/copilot__disabled_16.svg new file mode 100644 index 0000000000..8ee1455173 --- /dev/null +++ b/assets/icons/copilot__disabled_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/copilot_error_16.svg b/assets/icons/copilot_error_16.svg new file mode 100644 index 0000000000..fd00593dc9 --- /dev/null +++ b/assets/icons/copilot_error_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/copilot_init_16.svg b/assets/icons/copilot_init_16.svg index 68478e62aa..0d67201df6 100644 --- a/assets/icons/copilot_init_16.svg +++ b/assets/icons/copilot_init_16.svg @@ -1,4 +1,4 @@ - - + + From 655897b1829d403ffb7b75a64922d75d888304b0 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Mar 2023 14:32:51 -0400 Subject: [PATCH 46/53] Update icons --- assets/icons/copilot__disabled_16.svg | 4 ---- assets/icons/copilot_disabled_16.svg | 15 +++++++-------- assets/icons/copilot_error_16.svg | 5 ++++- assets/icons/copilot_init_16.svg | 2 +- 4 files changed, 12 insertions(+), 14 deletions(-) delete mode 100644 assets/icons/copilot__disabled_16.svg diff --git a/assets/icons/copilot__disabled_16.svg b/assets/icons/copilot__disabled_16.svg deleted file mode 100644 index 8ee1455173..0000000000 --- a/assets/icons/copilot__disabled_16.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/copilot_disabled_16.svg b/assets/icons/copilot_disabled_16.svg index 91bd009508..eba36a2b69 100644 --- a/assets/icons/copilot_disabled_16.svg +++ b/assets/icons/copilot_disabled_16.svg @@ -1,10 +1,9 @@ - - - + + + + + + - - - - - + diff --git a/assets/icons/copilot_error_16.svg b/assets/icons/copilot_error_16.svg index fd00593dc9..6069c554f1 100644 --- a/assets/icons/copilot_error_16.svg +++ b/assets/icons/copilot_error_16.svg @@ -1,4 +1,7 @@ - + + + + diff --git a/assets/icons/copilot_init_16.svg b/assets/icons/copilot_init_16.svg index 0d67201df6..6cbf63fb49 100644 --- a/assets/icons/copilot_init_16.svg +++ b/assets/icons/copilot_init_16.svg @@ -1,4 +1,4 @@ - + From f235d9f411eb6c2cc138d0832a1e94047138f326 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Mar 2023 14:34:33 -0400 Subject: [PATCH 47/53] Add zed plus copilot icon --- assets/icons/zed_plus_copilot_32.svg | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 assets/icons/zed_plus_copilot_32.svg diff --git a/assets/icons/zed_plus_copilot_32.svg b/assets/icons/zed_plus_copilot_32.svg new file mode 100644 index 0000000000..d024678c50 --- /dev/null +++ b/assets/icons/zed_plus_copilot_32.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + From f5d4bcd934e991143541f3f1fcdfb83afaec6f3a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 14:10:57 -0700 Subject: [PATCH 48/53] Added erorr states and first-pass error handling to the copilot status bar item. Added correct icons Added a new 'Toast' action which allows other crates to easily pop toasts with an optional click action --- .../{maybe_link_out.svg => link_out_12.svg} | 0 crates/copilot/src/copilot.rs | 76 +++++++++++++--- crates/copilot/src/sign_in.rs | 20 ++--- crates/copilot_button/src/copilot_button.rs | 87 ++++++++++++++---- crates/theme/src/theme.rs | 4 +- crates/workspace/src/notifications.rs | 40 +++++++-- crates/workspace/src/workspace.rs | 89 ++++++++++++++++++- styles/src/styleTree/copilot.ts | 20 +---- .../styleTree/simpleMessageNotification.ts | 16 +++- 9 files changed, 279 insertions(+), 73 deletions(-) rename assets/icons/{maybe_link_out.svg => link_out_12.svg} (100%) diff --git a/assets/icons/maybe_link_out.svg b/assets/icons/link_out_12.svg similarity index 100% rename from assets/icons/maybe_link_out.svg rename to assets/icons/link_out_12.svg diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 8a72f57b95..727b9d2d4f 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -28,7 +28,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; actions!(copilot_auth, [SignIn, SignOut]); const COPILOT_NAMESPACE: &'static str = "copilot"; -actions!(copilot, [NextSuggestion, PreviousSuggestion, Toggle]); +actions!( + copilot, + [NextSuggestion, PreviousSuggestion, Toggle, Reinstall] +); pub fn init(client: Arc, node_runtime: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx)); @@ -46,6 +49,13 @@ pub fn init(client: Arc, node_runtime: Arc, cx: &mut Mutabl .detach_and_log_err(cx); }); + cx.add_global_action(|_: &Reinstall, cx| { + let copilot = Copilot::global(cx).unwrap(); + copilot + .update(cx, |copilot, cx| copilot.reinstall(cx)) + .detach(); + }); + cx.observe(&copilot, |handle, cx| { let status = handle.read(cx).status(); cx.update_global::( @@ -73,7 +83,7 @@ pub fn init(client: Arc, node_runtime: Arc, cx: &mut Mutabl enum CopilotServer { Disabled, Starting { - _task: Shared>, + task: Shared>, }, Error(Arc), Started { @@ -97,9 +107,11 @@ enum SignInStatus { SignedOut, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone)] pub enum Status { - Starting, + Starting { + task: Shared>, + }, Error(Arc), Disabled, SignedOut, @@ -123,6 +135,8 @@ pub struct Completion { } pub struct Copilot { + http: Arc, + node_runtime: Arc, server: CopilotServer, } @@ -131,6 +145,13 @@ impl Entity for Copilot { } impl Copilot { + pub fn starting_task(&self) -> Option>> { + match self.server { + CopilotServer::Starting { ref task } => Some(task.clone()), + _ => None, + } + } + pub fn global(cx: &AppContext) -> Option> { if cx.has_global::>() { Some(cx.global::>().clone()) @@ -159,10 +180,12 @@ impl Copilot { } }) .shared(); - this.server = CopilotServer::Starting { _task: start_task } + this.server = CopilotServer::Starting { task: start_task }; + cx.notify(); } } else { - this.server = CopilotServer::Disabled + this.server = CopilotServer::Disabled; + cx.notify(); } } }) @@ -178,10 +201,14 @@ impl Copilot { .shared(); Self { - server: CopilotServer::Starting { _task: start_task }, + http, + node_runtime, + server: CopilotServer::Starting { task: start_task }, } } else { Self { + http, + node_runtime, server: CopilotServer::Disabled, } } @@ -332,6 +359,27 @@ impl Copilot { } } + fn reinstall(&mut self, cx: &mut ModelContext) -> Task<()> { + let start_task = cx + .spawn({ + let http = self.http.clone(); + let node_runtime = self.node_runtime.clone(); + move |this, cx| async move { + clear_copilot_dir().await; + Self::start_language_server(http, node_runtime, this, cx).await + } + }) + .shared(); + + self.server = CopilotServer::Starting { + task: start_task.clone(), + }; + + cx.notify(); + + cx.foreground().spawn(start_task) + } + pub fn completion( &self, buffer: &ModelHandle, @@ -391,7 +439,7 @@ impl Copilot { pub fn status(&self) -> Status { match &self.server { - CopilotServer::Starting { .. } => Status::Starting, + CopilotServer::Starting { task } => Status::Starting { task: task.clone() }, CopilotServer::Disabled => Status::Disabled, CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Started { status, .. } => match status { @@ -501,8 +549,12 @@ fn completion_from_lsp(completion: request::Completion, buffer: &BufferSnapshot) } } +async fn clear_copilot_dir() { + remove_matching(&paths::COPILOT_DIR, |_| true).await +} + async fn get_copilot_lsp(http: Arc) -> anyhow::Result { - const SERVER_PATH: &'static str = "agent.js"; + const SERVER_PATH: &'static str = "dist/agent.js"; ///Check for the latest copilot language server and download it if we haven't already async fn fetch_latest(http: Arc) -> anyhow::Result { @@ -514,6 +566,10 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { let server_path = version_dir.join(SERVER_PATH); if fs::metadata(&server_path).await.is_err() { + // Copilot LSP looks for this dist dir specifcially, 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 .get(0) @@ -526,7 +582,7 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { .map_err(|err| anyhow!("error downloading copilot release: {}", err))?; let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); let archive = Archive::new(decompressed_bytes); - archive.unpack(version_dir).await?; + archive.unpack(dist_dir).await?; remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await; } diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index cce064160e..168ba712ce 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -298,9 +298,7 @@ impl CopilotCodeVerification { .with_children([ Flex::row() .with_children([ - theme::ui::svg(&style.auth.copilot_icon).boxed(), - theme::ui::icon(&style.auth.plus_icon).boxed(), - theme::ui::svg(&style.auth.zed_icon).boxed(), + theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed() ]) .boxed(), Flex::column() @@ -362,9 +360,7 @@ impl CopilotCodeVerification { .with_children([ Flex::row() .with_children([ - theme::ui::svg(&style.auth.copilot_icon).boxed(), - theme::ui::icon(&style.auth.plus_icon).boxed(), - theme::ui::svg(&style.auth.zed_icon).boxed(), + theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed() ]) .boxed(), Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(), @@ -410,9 +406,7 @@ impl CopilotCodeVerification { .with_children([ Flex::row() .with_children([ - theme::ui::svg(&style.auth.copilot_icon).boxed(), - theme::ui::icon(&style.auth.plus_icon).boxed(), - theme::ui::svg(&style.auth.zed_icon).boxed(), + theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed() ]) .boxed(), Flex::column() @@ -483,13 +477,13 @@ impl View for CopilotCodeVerification { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let style = cx.global::().theme.copilot.clone(); + let style = cx.global::().theme.clone(); match &self.status { Status::SigningIn { prompt: Some(prompt), - } => Self::render_prompting_modal(&prompt, &style, cx), - Status::Unauthorized => Self::render_unauthorized_modal(&style, cx), - Status::Authorized => Self::render_enabled_modal(&style, cx), + } => Self::render_prompting_modal(&prompt, &style.copilot, cx), + Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx), + Status::Authorized => Self::render_enabled_modal(&style.copilot, cx), _ => Empty::new().boxed(), } } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index e61a8814af..7a0a45da82 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -8,12 +8,15 @@ use gpui::{ }; use settings::{settings_file::SettingsFile, Settings}; use workspace::{ - item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView, + item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast, + StatusItemView, }; -use copilot::{Copilot, SignIn, SignOut, Status}; +use copilot::{Copilot, Reinstall, SignIn, SignOut, Status}; const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; +const COPILOT_STARTING_TOAST_ID: usize = 1337; +const COPILOT_ERROR_TOAST_ID: usize = 1338; #[derive(Clone, PartialEq)] pub struct DeployCopilotMenu; @@ -36,7 +39,7 @@ impl_internal_actions!( DeployCopilotMenu, DeployCopilotModal, ToggleCopilotForLanguage, - ToggleCopilotGlobally + ToggleCopilotGlobally, ] ); @@ -93,14 +96,18 @@ impl View for CopilotButton { } let theme = settings.theme.clone(); - let active = self.popup_menu.read(cx).visible() /* || modal.is_shown */; - let authorized = Copilot::global(cx).unwrap().read(cx).status() == Status::Authorized; + let active = self.popup_menu.read(cx).visible(); + let status = Copilot::global(cx).unwrap().read(cx).status(); + let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); + let view_id = cx.view_id(); + Stack::new() .with_child( MouseEventHandler::::new(0, cx, { let theme = theme.clone(); + let status = status.clone(); move |state, _cx| { let style = theme .workspace @@ -112,14 +119,16 @@ impl View for CopilotButton { Flex::row() .with_child( Svg::new({ - if authorized { - if enabled { - "icons/copilot_16.svg" - } else { - "icons/copilot_disabled_16.svg" + match status { + Status::Error(_) => "icons/copilot_error_16.svg", + Status::Authorized => { + if enabled { + "icons/copilot_16.svg" + } else { + "icons/copilot_disabled_16.svg" + } } - } else { - "icons/copilot_init_16.svg" + _ => "icons/copilot_init_16.svg", } }) .with_color(style.icon_color) @@ -136,11 +145,50 @@ impl View for CopilotButton { } }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - if authorized { - cx.dispatch_action(DeployCopilotMenu); - } else { - cx.dispatch_action(SignIn); + .on_click(MouseButton::Left, { + let status = status.clone(); + move |_, cx| match status { + Status::Authorized => cx.dispatch_action(DeployCopilotMenu), + Status::Starting { ref task } => { + cx.dispatch_action(workspace::Toast::new( + COPILOT_STARTING_TOAST_ID, + "Copilot is starting...", + )); + let window_id = cx.window_id(); + let task = task.to_owned(); + cx.spawn(|mut cx| async move { + task.await; + cx.update(|cx| { + let status = Copilot::global(cx).unwrap().read(cx).status(); + match status { + Status::Authorized => cx.dispatch_action_at( + window_id, + view_id, + workspace::Toast::new( + COPILOT_STARTING_TOAST_ID, + "Copilot has started!", + ), + ), + _ => { + cx.dispatch_action_at( + window_id, + view_id, + DismissToast::new(COPILOT_STARTING_TOAST_ID), + ); + cx.dispatch_global_action(SignIn) + } + } + }) + }) + .detach(); + } + Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action( + COPILOT_ERROR_TOAST_ID, + format!("Copilot can't be started: {}", e), + "Reinstall Copilot", + Reinstall, + )), + _ => cx.dispatch_action(SignIn), } }) .with_tooltip::( @@ -195,9 +243,9 @@ impl CopilotButton { let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); menu_options.push(ContextMenuItem::item_for_view( if locally_enabled { - "Pause Copilot for file" + "Pause Copilot for this file" } else { - "Resume Copilot for file" + "Resume Copilot for this file" }, *view_id, copilot::Toggle, @@ -244,6 +292,7 @@ impl CopilotButton { Label::new("Copilot Settings", style.label.clone()).boxed(), theme::ui::icon(icon_style.style_for(state, false)).boxed(), ]) + .align_children_center() .boxed() }, ), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 7c9f42c2f5..1c7d0eba95 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -131,9 +131,7 @@ pub struct CopilotAuth { pub instruction_text: TextStyle, pub cta_button: ButtonStyle, pub content_width: f32, - pub copilot_icon: SvgStyle, - pub plus_icon: IconStyle, - pub zed_icon: SvgStyle, + pub copilot_plus_zed_icon: SvgStyle, pub device_code_group: ContainerStyle, pub github_group: ContainerStyle, pub header_group: ContainerStyle, diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index f19f876be5..1cb5d3f50d 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -97,7 +97,7 @@ impl Workspace { let notification = build_notification(cx); cx.subscribe(¬ification, move |this, handle, event, cx| { if handle.read(cx).should_dismiss_notification_on_event(event) { - this.dismiss_notification(type_id, id, cx); + this.dismiss_notification_internal(type_id, id, cx); } }) .detach(); @@ -107,7 +107,18 @@ impl Workspace { } } - fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext) { + pub fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext) { + let type_id = TypeId::of::(); + + self.dismiss_notification_internal(type_id, id, cx) + } + + fn dismiss_notification_internal( + &mut self, + type_id: TypeId, + id: usize, + cx: &mut ViewContext, + ) { self.notifications .retain(|(existing_type_id, existing_id, _)| { if (*existing_type_id, *existing_id) == (type_id, id) { @@ -183,6 +194,18 @@ pub mod simple_message_notification { } } + pub fn new_boxed_action>, S2: Into>>( + message: S1, + click_action: Box, + click_message: S2, + ) -> Self { + Self { + message: message.into(), + click_action: Some(click_action), + click_message: Some(click_message.into()), + } + } + pub fn new>, A: Action, S2: Into>>( message: S1, click_action: A, @@ -270,9 +293,13 @@ pub mod simple_message_notification { let style = theme.action_message.style_for(state, false); if let Some(click_message) = click_message { Some( - Text::new(click_message, style.text.clone()) - .contained() - .with_style(style.container) + Flex::row() + .with_child( + Text::new(click_message, style.text.clone()) + .contained() + .with_style(style.container) + .boxed(), + ) .boxed(), ) } else { @@ -288,7 +315,8 @@ pub mod simple_message_notification { .on_up(MouseButton::Left, |_, _| {}) .on_click(MouseButton::Left, move |_, cx| { if let Some(click_action) = click_action.as_ref() { - cx.dispatch_any_action(click_action.boxed_clone()) + cx.dispatch_any_action(click_action.boxed_clone()); + cx.dispatch_action(CancelMessageNotification) } }) .with_cursor_style(if has_click_action { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 83b87b9221..3fffe57e3e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -41,10 +41,10 @@ use gpui::{ impl_actions, impl_internal_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, - MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext, - SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowBounds, + Action, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, + ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, + RenderContext, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, + WeakViewHandle, WindowBounds, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use language::LanguageRegistry; @@ -165,6 +165,67 @@ pub struct OpenProjectEntryInPane { project_entry: ProjectEntryId, } +pub struct Toast { + id: usize, + msg: Cow<'static, str>, + click: Option<(Cow<'static, str>, Box)>, +} + +impl Toast { + pub fn new>>(id: usize, msg: I) -> Self { + Toast { + id, + msg: msg.into(), + click: None, + } + } + + pub fn new_action>, I2: Into>>( + id: usize, + msg: I1, + click_msg: I2, + action: impl Action, + ) -> Self { + Toast { + id, + msg: msg.into(), + click: Some((click_msg.into(), Box::new(action))), + } + } +} + +impl PartialEq for Toast { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.msg == other.msg + && self.click.is_some() == other.click.is_some() + } +} + +impl Clone for Toast { + fn clone(&self) -> Self { + Toast { + id: self.id, + msg: self.msg.to_owned(), + click: self + .click + .as_ref() + .map(|(msg, click)| (msg.to_owned(), click.boxed_clone())), + } + } +} + +#[derive(Clone, PartialEq)] +pub struct DismissToast { + id: usize, +} + +impl DismissToast { + pub fn new(id: usize) -> Self { + DismissToast { id } + } +} + pub type WorkspaceId = i64; impl_internal_actions!( @@ -178,6 +239,8 @@ impl_internal_actions!( SplitWithItem, SplitWithProjectEntry, OpenProjectEntryInPane, + Toast, + DismissToast ] ); impl_actions!(workspace, [ActivatePane]); @@ -353,6 +416,24 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { .detach(); }); + cx.add_action(|workspace: &mut Workspace, alert: &Toast, cx| { + workspace.dismiss_notification::(alert.id, cx); + workspace.show_notification(alert.id, cx, |cx| { + cx.add_view(|_cx| match &alert.click { + Some((click_msg, action)) => MessageNotification::new_boxed_action( + alert.msg.clone(), + action.boxed_clone(), + click_msg.clone(), + ), + None => MessageNotification::new_message(alert.msg.clone()), + }) + }) + }); + + cx.add_action(|workspace: &mut Workspace, alert: &DismissToast, cx| { + workspace.dismiss_notification::(alert.id, cx); + }); + let client = &app_state.client; client.add_view_request_handler(Workspace::handle_follow); client.add_view_message_handler(Workspace::handle_unfollow); diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index 106fed298f..f25bc08103 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -31,13 +31,13 @@ export default function copilot(colorScheme: ColorScheme) { return { outLinkIcon: { - icon: svg(foreground(layer, "variant"), "icons/maybe_link_out.svg", 12, 12), + icon: svg(foreground(layer, "variant"), "icons/link_out_12.svg", 12, 12), container: { cornerRadius: 6, - padding: { top: 6, bottom: 6, left: 6, right: 6 }, + padding: { left: 6 }, }, hover: { - icon: svg(foreground(layer, "hovered"), "icons/maybe_link_out.svg", 12, 12) + icon: svg(foreground(layer, "hovered"), "icons/link_out_12.svg", 12, 12) }, }, modal: { @@ -103,19 +103,7 @@ export default function copilot(colorScheme: ColorScheme) { right: 0 } }, - copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 32, 32), - plusIcon: { - icon: svg(foreground(layer, "default"), "icons/plus_12.svg", 12, 12), - container: { - padding: { - top: 12, - bottom: 12, - left: 12, - right: 12, - } - } - }, - zedIcon: svg(foreground(layer, "default"), "icons/logo_96.svg", 32, 32), + copilotPlusZedIcon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 32, 92), enableText: text(layer, "sans", { size: "md" }), enableGroup: { margin: { diff --git a/styles/src/styleTree/simpleMessageNotification.ts b/styles/src/styleTree/simpleMessageNotification.ts index 36b295c640..dde689e9bd 100644 --- a/styles/src/styleTree/simpleMessageNotification.ts +++ b/styles/src/styleTree/simpleMessageNotification.ts @@ -1,5 +1,5 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { foreground, text } from "./components" +import { background, border, foreground, text } from "./components" const headerPadding = 8 @@ -14,9 +14,21 @@ export default function simpleMessageNotification( }, actionMessage: { ...text(layer, "sans", { size: "xs" }), + border: border(layer, "active"), + cornerRadius: 4, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + + margin: { left: headerPadding, top: 6, bottom: 6 }, hover: { - color: foreground(layer, "hovered"), + ...text(layer, "sans", "default", { size: "xs" }), + background: background(layer, "hovered"), + border: border(layer, "active"), }, }, dismissButton: { From afc9b832c84cc2c405df251b8fafd967fd3dd9c5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 16:50:33 -0700 Subject: [PATCH 49/53] Finish device code flow for copilot --- crates/copilot/src/sign_in.rs | 584 ++++++++++++++------------------ crates/theme/src/theme.rs | 49 ++- crates/theme/src/ui.rs | 24 +- styles/src/styleTree/copilot.ts | 264 ++++++++------- 4 files changed, 442 insertions(+), 479 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 168ba712ce..46f331d8db 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -4,6 +4,7 @@ use gpui::{ ViewContext, ViewHandle, WindowKind, WindowOptions, }; use settings::Settings; +use theme::ui::modal; #[derive(PartialEq, Eq, Debug, Clone)] struct CopyUserCode; @@ -11,7 +12,7 @@ struct CopyUserCode; #[derive(PartialEq, Eq, Debug, Clone)] struct OpenGithub; -const _COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; +const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; pub fn init(cx: &mut MutableAppContext) { let copilot = Copilot::global(cx).unwrap(); @@ -66,53 +67,60 @@ pub fn init(cx: &mut MutableAppContext) { .detach(); // Modal theming test: - // use gpui::geometry::vector::vec2f; - // let window_size = cx.global::().theme.copilot.modal.dimensions(); - // let window_options = WindowOptions { - // bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)), - // titlebar: None, - // center: false, - // focus: false, - // kind: WindowKind::PopUp, - // is_movable: true, - // screen: None, - // }; - // let (_, _view) = cx.add_window(window_options, |_cx| { - // CopilotCodeVerification::new(Status::SigningIn { - // prompt: Some(PromptUserDeviceFlow { - // user_code: "ABCD-1234".to_string(), - // verification_uri: "https://github.com/login/device".to_string(), - // }), - // }) - // }); + use gpui::geometry::vector::vec2f; - // let window_size = cx.global::().theme.copilot.modal.dimensions(); - // let window_options = WindowOptions { - // bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(window_size.x(), 0.), window_size)), - // titlebar: None, - // center: false, - // focus: false, - // kind: WindowKind::PopUp, - // is_movable: true, - // screen: None, - // }; - // let (_, _view) = cx.add_window(window_options, |_cx| { - // CopilotCodeVerification::new(Status::Authorized) - // }); + let window_size = cx.global::().theme.copilot.modal.dimensions(); + let window_options = WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(0., 0.), window_size)), + titlebar: None, + center: false, + focus: false, + kind: WindowKind::PopUp, + is_movable: true, + screen: None, + }; + let (_, _view) = cx.add_window(window_options, |_cx| { + CopilotCodeVerification::new(Status::Authorized) + }); - // let window_size = cx.global::().theme.copilot.modal.dimensions(); - // let window_options = WindowOptions { - // bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(0., window_size.y()), window_size)), - // titlebar: None, - // center: false, - // focus: false, - // kind: WindowKind::PopUp, - // is_movable: true, - // screen: None, - // }; - // let (_, _view) = cx.add_window(window_options, |_cx| { - // CopilotCodeVerification::new(Status::Unauthorized) - // }); + let window_size = cx.global::().theme.copilot.modal.dimensions(); + let window_options = WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new( + vec2f(window_size.x() + 10., 0.), + window_size, + )), + titlebar: None, + center: false, + focus: false, + kind: WindowKind::PopUp, + is_movable: true, + screen: None, + }; + let (_, _view) = cx.add_window(window_options, |_cx| { + CopilotCodeVerification::new(Status::SigningIn { + prompt: Some(PromptUserDeviceFlow { + user_code: "ABCD-1234".to_string(), + verification_uri: "https://github.com/login/device".to_string(), + }), + }) + }); + + let window_size = cx.global::().theme.copilot.modal.dimensions(); + let window_options = WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new( + vec2f((window_size.x() + 10.) * 2., 0.), + window_size, + )), + titlebar: None, + center: false, + focus: false, + kind: WindowKind::PopUp, + is_movable: true, + screen: None, + }; + let (_, _view) = cx.add_window(window_options, |_cx| { + CopilotCodeVerification::new(Status::Unauthorized) + }); } pub struct CopilotCodeVerification { @@ -139,151 +147,43 @@ impl CopilotCodeVerification { .map(|item| item.text() == &data.user_code) .unwrap_or(false); - Flex::column() - .with_children([ - MouseEventHandler::::new(0, cx, |state, _cx| { - Flex::row() - .with_children([ - Label::new(data.user_code.clone(), style.auth.device_code.clone()) - .aligned() - .contained() - .with_style(style.auth.device_code_left_container) - .constrained() - .with_width(style.auth.device_code_left) - .boxed(), - Empty::new() - .constrained() - .with_width(1.) - .with_height(style.auth.device_code_seperator_height) - .contained() - .with_background_color( - style - .auth - .cta_button - .style_for(state, false) - .container - .border - .color, - ) - .boxed(), - Label::new( - if copied { "Copied!" } else { "Copy" }, - style.auth.cta_button.style_for(state, false).text.clone(), - ) - .aligned() - .contained() - .with_style(style.auth.device_code_right_container) - .constrained() - .with_width(style.auth.device_code_right) - .boxed(), - ]) + let device_code_style = &style.auth.prompting.device_code; + + MouseEventHandler::::new(0, cx, |state, _cx| { + Flex::row() + .with_children([ + Label::new(data.user_code.clone(), device_code_style.text.clone()) + .aligned() .contained() - .with_style(style.auth.device_code_cta.style_for(state, false).container) + .with_style(device_code_style.left_container) .constrained() - .with_width(style.auth.content_width) - .boxed() - }) - .on_click(gpui::MouseButton::Left, { - let user_code = data.user_code.clone(); - move |_, cx| { - cx.platform() - .write_to_clipboard(ClipboardItem::new(user_code.clone())); - cx.notify(); - } - }) - .with_cursor_style(gpui::CursorStyle::PointingHand) - .boxed(), - Flex::column() - .with_children([ - Label::new( - "Paste this code into GitHub after", - style.auth.hint.text.clone(), - ) + .with_width(device_code_style.left) .boxed(), - Label::new("clicking the button below.", style.auth.hint.text.clone()) - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.hint.container.clone()) - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.device_code_group) - .aligned() - .boxed() - } - - fn render_not_authorized_warning(style: &theme::Copilot) -> ElementBox { - Flex::column() - .with_children([ - Flex::column() - .with_children([ - Label::new( - "You must have an active copilot", - style.auth.warning.text.to_owned(), - ) - .aligned() - .boxed(), - Label::new( - "license to use it in Zed.", - style.auth.warning.text.to_owned(), - ) - .aligned() - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.warning.container) - .boxed(), - Flex::column() - .with_children([ - Label::new( - "Try connecting again once you", - style.auth.hint.text.to_owned(), - ) - .aligned() - .boxed(), - Label::new( - "have activated a Copilot license.", - style.auth.hint.text.to_owned(), - ) - .aligned() - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.not_authorized_hint) - .boxed(), - ]) - .align_children_center() - .boxed() - } - - fn render_copilot_enabled(style: &theme::Copilot) -> ElementBox { - Flex::column() - .with_children([ - Label::new( - "You can update your settings or", - style.auth.hint.text.clone(), - ) - .aligned() - .boxed(), - Label::new( - "sign out from the Copilot menu in", - style.auth.hint.text.clone(), - ) - .aligned() - .boxed(), - Label::new("the status bar.", style.auth.hint.text.clone()) + Label::new( + if copied { "Copied!" } else { "Copy" }, + device_code_style.cta.style_for(state, false).text.clone(), + ) .aligned() + .contained() + .with_style(*device_code_style.right_container.style_for(state, false)) + .constrained() + .with_width(device_code_style.right) .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.enabled_hint) - .boxed() + ]) + .contained() + .with_style(device_code_style.cta.style_for(state, false).container) + .boxed() + }) + .on_click(gpui::MouseButton::Left, { + let user_code = data.user_code.clone(); + move |_, cx| { + cx.platform() + .write_to_clipboard(ClipboardItem::new(user_code.clone())); + cx.notify(); + } + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed() } fn render_prompting_modal( @@ -291,171 +191,172 @@ impl CopilotCodeVerification { style: &theme::Copilot, cx: &mut gpui::RenderContext, ) -> ElementBox { - theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| { - Flex::column() - .with_children([ - Flex::column() - .with_children([ - Flex::row() - .with_children([ - theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed() - ]) - .boxed(), - Flex::column() - .with_children([ - Label::new( - "Enable Copilot by connecting", - style.auth.enable_text.clone(), - ) - .boxed(), - Label::new( - "your existing license.", - style.auth.enable_text.clone(), - ) - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.enable_group.clone()) - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.header_group) + Flex::column() + .with_children([ + Flex::column() + .with_children([ + Label::new( + "Enable Copilot by connecting", + style.auth.prompting.subheading.text.clone(), + ) .aligned() .boxed(), - Self::render_device_code(data, &style, cx), - Flex::column() - .with_child(theme::ui::cta_button_with_click( - "Connect to GitHub", - style.auth.content_width, - &style.auth.cta_button, - cx, - { - let verification_uri = data.verification_uri.clone(); - move |_, cx| cx.platform().open_url(&verification_uri) - }, - )) - .align_children_center() - .contained() - .with_style(style.auth.github_group) + Label::new( + "your existing license.", + style.auth.prompting.subheading.text.clone(), + ) .aligned() .boxed(), - ]) - .align_children_center() - .constrained() - .with_width(style.auth.content_width) - .aligned() - .boxed() - }) + ]) + .align_children_center() + .contained() + .with_style(style.auth.prompting.subheading.container) + .boxed(), + Self::render_device_code(data, &style, cx), + Flex::column() + .with_children([ + Label::new( + "Paste this code into GitHub after", + style.auth.prompting.hint.text.clone(), + ) + .aligned() + .boxed(), + Label::new( + "clicking the button below.", + style.auth.prompting.hint.text.clone(), + ) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.prompting.hint.container.clone()) + .boxed(), + theme::ui::cta_button_with_click( + "Connect to GitHub", + style.auth.content_width, + &style.auth.cta_button, + cx, + { + let verification_uri = data.verification_uri.clone(); + move |_, cx| cx.platform().open_url(&verification_uri) + }, + ) + .boxed(), + ]) + .align_children_center() + .boxed() } fn render_enabled_modal( style: &theme::Copilot, cx: &mut gpui::RenderContext, ) -> ElementBox { - theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| { - Flex::column() - .with_children([ - Flex::column() - .with_children([ - Flex::row() - .with_children([ - theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed() - ]) - .boxed(), - Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.header_group) + let enabled_style = &style.auth.authorized; + Flex::column() + .with_children([ + Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) + .contained() + .with_style(enabled_style.subheading.container) + .aligned() + .boxed(), + Flex::column() + .with_children([ + Label::new( + "You can update your settings or", + enabled_style.hint.text.clone(), + ) .aligned() .boxed(), - Self::render_copilot_enabled(&style), - Flex::column() - .with_child(theme::ui::cta_button_with_click( - "Close", - style.auth.content_width, - &style.auth.cta_button, - cx, - |_, cx| { - let window_id = cx.window_id(); - cx.remove_window(window_id) - }, - )) - .align_children_center() - .contained() - .with_style(style.auth.github_group) + Label::new( + "sign out from the Copilot menu in", + enabled_style.hint.text.clone(), + ) .aligned() .boxed(), - ]) - .align_children_center() - .constrained() - .with_width(style.auth.content_width) - .aligned() - .boxed() - }) + Label::new("the status bar.", enabled_style.hint.text.clone()) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(enabled_style.hint.container) + .boxed(), + theme::ui::cta_button_with_click( + "Done", + style.auth.content_width, + &style.auth.cta_button, + cx, + |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id) + }, + ) + .boxed(), + ]) + .align_children_center() + .boxed() } fn render_unauthorized_modal( style: &theme::Copilot, cx: &mut gpui::RenderContext, ) -> ElementBox { - theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| { - Flex::column() - .with_children([ - Flex::column() - .with_children([ - Flex::row() - .with_children([ - theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed() - ]) - .boxed(), - Flex::column() - .with_children([ - Label::new( - "Enable Copilot by connecting", - style.auth.enable_text.clone(), - ) - .boxed(), - Label::new( - "your existing license.", - style.auth.enable_text.clone(), - ) - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.enable_group.clone()) - .boxed(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.header_group) + let unauthorized_style = &style.auth.not_authorized; + + Flex::column() + .with_children([ + Flex::column() + .with_children([ + Label::new( + "Enable Copilot by connecting", + unauthorized_style.subheading.text.clone(), + ) .aligned() .boxed(), - Self::render_not_authorized_warning(&style), - Flex::column() - .with_child(theme::ui::cta_button_with_click( - "Close", - style.auth.content_width, - &style.auth.cta_button, - cx, - |_, cx| { - let window_id = cx.window_id(); - cx.remove_window(window_id) - }, - )) - .align_children_center() - .contained() - .with_style(style.auth.github_group) + Label::new( + "your existing license.", + unauthorized_style.subheading.text.clone(), + ) .aligned() .boxed(), - ]) - .align_children_center() - .constrained() - .with_width(style.auth.content_width) - .aligned() - .boxed() - }) + ]) + .align_children_center() + .contained() + .with_style(unauthorized_style.subheading.container) + .boxed(), + Flex::column() + .with_children([ + Label::new( + "You must have an active copilot", + unauthorized_style.warning.text.clone(), + ) + .aligned() + .boxed(), + Label::new( + "license to use it in Zed.", + unauthorized_style.warning.text.clone(), + ) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(unauthorized_style.warning.container) + .boxed(), + theme::ui::cta_button_with_click( + "Subscribe on GitHub", + style.auth.content_width, + &style.auth.cta_button, + cx, + |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id); + cx.platform().open_url(COPILOT_SIGN_UP_URL) + }, + ) + .boxed(), + ]) + .align_children_center() + .boxed() } } @@ -478,13 +379,22 @@ impl View for CopilotCodeVerification { fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { let style = cx.global::().theme.clone(); - match &self.status { - Status::SigningIn { - prompt: Some(prompt), - } => Self::render_prompting_modal(&prompt, &style.copilot, cx), - Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx), - Status::Authorized => Self::render_enabled_modal(&style.copilot, cx), - _ => Empty::new().boxed(), - } + + modal("Connect Copilot to Zed", &style.copilot.modal, cx, |cx| { + Flex::column() + .with_children([ + theme::ui::icon(&style.copilot.auth.header).boxed(), + match &self.status { + Status::SigningIn { + prompt: Some(prompt), + } => Self::render_prompting_modal(&prompt, &style.copilot, cx), + Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx), + Status::Authorized => Self::render_enabled_modal(&style.copilot, cx), + _ => Empty::new().boxed(), + }, + ]) + .align_children_center() + .boxed() + }) } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1c7d0eba95..9eb796dc6f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -126,28 +126,43 @@ pub struct Copilot { #[derive(Deserialize, Default, Clone)] pub struct CopilotAuth { - pub enable_group: ContainerStyle, - pub enable_text: TextStyle, - pub instruction_text: TextStyle, - pub cta_button: ButtonStyle, pub content_width: f32, - pub copilot_plus_zed_icon: SvgStyle, - pub device_code_group: ContainerStyle, - pub github_group: ContainerStyle, - pub header_group: ContainerStyle, - pub device_code: TextStyle, - pub device_code_cta: ButtonStyle, - pub device_code_left: f32, - pub device_code_left_container: ContainerStyle, - pub device_code_right: f32, - pub device_code_right_container: ContainerStyle, - pub device_code_seperator_height: f32, + pub prompting: CopilotAuthPrompting, + pub not_authorized: CopilotAuthNotAuthorized, + pub authorized: CopilotAuthAuthorized, + pub cta_button: ButtonStyle, + pub header: IconStyle, +} + +#[derive(Deserialize, Default, Clone)] +pub struct CopilotAuthPrompting { + pub subheading: ContainedText, pub hint: ContainedText, - pub enabled_hint: ContainerStyle, - pub not_authorized_hint: ContainerStyle, + pub device_code: DeviceCode, +} + +#[derive(Deserialize, Default, Clone)] +pub struct DeviceCode { + pub text: TextStyle, + pub cta: ButtonStyle, + pub left: f32, + pub left_container: ContainerStyle, + pub right: f32, + pub right_container: Interactive, +} + +#[derive(Deserialize, Default, Clone)] +pub struct CopilotAuthNotAuthorized { + pub subheading: ContainedText, pub warning: ContainedText, } +#[derive(Deserialize, Default, Clone)] +pub struct CopilotAuthAuthorized { + pub subheading: ContainedText, + pub hint: ContainedText, +} + #[derive(Deserialize, Default)] pub struct ContactsPopover { #[serde(flatten)] diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 7518d4c304..30ccef28de 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -186,6 +186,7 @@ where cta_button_with_click(label, max_width, style, cx, move |_, cx| { cx.dispatch_action(action.clone()) }) + .boxed() } pub fn cta_button_with_click( @@ -194,7 +195,7 @@ pub fn cta_button_with_click( style: &ButtonStyle, cx: &mut RenderContext, f: F, -) -> ElementBox +) -> MouseEventHandler where L: Into>, V: View, @@ -212,7 +213,6 @@ where }) .on_click(MouseButton::Left, f) .with_cursor_style(gpui::CursorStyle::PointingHand) - .boxed() } #[derive(Clone, Deserialize, Default)] @@ -241,7 +241,8 @@ where I: Into>, F: FnOnce(&mut gpui::RenderContext) -> ElementBox, { - let active = cx.window_is_active(cx.window_id()); + const TITLEBAR_HEIGHT: f32 = 28.; + // let active = cx.window_is_active(cx.window_id()); Flex::column() .with_child( @@ -251,13 +252,13 @@ where title, style .title_text - .style_for(&mut MouseState::default(), active) + .style_for(&mut MouseState::default(), false) .clone(), ) .boxed(), // FIXME: Get a better tag type MouseEventHandler::::new(999999, cx, |state, _cx| { - let style = style.close_icon.style_for(state, active); + let style = style.close_icon.style_for(state, false); icon(style).boxed() }) .on_click(gpui::MouseButton::Left, move |_, cx| { @@ -271,11 +272,18 @@ where ]) .contained() .with_style(style.titlebar) + .constrained() + .with_height(TITLEBAR_HEIGHT) + .boxed(), + ) + .with_child( + Container::new(build_modal(cx)) + .with_style(style.container) + .constrained() + .with_width(style.dimensions().x()) + .with_height(style.dimensions().y() - TITLEBAR_HEIGHT) .boxed(), ) - .with_child(build_modal(cx)) - .contained() - .with_style(style.container) .constrained() .with_height(style.dimensions().y()) .boxed() diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index f25bc08103..c2df2e5d40 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -3,17 +3,19 @@ import { background, border, foreground, svg, text } from "./components"; export default function copilot(colorScheme: ColorScheme) { - let layer = colorScheme.highest; + let layer = colorScheme.middle; - let content_width = 304; + let content_width = 264; let ctaButton = { // Copied from welcome screen. FIXME: Move this into a ZDS component background: background(layer), - border: border(layer, "active"), + border: border(layer, "default"), cornerRadius: 4, margin: { top: 4, bottom: 4, + left: 8, + right: 8 }, padding: { top: 3, @@ -42,155 +44,183 @@ export default function copilot(colorScheme: ColorScheme) { }, modal: { titleText: { - ...text(layer, "sans", { size: "md", color: background(layer, "default") }), - active: { - ...text(layer, "sans", { size: "md" }), - } + ...text(layer, "sans", { size: "xs", "weight": "bold" }) }, titlebar: { + background: background(colorScheme.lowest), border: border(layer, "active"), padding: { - top: 8, - bottom: 8, + top: 4, + bottom: 4, left: 8, right: 8, - }, - margin: { - top: 0, - left: 0, - right: 0, - bottom: 16 } }, container: { - background: background(colorScheme.highest), - + background: background(colorScheme.lowest), + padding: { + top: 0, + left: 0, + right: 0, + bottom: 8, + } }, closeIcon: { - icon: svg(background(layer, "on"), "icons/x_mark_16.svg", 16, 16), + icon: svg(foreground(layer, "variant"), "icons/x_mark_8.svg", 8, 8), container: { cornerRadius: 2, padding: { - top: 3, - bottom: 3, - left: 7, - right: 0, + top: 4, + bottom: 4, + left: 4, + right: 4, + }, + margin: { + right: 0 } }, - active: { - icon: svg(foreground(colorScheme.lowest, "warning"), "icons/x_mark_16.svg", 16, 16), + hover: { + icon: svg(foreground(layer, "on"), "icons/x_mark_8.svg", 8, 8), }, - hoverAndActive: { - icon: svg(foreground(layer, "on", "hovered"), "icons/x_mark_16.svg", 16, 16), - }, - clickedAndactive: { - icon: svg(foreground(layer, "on", "pressed"), "icons/x_mark_16.svg", 16, 16), + clicked: { + icon: svg(foreground(layer, "base"), "icons/x_mark_8.svg", 8, 8), } }, dimensions: { - width: 400, - height: 500, + width: 280, + height: 280, }, }, + auth: { content_width, - headerGroup: { - margin: { - top: 5, - bottom: 5, - left: 0, - right: 0 - } - }, - copilotPlusZedIcon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 32, 92), - enableText: text(layer, "sans", { size: "md" }), - enableGroup: { - margin: { - top: 5, - bottom: 5, - left: 0, - right: 0 - } - }, + ctaButton, - instructionText: text(layer, "sans"), - - deviceCodeGroup: { - margin: { - top: 20, - bottom: 20, - left: 0, - right: 0 - } - }, - deviceCode: - text(layer, "mono", { size: "md" }), - deviceCodeCta: { - ...ctaButton, - padding: { - top: 0, - bottom: 0, - left: 0, - right: 0, + header: { + icon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 92, 32), + container: { + margin: { + top: 35, + bottom: 5, + left: 0, + right: 0 + } }, }, - deviceCodeLeft: content_width * 2 / 3, - deviceCodeLeftContainer: { - padding: { - top: 3, - bottom: 3, - left: 0, - right: 0, + + prompting: { + subheading: { + ...text(layer, "sans", { size: "xs" }), + margin: { + top: 6, + bottom: 12, + left: 0, + right: 0 + } + }, + + hint: { + ...text(layer, "sans", { size: "xs", color: "#838994" }), + margin: { + top: 6, + bottom: 2 + } + }, + + deviceCode: { + text: + text(layer, "mono", { size: "sm" }), + cta: { + ...ctaButton, + background: background(colorScheme.lowest), + border: border(colorScheme.lowest, "inverted"), + padding: { + top: 0, + bottom: 0, + left: 16, + right: 16, + }, + margin: { + left: 16, + right: 16, + } + }, + left: content_width / 2, + leftContainer: { + padding: { + top: 3, + bottom: 3, + left: 0, + right: 6, + }, + }, + right: content_width * 1 / 3, + rightContainer: { + border: border(colorScheme.lowest, "inverted", { bottom: false, right: false, top: false, left: true }), + padding: { + top: 3, + bottom: 5, + left: 8, + right: 0, + }, + hover: { + border: border(layer, "active", { bottom: false, right: false, top: false, left: true }), + }, + } }, }, - deviceCodeRight: content_width * 1 / 3, - deviceCodeRightContainer: { - border: border(layer, "active", { bottom: false, right: false, top: false, left: true }), - padding: { - top: 3, - bottom: 5, - left: 0, - right: 0, + + notAuthorized: { + subheading: { + ...text(layer, "sans", { size: "xs" }), + + margin: { + top: 16, + bottom: 16, + left: 0, + right: 0 + } + }, + + warning: { + ...text(layer, "sans", { size: "xs", color: foreground(layer, "warning") }), + border: border(layer, "warning"), + background: background(layer, "warning"), + cornerRadius: 2, + padding: { + top: 4, + left: 4, + bottom: 4, + right: 4, + }, + margin: { + bottom: 16, + left: 8, + right: 8 + } }, }, - deviceCodeSeperatorHeight: 0, - hint: { - ...text(layer, "sans", { size: "xs" }), - margin: { - top: -5, - } - }, - enabledHint: { - margin: { - top: 10, - bottom: 10 - } - }, - notAuthorizedHint: { - margin: { - top: 10, - bottom: 10 - } - }, - warning: { - ...text(layer, "sans", { size: "md", color: foreground(layer, "warning") }), - border: border(layer, "warning"), - background_color: background(layer, "warning"), - cornerRadius: 2, - }, + authorized: { + subheading: { + ...text(layer, "sans", { size: "xs" }), - githubGroup: { - margin: { - top: 3, - bottom: 3, - left: 0, - right: 0 - } - }, + margin: { + top: 16, + bottom: 16 + } + }, - ctaButton + hint: { + ...text(layer, "sans", { size: "xs", color: "#838994" }), + margin: { + top: 24, + bottom: 4 + } + }, + + }, } } } From c28d2c490becf1f747836ed71de14e72a7f27bc5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 16:52:55 -0700 Subject: [PATCH 50/53] Remove test modals --- crates/copilot/src/sign_in.rs | 56 ----------------------------------- 1 file changed, 56 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 46f331d8db..2827aeac2d 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -65,62 +65,6 @@ pub fn init(cx: &mut MutableAppContext) { } }) .detach(); - - // Modal theming test: - use gpui::geometry::vector::vec2f; - - let window_size = cx.global::().theme.copilot.modal.dimensions(); - let window_options = WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(0., 0.), window_size)), - titlebar: None, - center: false, - focus: false, - kind: WindowKind::PopUp, - is_movable: true, - screen: None, - }; - let (_, _view) = cx.add_window(window_options, |_cx| { - CopilotCodeVerification::new(Status::Authorized) - }); - - let window_size = cx.global::().theme.copilot.modal.dimensions(); - let window_options = WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new( - vec2f(window_size.x() + 10., 0.), - window_size, - )), - titlebar: None, - center: false, - focus: false, - kind: WindowKind::PopUp, - is_movable: true, - screen: None, - }; - let (_, _view) = cx.add_window(window_options, |_cx| { - CopilotCodeVerification::new(Status::SigningIn { - prompt: Some(PromptUserDeviceFlow { - user_code: "ABCD-1234".to_string(), - verification_uri: "https://github.com/login/device".to_string(), - }), - }) - }); - - let window_size = cx.global::().theme.copilot.modal.dimensions(); - let window_options = WindowOptions { - bounds: gpui::WindowBounds::Fixed(RectF::new( - vec2f((window_size.x() + 10.) * 2., 0.), - window_size, - )), - titlebar: None, - center: false, - focus: false, - kind: WindowKind::PopUp, - is_movable: true, - screen: None, - }; - let (_, _view) = cx.add_window(window_options, |_cx| { - CopilotCodeVerification::new(Status::Unauthorized) - }); } pub struct CopilotCodeVerification { From c3188be4c1f06078ace0773028059744eedf8c65 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 16:58:11 -0700 Subject: [PATCH 51/53] cargofmt --- crates/zed/src/languages/typescript.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 013958704b..0c6e7e3c09 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -2,8 +2,8 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; -use node_runtime::NodeRuntime; use lsp::CodeActionKind; +use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; use std::{ From e38f52d5956451051769185a474800ab4bfc12d5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 17:19:29 -0700 Subject: [PATCH 52/53] Fix unrelated panics in tests --- crates/copilot_button/src/copilot_button.rs | 51 ++++++++++++--------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 7a0a45da82..9dd03a718b 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -77,6 +77,7 @@ pub struct CopilotButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, language: Option>, + // _settings_subscription: Subscription, } impl Entity for CopilotButton { @@ -97,7 +98,10 @@ impl View for CopilotButton { let theme = settings.theme.clone(); let active = self.popup_menu.read(cx).visible(); - let status = Copilot::global(cx).unwrap().read(cx).status(); + let Some(copilot) = Copilot::global(cx) else { + return Empty::new().boxed(); + }; + let status = copilot.read(cx).status(); let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); @@ -159,23 +163,25 @@ impl View for CopilotButton { cx.spawn(|mut cx| async move { task.await; cx.update(|cx| { - let status = Copilot::global(cx).unwrap().read(cx).status(); - match status { - Status::Authorized => cx.dispatch_action_at( - window_id, - view_id, - workspace::Toast::new( - COPILOT_STARTING_TOAST_ID, - "Copilot has started!", - ), - ), - _ => { - cx.dispatch_action_at( + if let Some(copilot) = Copilot::global(cx) { + let status = copilot.read(cx).status(); + match status { + Status::Authorized => cx.dispatch_action_at( window_id, view_id, - DismissToast::new(COPILOT_STARTING_TOAST_ID), - ); - cx.dispatch_global_action(SignIn) + workspace::Toast::new( + COPILOT_STARTING_TOAST_ID, + "Copilot has started!", + ), + ), + _ => { + cx.dispatch_action_at( + window_id, + view_id, + DismissToast::new(COPILOT_STARTING_TOAST_ID), + ); + cx.dispatch_global_action(SignIn) + } } } }) @@ -220,17 +226,20 @@ impl CopilotButton { }); cx.observe(&menu, |_, _, cx| cx.notify()).detach(); - cx.observe(&Copilot::global(cx).unwrap(), |_, _, cx| cx.notify()) - .detach(); - let this_handle = cx.handle(); - cx.observe_global::(move |cx| this_handle.update(cx, |_, cx| cx.notify())) - .detach(); + + Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach()); + + // TODO: Determine why this leaked. + // let this_handle = cx.handle(); + // let sub = + // cx.observe_global::(move |cx| this_handle.update(cx, |_, cx| cx.notify())); Self { popup_menu: menu, editor_subscription: None, editor_enabled: None, language: None, + // _settings_subscription: sub, } } From 713f5f604f469bc85b27a78d1eb0ffbb696c6602 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 17:40:11 -0700 Subject: [PATCH 53/53] Fix leaked handle and failure to update language in context menu --- crates/copilot_button/src/copilot_button.rs | 27 +++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 9dd03a718b..fc6aee8721 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -77,7 +77,6 @@ pub struct CopilotButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, language: Option>, - // _settings_subscription: Subscription, } impl Entity for CopilotButton { @@ -229,17 +228,19 @@ impl CopilotButton { Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach()); - // TODO: Determine why this leaked. - // let this_handle = cx.handle(); - // let sub = - // cx.observe_global::(move |cx| this_handle.update(cx, |_, cx| cx.notify())); + let this_handle = cx.handle().downgrade(); + cx.observe_global::(move |cx| { + if let Some(handle) = this_handle.upgrade(cx) { + handle.update(cx, |_, cx| cx.notify()) + } + }) + .detach(); Self { popup_menu: menu, editor_subscription: None, editor_enabled: None, language: None, - // _settings_subscription: sub, } } @@ -323,12 +324,6 @@ impl CopilotButton { pub fn update_enabled(&mut self, editor: ViewHandle, cx: &mut ViewContext) { let editor = editor.read(cx); - if let Some(enabled) = editor.copilot_state.user_enabled { - self.editor_enabled = Some(enabled); - cx.notify(); - return; - } - let snapshot = editor.buffer().read(cx).snapshot(cx); let settings = cx.global::(); let suggestion_anchor = editor.selections.newest_anchor().start; @@ -338,7 +333,13 @@ impl CopilotButton { .map(|language| language.name()); self.language = language_name.clone(); - self.editor_enabled = Some(settings.copilot_on(language_name.as_deref())); + + if let Some(enabled) = editor.copilot_state.user_enabled { + self.editor_enabled = Some(enabled); + } else { + self.editor_enabled = Some(settings.copilot_on(language_name.as_deref())); + } + cx.notify() } }