copilot: Fix references to uninitialized CopilotAuth global (#47954) (cherry-pick to preview) (#47966)

zed-zippy[bot] , Piotr Osiewicz , Anthony Eid , Cole Miller , Zed Zippy , and Anthony Eid created

Cherry-pick of #47954 to preview

----
Closes: #46593 #32635 #47924

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Cole Miller <cole@zed.dev>

Release Notes:

- Fixed issues with signing into Copilot via the Settings UI

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Zed Zippy
<234243425+zed-zippy[bot]@users.noreply.github.com>
Co-authored-by: Anthony Eid <anthony@zed.dev>

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
Co-authored-by: Anthony Eid <anthony@zed.dev>

Change summary

Cargo.lock                                                     |   2 
crates/copilot/src/copilot.rs                                  |  13 
crates/copilot_ui/src/sign_in.rs                               |  18 
crates/edit_prediction_ui/src/edit_prediction_button.rs        | 133 +--
crates/edit_prediction_ui/src/edit_prediction_ui.rs            |   4 
crates/settings_content/src/language.rs                        |  19 
crates/settings_ui/Cargo.toml                                  |   2 
crates/settings_ui/src/pages/edit_prediction_provider_setup.rs |  97 ++
8 files changed, 178 insertions(+), 110 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15124,8 +15124,10 @@ dependencies = [
  "bm25",
  "client",
  "component",
+ "copilot",
  "copilot_ui",
  "edit_prediction",
+ "edit_prediction_ui",
  "editor",
  "feature_flags",
  "fs",

crates/copilot/src/copilot.rs 🔗

@@ -271,27 +271,26 @@ impl GlobalCopilotAuth {
         fs: Arc<dyn Fs>,
         node_runtime: NodeRuntime,
         cx: &mut App,
-    ) {
+    ) -> GlobalCopilotAuth {
         let auth =
             GlobalCopilotAuth(cx.new(|cx| Copilot::new(None, server_id, fs, node_runtime, cx)));
-        cx.set_global(auth);
+        cx.set_global(auth.clone());
+        auth
     }
     pub fn try_global(cx: &mut App) -> Option<&GlobalCopilotAuth> {
         cx.try_global()
     }
 
-    pub fn get_or_init(cx: &mut App) -> Option<GlobalCopilotAuth> {
+    pub fn get_or_init(app_state: Arc<AppState>, cx: &mut App) -> GlobalCopilotAuth {
         if let Some(copilot) = cx.try_global::<Self>() {
-            Some(copilot.clone())
+            copilot.clone()
         } else {
-            let app_state = AppState::global(cx).upgrade()?;
             Self::set_global(
                 app_state.languages.next_language_server_id(),
                 app_state.fs.clone(),
                 app_state.node_runtime.clone(),
                 cx,
-            );
-            cx.try_global::<Self>().cloned()
+            )
         }
     }
 }

crates/copilot_ui/src/sign_in.rs 🔗

@@ -10,7 +10,7 @@ use gpui::{
 };
 use ui::{ButtonLike, CommonAnimationExt, ConfiguredApiCard, Vector, VectorName, prelude::*};
 use util::ResultExt as _;
-use workspace::{Toast, Workspace, notifications::NotificationId};
+use workspace::{AppState, Toast, Workspace, notifications::NotificationId};
 
 const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
 const ERROR_LABEL: &str =
@@ -457,7 +457,7 @@ impl Render for CopilotCodeVerification {
 
 pub struct ConfigurationView {
     copilot_status: Option<Status>,
-    is_authenticated: Box<dyn Fn(&App) -> bool + 'static>,
+    is_authenticated: Box<dyn Fn(&mut App) -> bool + 'static>,
     edit_prediction: bool,
     _subscription: Option<Subscription>,
 }
@@ -469,11 +469,13 @@ pub enum ConfigurationMode {
 
 impl ConfigurationView {
     pub fn new(
-        is_authenticated: impl Fn(&App) -> bool + 'static,
+        is_authenticated: impl Fn(&mut App) -> bool + 'static,
         mode: ConfigurationMode,
         cx: &mut Context<Self>,
     ) -> Self {
-        let copilot = GlobalCopilotAuth::try_global(cx).cloned();
+        let copilot = AppState::try_global(cx)
+            .and_then(|state| state.upgrade())
+            .map(|state| GlobalCopilotAuth::get_or_init(state, cx));
 
         Self {
             copilot_status: copilot.as_ref().map(|copilot| copilot.0.read(cx).status()),
@@ -567,7 +569,8 @@ impl ConfigurationView {
             .icon_position(IconPosition::Start)
             .icon_size(IconSize::Small)
             .on_click(|_, window, cx| {
-                if let Some(copilot) = GlobalCopilotAuth::get_or_init(cx) {
+                if let Some(app_state) = AppState::global(cx).upgrade() {
+                    let copilot = GlobalCopilotAuth::get_or_init(app_state, cx);
                     initiate_sign_in(copilot.0, window, cx)
                 }
             })
@@ -594,8 +597,9 @@ impl ConfigurationView {
             .icon_position(IconPosition::Start)
             .icon_size(IconSize::Small)
             .on_click(|_, window, cx| {
-                if let Some(copilot) = GlobalCopilotAuth::get_or_init(cx) {
-                    reinstall_and_sign_in(copilot.0, window, cx)
+                if let Some(app_state) = AppState::global(cx).upgrade() {
+                    let copilot = GlobalCopilotAuth::get_or_init(app_state, cx);
+                    reinstall_and_sign_in(copilot.0, window, cx);
                 }
             })
     }

crates/edit_prediction_ui/src/edit_prediction_button.rs 🔗

@@ -536,65 +536,13 @@ impl EditPredictionButton {
         }
     }
 
-    fn get_available_providers(&self, cx: &mut App) -> Vec<EditPredictionProvider> {
-        let mut providers = Vec::new();
-
-        providers.push(EditPredictionProvider::Zed);
-
-        if cx.has_flag::<Zeta2FeatureFlag>() {
-            providers.push(EditPredictionProvider::Experimental(
-                EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
-            ));
-        }
-
-        if let Some(_) = EditPredictionStore::try_global(cx)
-            .and_then(|store| store.read(cx).copilot_for_project(&self.project.upgrade()?))
-        {
-            providers.push(EditPredictionProvider::Copilot);
-        }
-
-        if let Some(supermaven) = Supermaven::global(cx) {
-            if let Supermaven::Spawned(agent) = supermaven.read(cx) {
-                if matches!(agent.account_status, AccountStatus::Ready) {
-                    providers.push(EditPredictionProvider::Supermaven);
-                }
-            }
-        }
-
-        if CodestralEditPredictionDelegate::has_api_key(cx) {
-            providers.push(EditPredictionProvider::Codestral);
-        }
-
-        if cx.has_flag::<SweepFeatureFlag>()
-            && edit_prediction::sweep_ai::sweep_api_token(cx)
-                .read(cx)
-                .has_key()
-        {
-            providers.push(EditPredictionProvider::Experimental(
-                EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
-            ));
-        }
-
-        if cx.has_flag::<MercuryFeatureFlag>()
-            && edit_prediction::mercury::mercury_api_token(cx)
-                .read(cx)
-                .has_key()
-        {
-            providers.push(EditPredictionProvider::Experimental(
-                EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
-            ));
-        }
-
-        providers
-    }
-
     fn add_provider_switching_section(
         &self,
         mut menu: ContextMenu,
         current_provider: EditPredictionProvider,
         cx: &mut App,
     ) -> ContextMenu {
-        let available_providers = self.get_available_providers(cx);
+        let available_providers = get_available_providers(cx);
 
         let providers: Vec<_> = available_providers
             .into_iter()
@@ -605,28 +553,12 @@ impl EditPredictionButton {
             menu = menu.separator().header("Providers");
 
             for provider in providers {
+                let Some(name) = provider.display_name() else {
+                    continue;
+                };
                 let is_current = provider == current_provider;
                 let fs = self.fs.clone();
 
-                let name = match provider {
-                    EditPredictionProvider::Zed => "Zed AI",
-                    EditPredictionProvider::Copilot => "GitHub Copilot",
-                    EditPredictionProvider::Supermaven => "Supermaven",
-                    EditPredictionProvider::Codestral => "Codestral",
-                    EditPredictionProvider::Experimental(
-                        EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
-                    ) => "Sweep",
-                    EditPredictionProvider::Experimental(
-                        EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
-                    ) => "Mercury",
-                    EditPredictionProvider::Experimental(
-                        EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
-                    ) => "Zeta2",
-                    EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => {
-                        continue;
-                    }
-                };
-
                 menu = menu.item(
                     ContextMenuEntry::new(name)
                         .toggleable(IconPosition::Start, is_current)
@@ -1339,7 +1271,7 @@ async fn open_disabled_globs_setting_in_editor(
     anyhow::Ok(())
 }
 
-fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
+pub fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
     update_settings_file(fs, cx, move |settings, _| {
         settings
             .project
@@ -1350,6 +1282,61 @@ fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredicti
     });
 }
 
+pub fn get_available_providers(cx: &mut App) -> Vec<EditPredictionProvider> {
+    let mut providers = Vec::new();
+
+    providers.push(EditPredictionProvider::Zed);
+
+    if cx.has_flag::<Zeta2FeatureFlag>() {
+        providers.push(EditPredictionProvider::Experimental(
+            EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
+        ));
+    }
+
+    if let Some(app_state) = workspace::AppState::global(cx).upgrade()
+        && copilot::GlobalCopilotAuth::get_or_init(app_state, cx)
+            .0
+            .read(cx)
+            .is_authenticated()
+    {
+        providers.push(EditPredictionProvider::Copilot);
+    };
+
+    if let Some(supermaven) = Supermaven::global(cx) {
+        if let Supermaven::Spawned(agent) = supermaven.read(cx) {
+            if matches!(agent.account_status, AccountStatus::Ready) {
+                providers.push(EditPredictionProvider::Supermaven);
+            }
+        }
+    }
+
+    if CodestralEditPredictionDelegate::has_api_key(cx) {
+        providers.push(EditPredictionProvider::Codestral);
+    }
+
+    if cx.has_flag::<SweepFeatureFlag>()
+        && edit_prediction::sweep_ai::sweep_api_token(cx)
+            .read(cx)
+            .has_key()
+    {
+        providers.push(EditPredictionProvider::Experimental(
+            EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
+        ));
+    }
+
+    if cx.has_flag::<MercuryFeatureFlag>()
+        && edit_prediction::mercury::mercury_api_token(cx)
+            .read(cx)
+            .has_key()
+    {
+        providers.push(EditPredictionProvider::Experimental(
+            EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
+        ));
+    }
+
+    providers
+}
+
 fn toggle_show_edit_predictions_for_language(
     language: Arc<Language>,
     fs: Arc<dyn Fs>,

crates/edit_prediction_ui/src/edit_prediction_ui.rs 🔗

@@ -16,7 +16,9 @@ use std::any::{Any as _, TypeId};
 use ui::{App, prelude::*};
 use workspace::{SplitDirection, Workspace};
 
-pub use edit_prediction_button::{EditPredictionButton, ToggleMenu};
+pub use edit_prediction_button::{
+    EditPredictionButton, ToggleMenu, get_available_providers, set_completion_provider,
+};
 
 use crate::rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag;
 

crates/settings_content/src/language.rs 🔗

@@ -167,6 +167,25 @@ impl EditPredictionProvider {
             | EditPredictionProvider::Experimental(_) => false,
         }
     }
+
+    pub fn display_name(&self) -> Option<&'static str> {
+        match self {
+            EditPredictionProvider::Zed => Some("Zed AI"),
+            EditPredictionProvider::Copilot => Some("GitHub Copilot"),
+            EditPredictionProvider::Supermaven => Some("Supermaven"),
+            EditPredictionProvider::Codestral => Some("Codestral"),
+            EditPredictionProvider::Experimental(
+                EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
+            ) => Some("Sweep"),
+            EditPredictionProvider::Experimental(
+                EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
+            ) => Some("Mercury"),
+            EditPredictionProvider::Experimental(
+                EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
+            ) => Some("Zeta2"),
+            EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => None,
+        }
+    }
 }
 
 /// The contents of the edit prediction settings.

crates/settings_ui/Cargo.toml 🔗

@@ -19,8 +19,10 @@ test-support = []
 anyhow.workspace = true
 bm25 = "2.3.2"
 component.workspace = true
+copilot.workspace = true
 copilot_ui.workspace = true
 edit_prediction.workspace = true
+edit_prediction_ui.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
 fs.workspace = true

crates/settings_ui/src/pages/edit_prediction_provider_setup.rs 🔗

@@ -1,13 +1,16 @@
 use edit_prediction::{
-    ApiKeyState, EditPredictionStore, MercuryFeatureFlag, SweepFeatureFlag,
+    ApiKeyState, MercuryFeatureFlag, SweepFeatureFlag,
     mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token},
     sweep_ai::{SWEEP_CREDENTIALS_URL, sweep_api_token},
 };
+use edit_prediction_ui::{get_available_providers, set_completion_provider};
 use feature_flags::FeatureFlagAppExt as _;
 use gpui::{Entity, ScrollHandle, prelude::*};
+use language::language_settings::AllLanguageSettings;
 use language_models::provider::mistral::{CODESTRAL_API_URL, codestral_api_key};
-use project::Project;
-use ui::{ButtonLink, ConfiguredApiCard, prelude::*};
+use settings::Settings as _;
+use ui::{ButtonLink, ConfiguredApiCard, ContextMenu, DropdownMenu, DropdownStyle, prelude::*};
+use workspace::AppState;
 
 use crate::{
     SettingField, SettingItem, SettingsFieldMetadata, SettingsPageItem, SettingsWindow, USER,
@@ -20,15 +23,9 @@ pub(crate) fn render_edit_prediction_setup_page(
     window: &mut Window,
     cx: &mut Context<SettingsWindow>,
 ) -> AnyElement {
-    let project = settings_window.original_window.as_ref().and_then(|window| {
-        window
-            .read_with(cx, |workspace, _| workspace.project().clone())
-            .ok()
-    });
     let providers = [
-        project.and_then(|project| {
-            render_github_copilot_provider(project, window, cx).map(IntoElement::into_any_element)
-        }),
+        Some(render_provider_dropdown(window, cx)),
+        render_github_copilot_provider(window, cx).map(IntoElement::into_any_element),
         cx.has_flag::<MercuryFeatureFlag>().then(|| {
             render_api_key_provider(
                 IconName::Inception,
@@ -94,6 +91,65 @@ pub(crate) fn render_edit_prediction_setup_page(
         .into_any_element()
 }
 
+fn render_provider_dropdown(window: &mut Window, cx: &mut App) -> AnyElement {
+    let current_provider = AllLanguageSettings::get_global(cx)
+        .edit_predictions
+        .provider;
+    let current_provider_name = current_provider.display_name().unwrap_or("No provider set");
+
+    let menu = ContextMenu::build(window, cx, move |mut menu, _, cx| {
+        let available_providers = get_available_providers(cx);
+        let fs = <dyn fs::Fs>::global(cx);
+
+        for provider in available_providers {
+            let Some(name) = provider.display_name() else {
+                continue;
+            };
+            let is_current = provider == current_provider;
+
+            menu = menu.toggleable_entry(name, is_current, IconPosition::Start, None, {
+                let fs = fs.clone();
+                move |_, cx| {
+                    set_completion_provider(fs.clone(), cx, provider);
+                }
+            });
+        }
+        menu
+    });
+
+    v_flex()
+        .id("provider-selector")
+        .min_w_0()
+        .gap_1p5()
+        .child(
+            SettingsSectionHeader::new("Active Provider")
+                .icon(IconName::Sparkle)
+                .no_padding(true),
+        )
+        .child(
+            h_flex()
+                .pt_2p5()
+                .w_full()
+                .justify_between()
+                .child(
+                    v_flex()
+                        .w_full()
+                        .max_w_1_2()
+                        .child(Label::new("Provider"))
+                        .child(
+                            Label::new("Select which provider to use for edit predictions.")
+                                .size(LabelSize::Small)
+                                .color(Color::Muted),
+                        ),
+                )
+                .child(
+                    DropdownMenu::new("provider-dropdown", current_provider_name, menu)
+                        .style(DropdownStyle::Outlined),
+                ),
+        )
+        .into_any_element()
+}
+
 fn render_api_key_provider(
     icon: IconName,
     title: &'static str,
@@ -330,20 +386,16 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
     ])
 }
 
-fn render_github_copilot_provider(
-    project: Entity<Project>,
-    window: &mut Window,
-    cx: &mut App,
-) -> Option<impl IntoElement> {
-    let copilot = EditPredictionStore::try_global(cx)?
-        .read(cx)
-        .copilot_for_project(&project);
+fn render_github_copilot_provider(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
     let configuration_view = window.use_state(cx, |_, cx| {
         copilot_ui::ConfigurationView::new(
             move |cx| {
-                copilot
-                    .as_ref()
-                    .is_some_and(|copilot| copilot.read(cx).is_authenticated())
+                if let Some(app_state) = AppState::global(cx).upgrade() {
+                    let copilot = copilot::GlobalCopilotAuth::get_or_init(app_state, cx);
+                    copilot.0.read(cx).is_authenticated()
+                } else {
+                    false
+                }
             },
             copilot_ui::ConfigurationMode::EditPrediction,
             cx,
@@ -354,6 +406,7 @@ fn render_github_copilot_provider(
         v_flex()
             .id("github-copilot")
             .min_w_0()
+            .pt_8()
             .gap_1p5()
             .child(
                 SettingsSectionHeader::new("GitHub Copilot")