From 15e29d44b942d7c3d5aa51063ae53ad130d71a53 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 23 Mar 2023 19:58:36 -0700 Subject: [PATCH] Add basic copilot modal --- crates/copilot/src/auth_modal.rs | 72 +++++++++++++++++++-- crates/copilot/src/copilot.rs | 108 +++++++++++++++++++++---------- crates/theme/src/theme.rs | 5 +- styles/src/styleTree/copilot.ts | 14 +++- 4 files changed, 158 insertions(+), 41 deletions(-) diff --git a/crates/copilot/src/auth_modal.rs b/crates/copilot/src/auth_modal.rs index 4786f1d470..b9ba50507b 100644 --- a/crates/copilot/src/auth_modal.rs +++ b/crates/copilot/src/auth_modal.rs @@ -1,10 +1,19 @@ -use gpui::{elements::Label, Element, Entity, View}; +use gpui::{ + elements::{Flex, Label, MouseEventHandler, ParentElement, Stack}, + Axis, Element, Entity, View, ViewContext, +}; use settings::Settings; +use crate::{Copilot, PromptingUser}; + +pub enum Event { + Dismiss, +} + pub struct AuthModal {} impl Entity for AuthModal { - type Event = (); + type Event = Event; } impl View for AuthModal { @@ -13,8 +22,63 @@ impl View for AuthModal { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let style = &cx.global::().theme.copilot; + let style = cx.global::().theme.copilot.clone(); - Label::new("[COPILOT AUTH INFO]", style.auth_modal.clone()).boxed() + let user_code_and_url = Copilot::global(cx).read(cx).prompting_user().cloned(); + let auth_text = style.auth_text.clone(); + MouseEventHandler::::new(0, cx, move |_state, cx| { + Stack::new() + .with_child(match user_code_and_url { + Some(PromptingUser { + user_code, + verification_uri, + }) => Flex::new(Axis::Vertical) + .with_children([ + Label::new(user_code, auth_text.clone()) + .constrained() + .with_width(540.) + .boxed(), + MouseEventHandler::::new(1, cx, move |_state, _cx| { + Label::new("Click here to open github!", auth_text.clone()) + .constrained() + .with_width(540.) + .boxed() + }) + .on_click(gpui::MouseButton::Left, move |_click, cx| { + cx.platform().open_url(&verification_uri) + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed(), + ]) + .boxed(), + None => Label::new("Not signing in", style.auth_text.clone()) + .constrained() + .with_width(540.) + .boxed(), + }) + .contained() + .with_style(style.auth_modal) + .constrained() + .with_max_width(540.) + .with_max_height(420.) + .named("Copilot Authentication status modal") + }) + .on_hover(|_, _| {}) + .on_click(gpui::MouseButton::Left, |_, _| {}) + .on_click(gpui::MouseButton::Left, |_, _| {}) + .boxed() + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.emit(Event::Dismiss) + } +} + +impl AuthModal { + pub fn new(cx: &mut ViewContext) -> Self { + cx.observe(&Copilot::global(cx), |_, _, cx| cx.notify()) + .detach(); + + AuthModal {} } } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 2bdcedb2cc..a688cfe85c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -26,30 +26,44 @@ pub fn init(client: Arc, cx: &mut MutableAppContext) { let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), cx)); cx.set_global(copilot.clone()); cx.add_action(|workspace: &mut Workspace, _: &SignIn, cx| { - if let Some(copilot) = Copilot::global(cx) { - if copilot.read(cx).status() == Status::Authorized { - return; - } - - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - - workspace.toggle_modal(cx, |_workspace, cx| cx.add_view(|_cx| AuthModal {})); + let copilot = Copilot::global(cx); + if copilot.read(cx).status() == Status::Authorized { + return; } + + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + + workspace.toggle_modal(cx, |_workspace, cx| build_auth_modal(cx)); }); - cx.add_action(|_: &mut Workspace, _: &SignOut, cx| { - if let Some(copilot) = Copilot::global(cx) { - copilot - .update(cx, |copilot, cx| copilot.sign_out(cx)) - .detach_and_log_err(cx); + cx.add_action(|workspace: &mut Workspace, _: &SignOut, cx| { + let copilot = Copilot::global(cx); + + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .detach_and_log_err(cx); + + if workspace.modal::().is_some() { + workspace.dismiss_modal(cx) } }); cx.add_action(|workspace: &mut Workspace, _: &ToggleAuthStatus, cx| { - workspace.toggle_modal(cx, |_workspace, cx| cx.add_view(|_cx| AuthModal {})) + workspace.toggle_modal(cx, |_workspace, cx| build_auth_modal(cx)) }) } +fn build_auth_modal(cx: &mut gpui::ViewContext) -> gpui::ViewHandle { + let modal = cx.add_view(|cx| AuthModal::new(cx)); + + cx.subscribe(&modal, |workspace, _, e: &auth_modal::Event, cx| match e { + auth_modal::Event::Dismiss => workspace.dismiss_modal(cx), + }) + .detach(); + + modal +} + enum CopilotServer { Downloading, Error(Arc), @@ -59,10 +73,17 @@ enum CopilotServer { }, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct PromptingUser { + user_code: String, + verification_uri: String, +} + #[derive(Clone, Debug, PartialEq, Eq)] enum SignInStatus { Authorized { user: String }, Unauthorized { user: String }, + PromptingUser(PromptingUser), SignedOut, } @@ -104,20 +125,12 @@ impl Entity for Copilot { } impl Copilot { - fn global(cx: &AppContext) -> Option> { - if cx.has_global::>() { - let copilot = cx.global::>().clone(); - if copilot.read(cx).status().is_authorized() { - Some(copilot) - } else { - None - } - } else { - None - } + fn global(cx: &AppContext) -> ModelHandle { + cx.global::>().clone() } fn start(http: Arc, cx: &mut ModelContext) -> Self { + // TODO: Don't eagerly download the LSP cx.spawn(|this, mut cx| async move { let start_language_server = async { let server_path = get_lsp_binary(http).await?; @@ -164,17 +177,20 @@ impl Copilot { .request::(request::SignInInitiateParams {}) .await?; if let request::SignInInitiateResult::PromptUserDeviceFlow(flow) = sign_in { - this.update(&mut cx, |_, cx| { - cx.emit(Event::PromptUserDeviceFlow { - user_code: flow.user_code.clone(), - verification_uri: flow.verification_uri, - }); + this.update(&mut cx, |this, cx| { + this.update_prompting_user( + flow.user_code.clone(), + flow.verification_uri, + cx, + ); }); + // TODO: catch an error here and clear the corresponding user code let response = server .request::(request::SignInConfirmParams { user_code: flow.user_code, }) .await?; + this.update(&mut cx, |this, cx| this.update_sign_in_status(response, cx)); } anyhow::Ok(()) @@ -268,12 +284,38 @@ impl Copilot { CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Started { status, .. } => match status { SignInStatus::Authorized { .. } => Status::Authorized, - SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::Unauthorized { .. } | SignInStatus::PromptingUser { .. } => { + Status::Unauthorized + } SignInStatus::SignedOut => Status::SignedOut, }, } } + pub fn prompting_user(&self) -> Option<&PromptingUser> { + if let CopilotServer::Started { status, .. } = &self.server { + if let SignInStatus::PromptingUser(prompt) = status { + return Some(prompt); + } + } + None + } + + fn update_prompting_user( + &mut self, + user_code: String, + verification_uri: String, + cx: &mut ModelContext, + ) { + if let CopilotServer::Started { status, .. } = &mut self.server { + *status = SignInStatus::PromptingUser(PromptingUser { + user_code, + verification_uri, + }); + cx.notify(); + } + } + fn update_sign_in_status( &mut self, lsp_status: request::SignInStatus, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 98419d1f4c..d6a4a431e9 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -116,9 +116,10 @@ pub struct AvatarStyle { pub outer_corner_radius: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, Clone)] pub struct Copilot { - pub auth_modal: TextStyle, + pub auth_modal: ContainerStyle, + pub auth_text: TextStyle, } #[derive(Deserialize, Default)] diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index 2c087da5a0..66f5c63b4e 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -1,11 +1,21 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { text } from "./components"; +import { background, border, text } from "./components"; export default function copilot(colorScheme: ColorScheme) { let layer = colorScheme.highest; + return { - authModal: text(layer, "sans") + authModal: { + background: background(colorScheme.lowest), + border: border(colorScheme.lowest), + shadow: colorScheme.modalShadow, + cornerRadius: 12, + padding: { + bottom: 4, + }, + }, + authText: text(layer, "sans") } }