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 02b27566e42f29eccd5b4a0ca145567e885cf36b..0e1f5a807c913fdd3290f727d9a157abd0c4e8e5 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 9f795992d5888d24ef083abe231ed1a4edd1a950..bf9214f49ee28867d65f5ed5a0298205ce253564 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 4272d7b1afa82ea1449fbd7f55ed79f2e3585a26..3ad3380d2619f1b562af9b4594919f6ce6ec20c6 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 cb6f29a42e5855d8d142c90fcadf5e5d5e5fe1c7..9c772f519b3258ebe325b273503dfd12b82a5dd9 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 86d6bc9912c74e1e8f2efcaec0fc0b5532bcdaa9..bb39b0669989d55b121bc455d891f72a20dbd480 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 0757cebf3ad5d836edbd59f47a03cbab02c0c211..0000000000000000000000000000000000000000 --- 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 9d486619d255dd7ab214a145f7f31162dd4a9c36..7ee099dfabf8f67256e7cd945ff7981c4cd5d3e7 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 db9e0d8c487b27a7474373af9d2c25a29e04b9d7..4c12a205660f7932a6a7b412c6ee686a6199372c 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 a0a730871d01e571ab1ce67e5966cefa03c763d5..8c6b1410017f311a73981ba8368a13a8edc865b7 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 91af40dc5a688077422d4e892aad59b5ad237c10..9c0f9f3bd896e599a1f7d459a5fd049f09e60977 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 0000000000000000000000000000000000000000..bd79d053d1bdd9753594f2a7745298526e25ff58 --- /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 0000000000000000000000000000000000000000..a91608197006fa2bd0b2ac2b90c5d36548060063 --- /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 0000000000000000000000000000000000000000..097f3187b83dbc1e5e46c71f48ebc254e5a194b4 --- /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 69053e9fc4c58e0711efa9e1ecb3f9c9833d242a..427f5e62f6c82ade99c2d494d8e20236236d9ed7 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 fedfa0c863f299ccc00be3a55ca664c7f8342877..8e3fc77aa81dbacd9ffe9c58156f46a1a8868113 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 1d40dad86480a8180c85fac262f9050c04e07c91..2357052d2cb13ad819caa5eecce4f2f210675ea3 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 b13b8af956c0f174c1897a920d47555bde119543..558ca588b49215deebffd588b1603d2ec84b61ff 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 0000000000000000000000000000000000000000..c6d562d15cc38531b7453d278dc8a349c9124035 --- /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 0000000000000000000000000000000000000000..33c0ea6a1ae24d9085f14973bb015a32546fa113 --- /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 0000000000000000000000000000000000000000..e29768a53e891e165dd859fd27be5325f35cdfa5 --- /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 63c3c6d884a4e91fdeaff3766adc97a99072ec24..e38f76d8a6e294cb52555cf28d9f164a5264c7e3 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 6a5ccb8bd52e95b4aa54aa88c5b07d0f71b1906f..07b2ffd0da69512bf0c211300bc4f6168c1dad92 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 fe60065486bff9c71b00ccf477df09b1c0d55208..eb04e052860545d2d1a5c4e38d31cf907e104079 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 4d7ce828d69635a4e8f43fafb7b05e0089c0aa5e..812fae9e0a84c0a7bb80a2d2bda84bb6c2f86643 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 c49c77f076d73d344345926fb3129fe74f6cf8e7..3a23afb9700c3fdfcc02232d581854edf92bfd78 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 906592fc2d4b92f947b3328caa5182e32614fb1e..88f5c4553b84db4d610c4acf78f3a4dd8eda2990 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 9f921a0c402c1d87f21e96aa3ecded5de55b1899..ecd4028fe0bec13b782783c32ce927d82c52e963 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 8fdef507908bbd1690a2f0e7b1ce735827e2a04b..9e0dd9b582a56b672acb499d0faa498b8496d275 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 9af309839fedb17a47b98b2d45ffe7fa79b0d940..760c5f353d0d98c6f58d85d5472a8c519a2c37da 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 a2cfbac96b4ff47b7cf06e49551185f1fa1892cd..f77b264fbfacc0e1a5981ad2352699c5030fa8ee 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 479308f370e6268d25d2b7bfae04ed383432e4d2..97c158fd1f8720a6236fcfc67bd65ac3df93ded2 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 38f50d2d88710d66d43cdb7cafe932f92c1be57d..9b82713d082d5d12a0243ebaf674b9a16550c3d4 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 7ffdac5218cb92a6a50622ae91e30eb0d8a07ead..f16761d87047e75ade4acb66353341d12961c9c6 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 41cbefbb732fc14fd70397213dde450582209716..079b6a5e45a97b920925777b5d6be96d1bca49e0 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 9a09c63bb6ca2447667ba41e484a0fb990412522..3a671c60f6eacab5233ea2ce9b234438965b194f 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 662c1f464d1fd1f216b0af37925950658e083ba4..d387f815f0cd345c4173f522370ab17495989592 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 0f8e90d7b268a71712375f0fda0406f0745110d6..b95a64fa1eb688341b8489b15d54c1a05460fe38 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 f9baf4f8f78f14c9ab6a7d7616b3affd3b992c37..d3704c84c876948b1c88fd73ec014bdeccc0568d 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 b6e82842dea80c777c1885e33fedfaf8fda439d5..6028ecd13433fa3ea109c2bed41f29759aad9bf1 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 fb6c6227c35af0bf350580e873f591629532c781..c88f2e94f9a58bfc5abe30d6ea5fa9b6bbb77edd 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 788be77e7587b46f191d344867a96ca9af7539f8..32706cb47f61b966637aeeaf6fd811177789f263 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 097f3187b83dbc1e5e46c71f48ebc254e5a194b4..cf531de418ac87efde17969b7b9bde5dba5a71fd 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 33c0ea6a1ae24d9085f14973bb015a32546fa113..02082108e146e7e0ca37ee1bdaaebae1a4dc36ae 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 0e1f5a807c913fdd3290f727d9a157abd0c4e8e5..028e20d98473eebf817453712238d0d350f83377 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 8edb1957afcf13b0e01c078d27ad75335ad09a3d..6b11f5ddbc19672f4c90fc6687b17a53425f1ae4 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 bf2e583d2cd56a6f49b07b3cb1f84d68be03fdcc..6b814941b8a25ef061dfba58a0ad47816b759036 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 9c772f519b3258ebe325b273503dfd12b82a5dd9..c75adf5bfa9857c06a80ad205cae22ee2603d634 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 2ee93f1a86b7fa5e5341c03ae32f5cfdbc1acf94..b85d9992986e94060eaf7ab14ae4d41dd60dc7f9 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 516a1b4fe4a2ebaa2b5405bbb8a2ca8490039039..50f81c335ce670a44d90c11c99a0d7e0f0be13a4 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 555deff1ce6673fdf480eb7cc52c5033b1050a68..6965a3f1836e6972e9fcca3343607d89ea2f3005 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 bd79d053d1bdd9753594f2a7745298526e25ff58..4e1339ee500fe104f75919e67b1fb2fafe283e4f 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 16ec37019a70007bca4c9528d578d8d7efd353cd..767bf57ba9439d9550baabe737bb0b5df22151ea 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 e28a378a679952bfacdd81cb54738f4c23e458ec..8ef2546b5d4be8f0837d94eb19a6a20506e727cc 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 44d0936808cedec50f64f545d3db007acbc30f3e..5bbd72745d068bd95fe331899cae43bf0f409130 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 1c0d0e93ea4549d052286b4161362acacf540b78..57b91876e388f9e2c60101c514daa7a661651b42 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 a1a3dbf71a3c8f8c5151b7729cb84bdac65b1f94..8c4d8532341dfe4ec98bff1204f56ba899be359f 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 66708943f94a2913c4bd394eea6a8c931cac94ed..f4981ac13a9744e82b08eff456e947e59db6ddc8 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 bd994b5407ba6a3d5af787e50df9c77ebd432b2b..8715142dd30baa273a3fe98dfd8c410dbfdbf5f4 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 ac4badbe2a35db850793ed569876d6c5070ba433..4311f043915eb0f7d4e1db62e96c9ffcc78de6c9 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 71a6235b95e28d1a1178d9a5c6253b7d190fbf79..70032d83aaa3bb930c5b93336b9fcf8d6d3fec2c 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 319a026456b0a2b3cd500c567b01152d30a63542..8cced6d08951bdecabb22c8921c30262acf97ad4 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 aa1f49977c9f7761ecd713e8258bde7c0eaa8b1c..4370aaab06870ed8f232271a51eced39562164ad 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 e7a8079caa944633560bc245a41082197c1e9cb4..2371cfa9fd8c9ba0feffd18bd32032847d125ba7 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 6b86b19fc859ee2e40b2d3877a674bb617def09c..8c3a2fb83fecad5ed209dfd8e33cf99465de2f2e 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 e661485373f87940bdc38b3ae7b1e69ada36c321..51cb78c7a1370caad39dd884a20ea50d14908596 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 13efa10dc223162a1034eeb212fc73a0943904e2..cf509a20d34d3195caefdf58164a57229b0de5e3 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 b42a6fc674d230adeed763a4de8f10310d0b19e8..f5c144a3adb915c61ae52af38fa78cc961b00639 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 d245700d58ec1af022d12ef209f78f4690c4c04c..2b72959e25e76bfe35ea8c8ddbd921db123fc06e 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 ff71a2493e8d7c60ae35559faa6d887103f5f110..2773dd2f3b60ba27e59fa85d69b61dc684c60fc8 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 e8c03a1a5e46db578d1f4dd961acb123a5235ce0..f786d4abc6497b19c66a3ffc334afb20daf2a28e 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 566fcfe3557649bacffb4e32a5f489b9d5d9e1cd..6eeab7d7d9f807f20bd741c49804d22efae0e486 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 56a8a3c45222c8d14a774e169ef91d1169135106..56796fca592b2aba5fd1ef4f5f712f3d4dd124e5 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 1e5b9d6070b1ed5286ea1a0a11788c210f42c57e..726a1a674fd57255f6e5772ef9c60547d64154b9 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 a0ef4ad9f8c378f206c1fe60ec2fa83a4f82cbd3..cf8ff1db14fcb3202b21c26dcc872e90f417af21 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 558ca588b49215deebffd588b1603d2ec84b61ff..0e3a8d96beadccea39f2aee80988c4ce351c9634 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 dd79d56d8fd24ab32c8959682476a6c01c088165..bafa2c7a557442405f8ed5d95e3cc366e35663ca 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 9a6813f6271756e06da33ab2275f4ba80a5b0e4d..5a2380de3f4097a943d8cfc8e7f19c8926457894 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 812fae9e0a84c0a7bb80a2d2bda84bb6c2f86643..a9cbfbc7374d02deeac6f1ed0a04c257fc473081 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 effbf2ed8abbfa0f6bf35156227f2a8e4e21eeba..5a5072995fdb3468f0ff3cb244bd8a89698b450f 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 | 107 +++++++++++++++++++++++++--------- crates/copilot/src/request.rs | 22 +++++++ 2 files changed, 101 insertions(+), 28 deletions(-) create mode 100644 crates/copilot/src/request.rs diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index cf531de418ac87efde17969b7b9bde5dba5a71fd..22d1246550aed8cf915c64848d98291f133fe9f1 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(); - - 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 global(cx: &AppContext) -> Option> { + if cx.has_global::>() { + Some(cx.global::>().clone()) + } else { + None + } + } - dbg!(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)) + }; - 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 0000000000000000000000000000000000000000..3f1f66482e556f8b43dad527d2c33751007d8a90 --- /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 | 81 +++++++++++++-- 4 files changed, 222 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 028e20d98473eebf817453712238d0d350f83377..a5886a946644842831c5174e24174b029c219877 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 4e1339ee500fe104f75919e67b1fb2fafe283e4f..301051a3b0909d2536d9ff4ca5deea5ef1fa1679 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 22d1246550aed8cf915c64848d98291f133fe9f1..8bea11b1f72e155cefdf0a05f6da98a6002a5097 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 3f1f66482e556f8b43dad527d2c33751007d8a90..1b022272731e7da8bbc58c50f5346d2fc5f8cb45 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, } +impl lsp::request::Request for CheckStatus { + type Params = CheckStatusParams; + 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 CheckStatusResult { - pub status: String, - pub user: Option, +pub struct PromptUserDeviceFlow { + pub user_code: String, + pub verification_uri: String, } -impl lsp::request::Request for CheckStatus { - type Params = CheckStatusParams; - type Result = CheckStatusResult; - const METHOD: &'static str = "checkStatus"; +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 8bea11b1f72e155cefdf0a05f6da98a6002a5097..c768442611720f63381d8ef9538aebad6645c8b7 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 a5886a946644842831c5174e24174b029c219877..202b511a6d80e5276cdcd1f00c23e3c3134dc97a 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 301051a3b0909d2536d9ff4ca5deea5ef1fa1679..190a3994755f3107050369e07063c163c5e90be6 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 c768442611720f63381d8ef9538aebad6645c8b7..4abdef0ab4f9b5ef88b68d172ab245ace30be0f6 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 190a3994755f3107050369e07063c163c5e90be6..c17e7cac59f6d7e67bb0acd3d565a3a45a34a0f2 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 4abdef0ab4f9b5ef88b68d172ab245ace30be0f6..eafc4d2d98d144d8c6a5b22f19dbf97fb743ee9d 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 1b022272731e7da8bbc58c50f5346d2fc5f8cb45..3fe04532e1dfa3b14201998a50dcf1f96b0345f7 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 eafc4d2d98d144d8c6a5b22f19dbf97fb743ee9d..9a24139ad6655a86e55d52332dceb99adb3381c5 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 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 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 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?); + 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)), + }; - 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 3fe04532e1dfa3b14201998a50dcf1f96b0345f7..f3a86698e146d7e853ef89ce704e111888ae056b 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 0000000000000000000000000000000000000000..4786f1d470f4ce56c28eab35a17fe924bc274839 --- /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 9a24139ad6655a86e55d52332dceb99adb3381c5..2bdcedb2cc05f2cafee41e59b5d8dfd8cdd011d3 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 d64a1d2499a690e245e16e94b1d76dcdc7327132..98419d1f4cfe3e19e7760756b5d07e7b07f9a38d 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 423ce37d481e8f47bf9118ad6134b5c123a4ed31..f3315aa7cd9183c3560aef9388fdcbc62e3809cc 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 0000000000000000000000000000000000000000..2c087da5a0801d6301daa6160deac8e14f116814 --- /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 | 106 +++++++++++++++++++++---------- crates/theme/src/theme.rs | 5 +- styles/src/styleTree/copilot.ts | 14 +++- 4 files changed, 157 insertions(+), 40 deletions(-) diff --git a/crates/copilot/src/auth_modal.rs b/crates/copilot/src/auth_modal.rs index 4786f1d470f4ce56c28eab35a17fe924bc274839..b9ba50507b59f47da10178c3811dcc0b29ec4d7f 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(); + + 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(); - Label::new("[COPILOT AUTH INFO]", style.auth_modal.clone()).boxed() + AuthModal {} } } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 2bdcedb2cc05f2cafee41e59b5d8dfd8cdd011d3..a688cfe85cff1a1932b56a786329c94ad506575a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -26,28 +26,42 @@ 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; - } + 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); + 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 {})); - } + 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 { @@ -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 98419d1f4cfe3e19e7760756b5d07e7b07f9a38d..d6a4a431e9710c7988aa425ce54518136a3884f4 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 2c087da5a0801d6301daa6160deac8e14f116814..66f5c63b4e4f1faa4c6334cd7d006d4bf7b1fb19 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 a688cfe85cff1a1932b56a786329c94ad506575a..3c0c4929200782cf950c65634e7a9bef1490dfbb 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 | 238 +++++++++++-------------- crates/copilot/src/request.rs | 2 +- crates/copilot/src/sign_in.rs | 98 ++++++++++ crates/gpui/src/platform/mac/window.rs | 1 + 5 files changed, 202 insertions(+), 221 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 b9ba50507b59f47da10178c3811dcc0b29ec4d7f..0000000000000000000000000000000000000000 --- 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 3c0c4929200782cf950c65634e7a9bef1490dfbb..f9f019268db9990618fcce0416df35fda093da89 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, + if let CopilotServer::Started { server, status } = &mut self.server { + let task = match status { + SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { + Task::ready(Ok(())).shared() + } + 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)) + } + }) }) - .await?; - - this.update(&mut cx, |this, cx| this.update_sign_in_status(response, cx)); + .shared(); + *status = SignInStatus::SigningIn { + prompt: None, + task: task.clone(), + }; + cx.notify(); + task } - anyhow::Ok(()) - }) + }; + + 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 f3a86698e146d7e853ef89ce704e111888ae056b..ea7f4577b6864fff2d4c52916eb3eefc99e0f975 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 0000000000000000000000000000000000000000..9551b2c413931ef51da5c6d97fd9f9c58355bf70 --- /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 a0c1820368c802b5453ed33c2d8f1e50322228d8..af3e244e75168bcec79bbc5c3c26d1abd7115e9c 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 6a479681181e41ff454d2e7d50e554a9c7328598..3ff6c1d8cbb7065135282e6ae521cb1269e6a31c 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 af3e244e75168bcec79bbc5c3c26d1abd7115e9c..c954a4172961dd3750a2914f6d025ab2aabf4383 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 9551b2c413931ef51da5c6d97fd9f9c58355bf70..1b1f0f816d185ae478c30a6bae258399edb8cb59 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 202b511a6d80e5276cdcd1f00c23e3c3134dc97a..5c43455d54e85b0bdb2cbcd132653c234ca010c0 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 f9f019268db9990618fcce0416df35fda093da89..af9b7042c8da5d7732dbce9ab1536f75e4563c78 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 1b1f0f816d185ae478c30a6bae258399edb8cb59..67b93385ace370a1e36aea08c512aa314ed84c4f 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 5bbd72745d068bd95fe331899cae43bf0f409130..ef2489d7ec545e7a923c5de91d39132e2425ebb0 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 b9388dca7868e1be63059932d1e3599eec0b3efb..49c45eed1fd88b6ce371fa26b45c969fbc78d05c 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 c954a4172961dd3750a2914f6d025ab2aabf4383..5d28397f8bbeb5b3f5a148e424bb17883ee8124d 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 adda80d9edbd19ff4021d3ee91b8d3b2acc7778a..03e24c8bc34f285e17321e981af29c15cd113631 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 af9b7042c8da5d7732dbce9ab1536f75e4563c78..f0d3ed7258d6e102fa90dc194de25ccb271d7691 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 49c45eed1fd88b6ce371fa26b45c969fbc78d05c..edfacae2689670612bdecbad0b280b4f067a8dde 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 | 66 ++++++++++--------- crates/theme/src/theme.rs | 1 + styles/src/styleTree/editor.ts | 3 + 9 files changed, 93 insertions(+), 71 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index f49b9e34b8e221a3cf0e79af20c88a472fd7bd4d..7788e8d38d0ce4fcc89e38bc1e8081fa25ac96ae 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 adea668179555db73e28cb99a45396a350aedf0d..c4af03e7037d5073ae0ae76d263a3e759f680bc4 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 2d0225644fbbb77d8037976c602b92e32792cf02..e76904df174bec7f5c44f201bacdebdb06b4153d 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 45c92ea7b84ec0809213a6dc80238030d4bd1d79..b77556175d7e83c36d5b2ff4851961ced895a155 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 f0d10ad42339eb37a7e6ae292ee675c4990e8ad8..e55953d2b5a8721a1d217dbf4e932dd4c9c4bd01 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 edfacae2689670612bdecbad0b280b4f067a8dde..40428e1261a9300e600e0c4dce6651ee2276b4aa 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 780f8cd1d539bfff43d4c914f303a9e322efb71f..c349559d7dd09099368fc91eb1092c20a7fa5810 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)); - - 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 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(); + let mut diagnostic_highlight = HighlightStyle::default(); - if chunk.is_unnecessary { - diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); - } + 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(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); - } + 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 d6a4a431e9710c7988aa425ce54518136a3884f4..ef6a73f5d76130d09942d14b96e620f533c031e8 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 4a7aae4c1bf0936da11034c34629ad01256ec727..e0c73109fc6a9c9b3b02ebac6c2167f3344cbb90 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 f0d3ed7258d6e102fa90dc194de25ccb271d7691..aa36991faca9a66e3c5b6983a08e49f69cca122f 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 40428e1261a9300e600e0c4dce6651ee2276b4aa..cea0d0e8ece891f3a05aa0182027a7c3dd2e7d5d 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 e0c73109fc6a9c9b3b02ebac6c2167f3344cbb90..304ba33b034cf9515cb5536e247919f88379d1e1 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 304ba33b034cf9515cb5536e247919f88379d1e1..a1b791d00da6eb80366d08c02bfc4814893e9eb3 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 cea0d0e8ece891f3a05aa0182027a7c3dd2e7d5d..cfe803adfc2dc19ee4758dd56c7b96240e40780b 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 cfe803adfc2dc19ee4758dd56c7b96240e40780b..fca43214222b00ea16035ce6216a0b450ee5d93f 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 fca43214222b00ea16035ce6216a0b450ee5d93f..6b47034674d8dd9058a5b441fd34481f70f1acf4 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 5c43455d54e85b0bdb2cbcd132653c234ca010c0..65d68aa3a89657b57678d3e58e9eb65d9d1fcaa9 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 0000000000000000000000000000000000000000..4a7ded397623c25fa0c5dda08d639230cd1327b6 --- /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 c17e7cac59f6d7e67bb0acd3d565a3a45a34a0f2..a7582a6ffc57ad803a005efcf801004eca8f7c02 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 aa36991faca9a66e3c5b6983a08e49f69cca122f..2763eea0fd363fe175d7761db94efa943ee1b7f7 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 67b93385ace370a1e36aea08c512aa314ed84c4f..cdec0b8963cb1ce5a04875dbe3e3da6c1db24353 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 ef6a73f5d76130d09942d14b96e620f533c031e8..ce4d8a04fba102ca1f9bffb42c82aa9d35360832 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 5441e711685970d3c33c9db94f9ac03815626531..392b1134a68d9a810861e6a5b41f71301c449cce 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 3a35920b88922bb2b7bf13e5bd849acd77e4be3a..fb55c79a51e09a37d02c1ffa1e26f0522f5e282c 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 33546c997866ef5c9c79d94d17e67f8d21f7bb5f..6b21eec405a8f76caf3ee6f952e0937a0fd20b50 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 66f5c63b4e4f1faa4c6334cd7d006d4bf7b1fb19..4772a2f673ea86bb2cf71620380e64271b28856c 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), + }, + 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), + } }, - }, - authText: text(layer, "sans") + } } } diff --git a/styles/src/styleTree/welcome.ts b/styles/src/styleTree/welcome.ts index 252489ef1bdd1618693b1180e30f2c61591701e9..23e29c4a4049a5704703f8d28e874e186b83fcf7 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 1de2fe95020527765eae2c4b892642b447d11a0f..11f6561bd3f4ed37cd24a53ae941c8754b09b512 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 | 227 ++++++++++++++++++++++---------- crates/theme/src/theme.rs | 24 +++- crates/theme/src/ui.rs | 62 ++++++++- styles/src/styleTree/copilot.ts | 144 +++++++++++++++----- 5 files changed, 354 insertions(+), 111 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 2763eea0fd363fe175d7761db94efa943ee1b7f7..5ad32ed3a571512261a2e6efdb2b87d3d710e1ec 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 cdec0b8963cb1ce5a04875dbe3e3da6c1db24353..80411f18dac4743df0c36eb54391ebf0b3e9a1cf 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 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(), - ) + 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| { + 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(), + 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, + ) + .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(), - 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( + "Copy it and enter it on 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() + 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 ce4d8a04fba102ca1f9bffb42c82aa9d35360832..ae072eca32d1e3bc00ca0511b3f550a6f9385585 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 392b1134a68d9a810861e6a5b41f71301c449cce..50239bdea57dbefde47c1863fa91190ca8353d6c 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 4772a2f673ea86bb2cf71620380e64271b28856c..75fc99b591581854206b77f1b2241c96298b8cff 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 65d68aa3a89657b57678d3e58e9eb65d9d1fcaa9..7654a1ac47157d884393fdab009510cc6d9c1689 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 bf9214f49ee28867d65f5ed5a0298205ce253564..f097b5b2c7c76b407469e02c99862cae6bf5dfa0 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 a7582a6ffc57ad803a005efcf801004eca8f7c02..74dd73df0b5ebbe6ee2327051ed36a786684cf68 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 5ad32ed3a571512261a2e6efdb2b87d3d710e1ec..d12e5995d0cf109e4cc14968fac9b2cc2a31d91e 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 0000000000000000000000000000000000000000..32ab6abbb3c1b022495d286859b4826881de1f19 --- /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 02082108e146e7e0ca37ee1bdaaebae1a4dc36ae..5170bd6f4f397c77c505c606401e7f3299637b7f 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 a9cbfbc7374d02deeac6f1ed0a04c257fc473081..72470e216b47cefce856688ab15ebb7c80d48834 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 3a23afb9700c3fdfcc02232d581854edf92bfd78..12e6c1b1f2eb3f389c156686c9a7aabb0d1045ae 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 88f5c4553b84db4d610c4acf78f3a4dd8eda2990..e142028196deb8c5af3a19f32d5e5b3c1716c9af 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 ecd4028fe0bec13b782783c32ce927d82c52e963..a2debcdb2d5a3f6c08a476dd6a05bd36e9fe8315 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 9e0dd9b582a56b672acb499d0faa498b8496d275..0000000000000000000000000000000000000000 --- 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 f77b264fbfacc0e1a5981ad2352699c5030fa8ee..db743f02af743f63f339c0748eb2c392bccc6177 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 97c158fd1f8720a6236fcfc67bd65ac3df93ded2..4da105668522f80f0a7c5f27847431ec741d1516 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 f16761d87047e75ade4acb66353341d12961c9c6..2a18138cb71193c9f07e2c02f3335959e85da123 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 3a671c60f6eacab5233ea2ce9b234438965b194f..6227b8336d78f2442b717a8d51742e0c62ed3abd 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 b95a64fa1eb688341b8489b15d54c1a05460fe38..d5a67731292612bad3c70509d8cd086a2172ef49 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 d3704c84c876948b1c88fd73ec014bdeccc0568d..69a135e6ec6109f60e65f67eca015c28b0fc98ee 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 6028ecd13433fa3ea109c2bed41f29759aad9bf1..7339512f1a31233a94962b0aad00a497f2566013 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 c88f2e94f9a58bfc5abe30d6ea5fa9b6bbb77edd..8f7b858dfd716f4b77c7c6799d36e56632ac2967 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 32706cb47f61b966637aeeaf6fd811177789f263..43172d07fa29a2ab291e32d98307372737f8e3ed 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 | 514 ++++++++++++++++++++++---------- 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, 479 insertions(+), 205 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index d12e5995d0cf109e4cc14968fac9b2cc2a31d91e..71277cd67a8242de38cd8c645ef2ced770f08733 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 80411f18dac4743df0c36eb54391ebf0b3e9a1cf..9b4bc78d7c04c7a6d81a3c97ab09b9fbd64b865f 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(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.header_group) - .aligned() - .boxed(), - 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, + 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(), - Label::new( - if copied { "Copied!" } else { "Copy" }, - style - .auth - .cta_button - .style_for(state, false) - .text - .clone(), - ) - .aligned() + ]) + .align_children_center() .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_style(style.auth.enable_group.clone()) + .boxed() } - }) - .with_cursor_style(gpui::CursorStyle::PointingHand) - .boxed(), + SignInContents::Enabled => { + Label::new("Copilot Enabled!", style.auth.enable_text.clone()) + .boxed() + } + }, ]) .align_children_center() .contained() - .with_style(style.auth.device_code_group) + .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( - "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) - }, - ), - ]) + .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() @@ -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 0c8256fefb4b64e2b34d01c630ab21454552cd96..a744018e1feb9ae5d9d75c5b4ea6cbbd0b07cfce 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 ae072eca32d1e3bc00ca0511b3f550a6f9385585..29acdd92fe5b0682e4568e951d24e51fb6d2e304 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 50239bdea57dbefde47c1863fa91190ca8353d6c..7518d4c304a0f779968dbc934f531dc0be48cd07 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 75fc99b591581854206b77f1b2241c96298b8cff..fe77cab8dcf23181ef7fba5fa9dbeb1c7ef0f723 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 6b47034674d8dd9058a5b441fd34481f70f1acf4..9c1b0913aa871b7983a781198ec9db54290ff873 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 9c1b0913aa871b7983a781198ec9db54290ff873..ccd090c4092ed7b8227cd036312a8a31713d3a78 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 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 db743f02af743f63f339c0748eb2c392bccc6177..20f097ba7f8b173cdd3c74dec0ae5f711806111e 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 4da105668522f80f0a7c5f27847431ec741d1516..7919f7510d4ee77fce75f59e9a65a88df58c941a 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 6227b8336d78f2442b717a8d51742e0c62ed3abd..d5fd865221b2c37953a768ca01ca8230bc577981 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 | 109 +++++++++++------- crates/copilot/src/sign_in.rs | 6 +- crates/vim/src/vim.rs | 2 +- 7 files changed, 79 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7654a1ac47157d884393fdab009510cc6d9c1689..ade31e169ede16bde4d634f6d0feb4709244f268 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 acef74dbd64a326b82556a42326abef1df4f5da3..eb4e4d8462720c773d7e48b92f413cad2ca1970a 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 52a0e1cdc0b15f62ef2a66da56f39ac0e2169a03..229e4a04e5e52b5fbf60a9820288b8b0a61a9cc0 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 74dd73df0b5ebbe6ee2327051ed36a786684cf68..f39ab604e2e4f3d0b97714890fbf3a0f7d2a2187 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 71277cd67a8242de38cd8c645ef2ced770f08733..98107ec8cf9375ee4f98a5ff8e3edce6d47f0f22 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_auth, [SignIn, SignOut]); -actions!(copilot, [SignIn, SignOut, NextSuggestion]); +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))?; - - fs::create_dir_all(&*paths::COPILOT_DIR).await?; - 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; + async fn fetch_latest( + _http: Arc, + node: Arc, + ) -> anyhow::Result { + const COPILOT_NPM_PACKAGE: &'static str = "copilot-node-server"; + + let release = node.npm_package_latest_version(COPILOT_NPM_PACKAGE).await?; + + let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.clone())); + + 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 9b4bc78d7c04c7a6d81a3c97ab09b9fbd64b865f..6f32347aaaa68fdc94d21f98183ce50aa772732a 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 33f142c21e692294a498d951c39b3ee05a0b1cf8..34afcb5f843d4f39f0ae673ee51c0bb613f64546 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 98107ec8cf9375ee4f98a5ff8e3edce6d47f0f22..3da2e1de71bd429490b2d9347e6b276963d6051f 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 4566776a342381c5bc01b79bee46b53dc302e461..f56364cfa85d74d9a7d02ed506c61b970f6ca7c0 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 | 520 ++++++++++++++++++++-------------- 2 files changed, 303 insertions(+), 218 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 3da2e1de71bd429490b2d9347e6b276963d6051f..61bb408de400ebb77102605953fd5a9035a8d45c 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 6f32347aaaa68fdc94d21f98183ce50aa772732a..fb31f9a8e81701744b0038b17a4b9c9fabe3b508 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); - } - 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); + 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); } - 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 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 new(status: Status) -> Self { + Self { status } } - 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,28 +286,111 @@ impl CopilotCodeVerification { .with_style(style.auth.enabled_hint) .boxed() } -} - -impl Entity for CopilotCodeVerification { - type Event = (); -} -impl View for CopilotCodeVerification { - fn ui_name() -> &'static str { - "CopilotCodeVerification" - } + 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) => { - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext) { - cx.notify() - } + // } + // 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) => { - fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext) { - cx.notify() + // } + // // 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(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let style = cx.global::().theme.copilot.clone(); - + 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([ @@ -357,81 +403,89 @@ impl View for CopilotCodeVerification { 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() - } - }, + 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), - }, + 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_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) - }, + .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(), ) - } - 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) - }, - ), - } - }) + .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) @@ -446,3 +500,33 @@ impl View for CopilotCodeVerification { }) } } + +impl Entity for CopilotCodeVerification { + type Event = (); +} + +impl View for CopilotCodeVerification { + fn ui_name() -> &'static str { + "CopilotCodeVerification" + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext) { + 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(); + 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 ade31e169ede16bde4d634f6d0feb4709244f268..36c32924c5be30d12fc1bf0965abfab5941ac218 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 03e24c8bc34f285e17321e981af29c15cd113631..1a8350bb536a6fb5a09f977e36de831e7e8745a2 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 7b775d6309d66b53784db02a428729c8aea25981..fbb52e00dca9f2ebc83e6eea42864a1dfb269d58 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 f39ab604e2e4f3d0b97714890fbf3a0f7d2a2187..47f49f99102432fcdf3bcf476a90b53be5f21a8f 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 61bb408de400ebb77102605953fd5a9035a8d45c..efa693278ee803bac9e6d92dbb31a80f899f0cb0 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 0000000000000000000000000000000000000000..9c8a8c4d6e7d768cbdf9614e2680d3f9b40ae92a --- /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 fb31f9a8e81701744b0038b17a4b9c9fabe3b508..0a9299f512f298d783ef54d1fc573eac0ef77d0c 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 ccd090c4092ed7b8227cd036312a8a31713d3a78..4dbbf66a84dac168bc0980e1bce553034de79c36 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 f56364cfa85d74d9a7d02ed506c61b970f6ca7c0..6688b3c4d4eccc5ceb3bed5273676879dc739824 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 0000000000000000000000000000000000000000..35e84a6d7a121acdb18c2a1e397157e542cb88c0 --- /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 0000000000000000000000000000000000000000..91bd0095087cf5911c51dee0554b4c295f755e7a --- /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 0000000000000000000000000000000000000000..68478e62aaaa82b761602f86956e74f4eb42c5fc --- /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 | 298 +++++++++++---------------- crates/editor/src/editor.rs | 4 - crates/zed/src/zed.rs | 3 + 3 files changed, 129 insertions(+), 176 deletions(-) diff --git a/crates/copilot/src/copilot_button.rs b/crates/copilot/src/copilot_button.rs index 9c8a8c4d6e7d768cbdf9614e2680d3f9b40ae92a..b563d7e34e6d13d4d1e01dc1597e563be00be5a6 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}; - -// #[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(); -// } -// } +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}; + +const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; + +#[derive(Clone, PartialEq)] +pub struct DeployCopilotMenu; + +impl_internal_actions!(copilot, [DeployCopilotMenu]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(CopilotButton::deploy_copilot_menu); +} + +pub struct CopilotButton { + popup_menu: ViewHandle, +} + +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 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, visible); + + 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(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_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) { + cx.notify(); + } +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4dbbf66a84dac168bc0980e1bce553034de79c36..f8f83dc10136d27fe9d362b65d91cc0cd2f5be43 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 43172d07fa29a2ab291e32d98307372737f8e3ed..d9c91225c2f5978a404712bf208ca0a3c7bceb24 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 b563d7e34e6d13d4d1e01dc1597e563be00be5a6..fdc5dc776f1b7c0584294eaef06b213f5a927a2b 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 1f7c9bc8141f7710bd08aaac5fe8beae2b8306f0..84abc6e10147701f2fed38e0c476aad8c6b07930 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 f097b5b2c7c76b407469e02c99862cae6bf5dfa0..8fad52c8f4d9f2906baf4a3aa72a16e1a848f959 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 0000000000000000000000000000000000000000..561f012452cd5a7a76ecbbc4cd608f6fe0912d06 --- /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 3228f7d5a6f3a0b987e4c80694b9ff8777cfe630..b5e8696ec79f6d8661c82797ec78885d76c94f97 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 e1b9f81c1a6342a1f23377bc16f8cf985d96f2a3..ffc121576edcce232d1b45d73e51bae321c8db7a 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 efa693278ee803bac9e6d92dbb31a80f899f0cb0..6dd2f7518b58e33c947171a0824892ddc98f1b13 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 fdc5dc776f1b7c0584294eaef06b213f5a927a2b..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..7fc4204449517551186cdb0424fd1ea9d8bc1176 --- /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 0000000000000000000000000000000000000000..f44493b32394c8c64d533d451f1e201290f75716 --- /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 0000000000000000000000000000000000000000..45255b4f65eeb4fed22afa5771ed61275fe3a59e --- /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 f8f83dc10136d27fe9d362b65d91cc0cd2f5be43..e0ab8d84b45777a11dc5860d5857dea758cb3bef 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 a42dc1cfa80ed2f7f9da459614092c41a44df2c0..bf3e17e1f1d938a0c65f1a0ef76925c8f1971db0 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 6eeab7d7d9f807f20bd741c49804d22efae0e486..59728083966898da1f0be4e966b9de0df00ca7ca 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 6688b3c4d4eccc5ceb3bed5273676879dc739824..e28ce180b11a6979454ba53457415b886f5baf5c 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 From for bool { + fn from(value: OnOff) -> bool { + value.as_bool() + } } -impl Into for OnOff { - fn into(self) -> bool { - self.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 042249c265a0d3fabb25dcf95c996497da76baee..7c9f42c2f5335a0afad35f65ba5cfa85824c5e8b 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 76f46f83c5f87c88b8dd5c68e2e2d98e39798a79..f19f876be5bfaeb120fd1483e4a80c8f187ce2bf 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 eb04e052860545d2d1a5c4e38d31cf907e104079..83b87b92218491fcdf6acc14ec907a343c8074a3 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 c13ae2411c480740715caac9f4d07ffffcd8a733..2d59d8f30913df4b0a4d32ad01e0046ea2779f9a 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 d9c91225c2f5978a404712bf208ca0a3c7bceb24..01b493bf7dc59b05fa319ba070df690778b2d5d1 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 fe77cab8dcf23181ef7fba5fa9dbeb1c7ef0f723..106fed298fac8d0edafb05cfa509cba17e52d716 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 0a9299f512f298d783ef54d1fc573eac0ef77d0c..cce064160e9816a20fc2bf39db850d592091358d 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 45255b4f65eeb4fed22afa5771ed61275fe3a59e..e61a8814af3477d3b4eef5c282cda87bec47ef0c 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 e28ce180b11a6979454ba53457415b886f5baf5c..2e087dd26080a593830815f8aae93a91eb168200 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 a91608197006fa2bd0b2ac2b90c5d36548060063..0000000000000000000000000000000000000000 --- 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 2e087dd26080a593830815f8aae93a91eb168200..5825e48d5a6e2019027237341aa1ad6c6f1db2b4 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 84abc6e10147701f2fed38e0c476aad8c6b07930..403dffadfc5666c93fef74c5b1435271ef206a2c 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 47f49f99102432fcdf3bcf476a90b53be5f21a8f..5c9ef2d7c4244041ba27bb25df41aec944a2bb9e 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 6dd2f7518b58e33c947171a0824892ddc98f1b13..8a72f57b95f3101a270583868067574ea12f9d36 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"; - - let release = node.npm_package_latest_version(COPILOT_NPM_PACKAGE).await?; + async fn fetch_latest(http: Arc) -> anyhow::Result { + let release = latest_github_release("zed-industries/copilot", http.clone()).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 5825e48d5a6e2019027237341aa1ad6c6f1db2b4..1c796ad5c3fdf6a7b9af165ea227c94bccfc6457 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 07b2ffd0da69512bf0c211300bc4f6168c1dad92..d9db47c2bb1476a9dbdcb512bf5427cfb550bc77 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 35e84a6d7a121acdb18c2a1e397157e542cb88c0..e14b61ce8bc73cc09242256706283e7e2831f8fb 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 0000000000000000000000000000000000000000..8ee145517385f22d64da54ed79913651629eef36 --- /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 0000000000000000000000000000000000000000..fd00593dc95ad36815e54104a305e1c989c5e797 --- /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 68478e62aaaa82b761602f86956e74f4eb42c5fc..0d67201df6d7912311f8bfd71cb330117357adee 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 8ee145517385f22d64da54ed79913651629eef36..0000000000000000000000000000000000000000 --- 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 91bd0095087cf5911c51dee0554b4c295f755e7a..eba36a2b692aca5841d5f3a8d131df980004fd9b 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 fd00593dc95ad36815e54104a305e1c989c5e797..6069c554f1da71202b57a541a7b15195287723c9 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 0d67201df6d7912311f8bfd71cb330117357adee..6cbf63fb49324409a096df8d4c60e5ba6bd7a87d 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 0000000000000000000000000000000000000000..d024678c500640dc53eadeea0987e9c20070629b --- /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 8a72f57b95f3101a270583868067574ea12f9d36..727b9d2d4fd4889a8845a4abfd6ef176bae2b45a 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 cce064160e9816a20fc2bf39db850d592091358d..168ba712ced815ce35975e67f589e941c28e8ee1 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 e61a8814af3477d3b4eef5c282cda87bec47ef0c..7a0a45da828aef2804a47eee9fa9a2bdc1356da2 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 7c9f42c2f5335a0afad35f65ba5cfa85824c5e8b..1c7d0eba95576f4ef63102c9c0be680fe287175b 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 f19f876be5bfaeb120fd1483e4a80c8f187ce2bf..1cb5d3f50d7539885e0b4db317238e3263c3169c 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 83b87b92218491fcdf6acc14ec907a343c8074a3..3fffe57e3e8f494d61b85a0152944ce9d2c97f06 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 106fed298fac8d0edafb05cfa509cba17e52d716..f25bc08103deb1432cdb8e6581cf8fe64456c75f 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 36b295c640241ce161fecff3450a85421aca75e3..dde689e9bd0563145f0b91ac4109a55eb5911d6c 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 | 528 +++++++++++++------------------- crates/theme/src/theme.rs | 49 ++- crates/theme/src/ui.rs | 24 +- styles/src/styleTree/copilot.ts | 260 +++++++++------- 4 files changed, 412 insertions(+), 449 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 168ba712ced815ce35975e67f589e941c28e8ee1..46f331d8dba9bbf16223dfef6bd1982bb1185db3 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,323 +147,216 @@ 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() + Label::new( + if copied { "Copied!" } else { "Copy" }, + device_code_style.cta.style_for(state, false).text.clone(), + ) + .aligned() .contained() - .with_style(style.auth.hint.container.clone()) + .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.device_code_group) - .aligned() - .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_not_authorized_warning(style: &theme::Copilot) -> ElementBox { + fn render_prompting_modal( + data: &PromptUserDeviceFlow, + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { Flex::column() .with_children([ Flex::column() .with_children([ Label::new( - "You must have an active copilot", - style.auth.warning.text.to_owned(), + "Enable Copilot by connecting", + style.auth.prompting.subheading.text.clone(), ) .aligned() .boxed(), Label::new( - "license to use it in Zed.", - style.auth.warning.text.to_owned(), + "your existing license.", + style.auth.prompting.subheading.text.clone(), ) .aligned() .boxed(), ]) .align_children_center() .contained() - .with_style(style.auth.warning.container) + .with_style(style.auth.prompting.subheading.container) .boxed(), + Self::render_device_code(data, &style, cx), Flex::column() .with_children([ Label::new( - "Try connecting again once you", - style.auth.hint.text.to_owned(), + "Paste this code into GitHub after", + style.auth.prompting.hint.text.clone(), ) .aligned() .boxed(), Label::new( - "have activated a Copilot license.", - style.auth.hint.text.to_owned(), + "clicking the button below.", + style.auth.prompting.hint.text.clone(), ) .aligned() .boxed(), ]) .align_children_center() .contained() - .with_style(style.auth.not_authorized_hint) + .with_style(style.auth.prompting.hint.container.clone()) .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(), + 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) + }, ) - .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() } - - fn render_prompting_modal( - data: &PromptUserDeviceFlow, + 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(), - 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 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_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( + "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_enabled_modal( + 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(), - Label::new("Copilot Enabled!", style.auth.enable_text.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_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( + "your existing license.", + unauthorized_style.subheading.text.clone(), + ) .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_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) + ]) + .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(), - 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( + "license to use it in Zed.", + unauthorized_style.warning.text.clone(), + ) .aligned() .boxed(), - ]) - .align_children_center() - .constrained() - .with_width(style.auth.content_width) - .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 1c7d0eba95576f4ef63102c9c0be680fe287175b..9eb796dc6f83f7bcde866d1025d553095ab77514 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 7518d4c304a0f779968dbc934f531dc0be48cd07..30ccef28defd36b9b17c02d23a66eeac208a3147 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 f25bc08103deb1432cdb8e6581cf8fe64456c75f..c2df2e5d405917606831b93b649d72ccec3ab2af 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: { + } + }, + container: { + background: background(colorScheme.lowest), + padding: { top: 0, left: 0, right: 0, - bottom: 16 + bottom: 8, } }, - container: { - background: background(colorScheme.highest), - - }, 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, + + header: { + icon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 92, 32), + container: { + margin: { + top: 35, + bottom: 5, + left: 0, + right: 0 + } + }, }, - instructionText: text(layer, "sans"), + prompting: { + subheading: { + ...text(layer, "sans", { size: "xs" }), + margin: { + top: 6, + bottom: 12, + left: 0, + right: 0 + } + }, - 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, + hint: { + ...text(layer, "sans", { size: "xs", color: "#838994" }), + margin: { + top: 6, + bottom: 2 + } }, - }, - deviceCodeLeft: content_width * 2 / 3, - deviceCodeLeftContainer: { - padding: { - top: 3, - bottom: 3, - left: 0, - right: 0, + + 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 + } }, - }, - 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, + 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 + } + }, }, - githubGroup: { - margin: { - top: 3, - bottom: 3, - left: 0, - right: 0 - } - }, + authorized: { + subheading: { + ...text(layer, "sans", { size: "xs" }), + + 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 46f331d8dba9bbf16223dfef6bd1982bb1185db3..2827aeac2d4fac726f4f6b70019d413784ade2a8 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 013958704b140d1a55f6f3bf48658d01d53feb7a..0c6e7e3c0903cb1cd13304b28f47183503c28c4d 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 7a0a45da828aef2804a47eee9fa9a2bdc1356da2..9dd03a718b3fcec93d42eec74642972b76092a6f 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 9dd03a718b3fcec93d42eec74642972b76092a6f..fc6aee872134b9359e3611a0a341ab0590415cbf 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() } }