add copilot menu

Mikayla Maki created

Change summary

crates/copilot/src/copilot.rs               |   4 
crates/copilot/src/sign_in.rs               |  47 +++++++-
crates/copilot_button/src/copilot_button.rs | 124 +++++++++++++++-------
3 files changed, 130 insertions(+), 45 deletions(-)

Detailed changes

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();
 

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<Self>) {
@@ -143,6 +159,7 @@ impl CopilotCodeVerification {
     }
 
     fn render_prompting_modal(
+        connect_clicked: bool,
         data: &PromptUserDeviceFlow,
         style: &theme::Copilot,
         cx: &mut gpui::RenderContext<Self>,
@@ -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(),
                     },
                 ])

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<str>,
@@ -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::<Self>::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::<Self, _>(
@@ -244,6 +272,26 @@ impl CopilotButton {
         }
     }
 
+    pub fn deploy_copilot_start_menu(
+        &mut self,
+        _: &DeployCopilotStartMenu,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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<Self>) {
         let settings = cx.global::<Settings>();