diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 11c1ee83d8..fed19c7791 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -170,7 +170,9 @@ enum SignInStatus { prompt: Option, task: Shared>>>, }, - SignedOut, + SignedOut { + awaiting_signing_in: bool, + }, } #[derive(Debug, Clone)] @@ -180,7 +182,9 @@ pub enum Status { }, Error(Arc), Disabled, - SignedOut, + SignedOut { + awaiting_signing_in: bool, + }, SigningIn { prompt: Option, }, @@ -345,8 +349,8 @@ impl Copilot { buffers: Default::default(), _subscription: cx.on_app_quit(Self::shutdown_language_server), }; - this.enable_or_disable_copilot(cx); - cx.observe_global::(move |this, cx| this.enable_or_disable_copilot(cx)) + this.start_copilot(true, false, cx); + cx.observe_global::(move |this, cx| this.start_copilot(true, false, cx)) .detach(); this } @@ -364,26 +368,40 @@ impl Copilot { } } - fn enable_or_disable_copilot(&mut self, cx: &mut Context) { + fn start_copilot( + &mut self, + check_edit_prediction_provider: bool, + awaiting_sign_in_after_start: bool, + cx: &mut Context, + ) { + if !matches!(self.server, CopilotServer::Disabled) { + return; + } + let language_settings = all_language_settings(None, cx); + if check_edit_prediction_provider + && language_settings.edit_predictions.provider != EditPredictionProvider::Copilot + { + return; + } let server_id = self.server_id; let http = self.http.clone(); let node_runtime = self.node_runtime.clone(); - let language_settings = all_language_settings(None, cx); - if language_settings.edit_predictions.provider == EditPredictionProvider::Copilot { - if matches!(self.server, CopilotServer::Disabled) { - let env = self.build_env(&language_settings.edit_predictions.copilot); - let start_task = cx - .spawn(move |this, cx| { - Self::start_language_server(server_id, http, node_runtime, env, this, cx) - }) - .shared(); - self.server = CopilotServer::Starting { task: start_task }; - cx.notify(); - } - } else { - self.server = CopilotServer::Disabled; - cx.notify(); - } + let env = self.build_env(&language_settings.edit_predictions.copilot); + let start_task = cx + .spawn(move |this, cx| { + Self::start_language_server( + server_id, + http, + node_runtime, + env, + this, + awaiting_sign_in_after_start, + cx, + ) + }) + .shared(); + self.server = CopilotServer::Starting { task: start_task }; + cx.notify(); } fn build_env(&self, copilot_settings: &CopilotSettings) -> Option> { @@ -449,6 +467,7 @@ impl Copilot { node_runtime: NodeRuntime, env: Option>, this: WeakEntity, + awaiting_sign_in_after_start: bool, mut cx: AsyncApp, ) { let start_language_server = async { @@ -522,7 +541,9 @@ impl Copilot { Ok((server, status)) => { this.server = CopilotServer::Running(RunningCopilotServer { lsp: server, - sign_in_status: SignInStatus::SignedOut, + sign_in_status: SignInStatus::SignedOut { + awaiting_signing_in: awaiting_sign_in_after_start, + }, registered_buffers: Default::default(), }); cx.emit(Event::CopilotLanguageServerStarted); @@ -545,7 +566,7 @@ impl Copilot { cx.notify(); task.clone() } - SignInStatus::SignedOut | SignInStatus::Unauthorized { .. } => { + SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => { let lsp = server.lsp.clone(); let task = cx .spawn(|this, mut cx| async move { @@ -633,10 +654,6 @@ impl Copilot { anyhow::Ok(()) }) } - CopilotServer::Disabled => cx.background_spawn(async move { - clear_copilot_config_dir().await; - anyhow::Ok(()) - }), _ => Task::ready(Err(anyhow!("copilot hasn't started yet"))), } } @@ -651,7 +668,8 @@ impl Copilot { let server_id = self.server_id; move |this, cx| async move { clear_copilot_dir().await; - Self::start_language_server(server_id, http, node_runtime, env, this, cx).await + Self::start_language_server(server_id, http, node_runtime, env, this, false, cx) + .await } }) .shared(); @@ -961,7 +979,11 @@ impl Copilot { SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { prompt: prompt.clone(), }, - SignInStatus::SignedOut => Status::SignedOut, + SignInStatus::SignedOut { + awaiting_signing_in, + } => Status::SignedOut { + awaiting_signing_in: *awaiting_signing_in, + }, } } } @@ -990,7 +1012,11 @@ impl Copilot { } } request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => { - server.sign_in_status = SignInStatus::SignedOut; + if !matches!(server.sign_in_status, SignInStatus::SignedOut { .. }) { + server.sign_in_status = SignInStatus::SignedOut { + awaiting_signing_in: false, + }; + } cx.emit(Event::CopilotAuthSignedOut); for buffer in self.buffers.iter().cloned().collect::>() { self.unregister_buffer(&buffer); @@ -1021,10 +1047,6 @@ async fn clear_copilot_dir() { remove_matching(paths::copilot_dir(), |_| true).await } -async fn clear_copilot_config_dir() { - remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await -} - async fn get_copilot_lsp(http: Arc) -> anyhow::Result { const SERVER_PATH: &str = "dist/language-server.js"; diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index d10543a360..3ae6078f1d 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,9 +1,10 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - div, App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled, - Subscription, Window, + div, percentage, svg, Animation, AnimationExt, App, ClipboardItem, Context, DismissEvent, + Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, + MouseDownEvent, ParentElement, Render, Styled, Subscription, Transformation, Window, }; +use std::time::Duration; use ui::{prelude::*, Button, Label, Vector, VectorName}; use util::ResultExt as _; use workspace::notifications::NotificationId; @@ -17,11 +18,13 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) { let Some(copilot) = Copilot::global(cx) else { return; }; - let status = copilot.read(cx).status(); let Some(workspace) = window.root::().flatten() else { return; }; - match status { + if matches!(copilot.read(cx).status(), Status::Disabled) { + copilot.update(cx, |this, cx| this.start_copilot(false, true, cx)); + } + match copilot.read(cx).status() { Status::Starting { task } => { workspace.update(cx, |workspace, cx| { workspace.show_toast( @@ -54,6 +57,15 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) { copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) .detach_and_log_err(cx); + if let Some(window_handle) = cx.active_window() { + window_handle + .update(cx, |_, window, cx| { + workspace.toggle_modal(window, cx, |_, cx| { + CopilotCodeVerification::new(&copilot, cx) + }); + }) + .log_err(); + } } }) .log_err(); @@ -76,6 +88,7 @@ pub struct CopilotCodeVerification { status: Status, connect_clicked: bool, focus_handle: FocusHandle, + copilot: Entity, _subscription: Subscription, } @@ -86,7 +99,20 @@ impl Focusable for CopilotCodeVerification { } impl EventEmitter for CopilotCodeVerification {} -impl ModalView for CopilotCodeVerification {} +impl ModalView for CopilotCodeVerification { + fn on_before_dismiss( + &mut self, + _: &mut Window, + cx: &mut Context, + ) -> workspace::DismissDecision { + self.copilot.update(cx, |copilot, cx| { + if matches!(copilot.status(), Status::SigningIn { .. }) { + copilot.sign_out(cx).detach_and_log_err(cx); + } + }); + workspace::DismissDecision::Dismiss(true) + } +} impl CopilotCodeVerification { pub fn new(copilot: &Entity, cx: &mut Context) -> Self { @@ -95,6 +121,7 @@ impl CopilotCodeVerification { status, connect_clicked: false, focus_handle: cx.focus_handle(), + copilot: copilot.clone(), _subscription: cx.observe(copilot, |this, copilot, cx| { let status = copilot.read(cx).status(); match status { @@ -180,9 +207,12 @@ impl CopilotCodeVerification { .child( Button::new("copilot-enable-cancel-button", "Cancel") .full_width() - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), + .on_click(cx.listener(|_, _, _, cx| { + cx.emit(DismissEvent); + })), ) } + fn render_enabled_modal(cx: &mut Context) -> impl Element { v_flex() .gap_2() @@ -216,16 +246,27 @@ impl CopilotCodeVerification { ) } - fn render_disabled_modal() -> impl Element { - v_flex() - .child(Headline::new("Copilot is disabled").size(HeadlineSize::Large)) - .child(Label::new("You can enable Copilot in your settings.")) + fn render_loading(window: &mut Window, _: &mut Context) -> impl Element { + let loading_icon = svg() + .size_8() + .path(IconName::ArrowCircle.path()) + .text_color(window.text_style().color) + .with_animation( + "icon_circle_arrow", + Animation::new(Duration::from_secs(2)).repeat(), + |svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))), + ); + + h_flex().justify_center().child(loading_icon) } } impl Render for CopilotCodeVerification { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let prompt = match &self.status { + Status::SigningIn { prompt: None } => { + Self::render_loading(window, cx).into_any_element() + } Status::SigningIn { prompt: Some(prompt), } => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(), @@ -237,10 +278,6 @@ impl Render for CopilotCodeVerification { self.connect_clicked = false; Self::render_enabled_modal(cx).into_any_element() } - Status::Disabled => { - self.connect_clicked = false; - Self::render_disabled_modal().into_any_element() - } _ => div().into_any_element(), }; diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 4e373f6b0d..5f51c5c2ee 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -580,8 +580,6 @@ pub struct CopilotSettingsContent { #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct FeaturesContent { - /// Whether the GitHub Copilot feature is enabled. - pub copilot: Option, /// Determines which edit prediction provider to use. pub edit_prediction_provider: Option, } @@ -1158,7 +1156,6 @@ impl settings::Settings for AllLanguageSettings { languages.insert(language_name.clone(), language_settings); } - let mut copilot_enabled = default_value.features.as_ref().and_then(|f| f.copilot); let mut edit_prediction_provider = default_value .features .as_ref() @@ -1205,9 +1202,6 @@ impl settings::Settings for AllLanguageSettings { } for user_settings in sources.customizations() { - if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) { - copilot_enabled = Some(copilot); - } if let Some(provider) = user_settings .features .as_ref() @@ -1282,8 +1276,6 @@ impl settings::Settings for AllLanguageSettings { edit_predictions: EditPredictionSettings { provider: if let Some(provider) = edit_prediction_provider { provider - } else if copilot_enabled.unwrap_or(true) { - EditPredictionProvider::Copilot } else { EditPredictionProvider::None }, diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index c680cf78e7..70221a318d 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -129,7 +129,7 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider { Status::Error(err) => anyhow!(format!("Received the following error while signing into Copilot: {err}")), Status::Starting { task: _ } => anyhow!("Copilot is still starting, please wait for Copilot to start then try again"), Status::Unauthorized => anyhow!("Unable to authorize with Copilot. Please make sure that you have an active Copilot and Copilot Chat subscription."), - Status::SignedOut => anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again."), + Status::SignedOut {..} => anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again."), Status::SigningIn { prompt: _ } => anyhow!("Still signing into Copilot..."), }; @@ -366,7 +366,6 @@ impl Render for ConfigurationView { match &self.copilot_status { Some(status) => match status { - Status::Disabled => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)), Status::Starting { task: _ } => { const LABEL: &str = "Starting Copilot..."; v_flex() @@ -376,7 +375,10 @@ impl Render for ConfigurationView { .child(Label::new(LABEL)) .child(loading_icon) } - Status::SigningIn { prompt: _ } => { + Status::SigningIn { prompt: _ } + | Status::SignedOut { + awaiting_signing_in: true, + } => { const LABEL: &str = "Signing in to Copilot..."; v_flex() .gap_6()