diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 084d7fcf7a..1967c3cd14 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -202,7 +202,9 @@ impl Copilot { .spawn({ let http = http.clone(); let node_runtime = node_runtime.clone(); - move |this, cx| Self::start_language_server(http, node_runtime, this, cx) + move |this, cx| async { + Self::start_language_server(http, node_runtime, this, cx).await + } }) .shared(); diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index a65be325ce..dc09ddf3f2 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -2,12 +2,18 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ elements::*, geometry::rect::RectF, + impl_internal_actions, platform::{WindowBounds, WindowKind, WindowOptions}, AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle, }; use settings::Settings; use theme::ui::modal; +#[derive(PartialEq, Eq, Debug, Clone)] +struct ClickedConnect; + +impl_internal_actions!(copilot_verification, [ClickedConnect]); + #[derive(PartialEq, Eq, Debug, Clone)] struct CopyUserCode; @@ -56,6 +62,12 @@ pub fn init(cx: &mut AppContext) { } }) .detach(); + + cx.add_action( + |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| { + code_verification.connect_clicked = true; + }, + ); } fn create_copilot_auth_window( @@ -81,11 +93,15 @@ fn create_copilot_auth_window( pub struct CopilotCodeVerification { status: Status, + connect_clicked: bool, } impl CopilotCodeVerification { pub fn new(status: Status) -> Self { - Self { status } + Self { + status, + connect_clicked: false, + } } pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { @@ -143,6 +159,7 @@ impl CopilotCodeVerification { } fn render_prompting_modal( + connect_clicked: bool, data: &PromptUserDeviceFlow, style: &theme::Copilot, cx: &mut gpui::RenderContext, @@ -189,13 +206,20 @@ impl CopilotCodeVerification { .with_style(style.auth.prompting.hint.container.clone()) .boxed(), theme::ui::cta_button_with_click( - "Connect to GitHub", + if connect_clicked { + "Waiting for connection..." + } else { + "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) + move |_, cx| { + cx.platform().open_url(&verification_uri); + cx.dispatch_action(ClickedConnect) + } }, ) .boxed(), @@ -343,9 +367,20 @@ impl View for CopilotCodeVerification { 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), + } => Self::render_prompting_modal( + self.connect_clicked, + &prompt, + &style.copilot, + cx, + ), + Status::Unauthorized => { + self.connect_clicked = false; + Self::render_unauthorized_modal(&style.copilot, cx) + } + Status::Authorized => { + self.connect_clicked = false; + 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 8d535cc96b..b7e93536b9 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -24,6 +24,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338; #[derive(Clone, PartialEq)] pub struct DeployCopilotMenu; +#[derive(Clone, PartialEq)] +pub struct DeployCopilotStartMenu; + +#[derive(Clone, PartialEq)] +pub struct HideCopilot; + +#[derive(Clone, PartialEq)] +pub struct InitiateSignIn; + #[derive(Clone, PartialEq)] pub struct ToggleCopilotForLanguage { language: Arc, @@ -40,6 +49,9 @@ impl_internal_actions!( copilot, [ DeployCopilotMenu, + DeployCopilotStartMenu, + HideCopilot, + InitiateSignIn, DeployCopilotModal, ToggleCopilotForLanguage, ToggleCopilotGlobally, @@ -48,6 +60,7 @@ impl_internal_actions!( pub fn init(cx: &mut AppContext) { cx.add_action(CopilotButton::deploy_copilot_menu); + cx.add_action(CopilotButton::deploy_copilot_start_menu); cx.add_action( |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| { let language = action.language.clone(); @@ -73,6 +86,58 @@ pub fn init(cx: &mut AppContext) { file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) }) }); + + cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| { + SettingsFile::update(cx, move |file_contents| { + file_contents.features.copilot = Some(false) + }) + }); + + cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| { + let Some(copilot) = Copilot::global(cx) else { + return; + }; + let status = copilot.read(cx).status(); + + match status { + Status::Starting { 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(|handle, mut cx| async move { + task.await; + cx.update(|cx| { + if let Some(copilot) = Copilot::global(cx) { + let status = copilot.read(cx).status(); + match status { + Status::Authorized => cx.dispatch_action_at( + window_id, + handle.id(), + workspace::Toast::new( + COPILOT_STARTING_TOAST_ID, + "Copilot has started!", + ), + ), + _ => { + cx.dispatch_action_at( + window_id, + handle.id(), + DismissToast::new(COPILOT_STARTING_TOAST_ID), + ); + cx.dispatch_action_at(window_id, handle.id(), SignIn) + } + } + } + }) + }) + .detach(); + } + _ => cx.dispatch_action(SignIn), + } + }) } pub struct CopilotButton { @@ -109,8 +174,6 @@ impl View for CopilotButton { .editor_enabled .unwrap_or(settings.show_copilot_suggestions(None)); - let view_id = cx.view_id(); - Stack::new() .with_child( MouseEventHandler::::new(0, cx, { @@ -157,48 +220,13 @@ impl View for CopilotButton { 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| { - 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, - 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), + _ => cx.dispatch_action(DeployCopilotStartMenu), } }) .with_tooltip::( @@ -244,6 +272,26 @@ impl CopilotButton { } } + pub fn deploy_copilot_start_menu( + &mut self, + _: &DeployCopilotStartMenu, + cx: &mut ViewContext, + ) { + let mut menu_options = Vec::with_capacity(2); + + menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn)); + menu_options.push(ContextMenuItem::item("Hide Copilot", HideCopilot)); + + self.popup_menu.update(cx, |menu, cx| { + menu.show( + Default::default(), + AnchorCorner::BottomRight, + menu_options, + cx, + ); + }); + } + pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext) { let settings = cx.global::();