edit prediction: Drop project state after a project goes away (#48191) (cherry-pick to preview) (#48206)

zed-zippy[bot] and Piotr Osiewicz created

Cherry-pick of #48191 to preview

----
Closes #48097

Release Notes:

- Fixed Copilot instances not being cleared up after their window is
closed.
- Copilot edit prediction provider now respects `disable_ai` setting.

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>

Change summary

Cargo.lock                                                     |  1 
crates/copilot/src/copilot.rs                                  | 21 ++-
crates/copilot_ui/Cargo.toml                                   |  1 
crates/copilot_ui/src/copilot_ui.rs                            |  6 
crates/copilot_ui/src/sign_in.rs                               | 12 +
crates/edit_prediction/src/edit_prediction.rs                  | 17 ++
crates/edit_prediction_ui/src/edit_prediction_button.rs        |  6 
crates/settings_ui/src/pages/edit_prediction_provider_setup.rs |  4 
8 files changed, 46 insertions(+), 22 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3743,6 +3743,7 @@ dependencies = [
  "log",
  "lsp",
  "menu",
+ "project",
  "serde_json",
  "settings",
  "ui",

crates/copilot/src/copilot.rs 🔗

@@ -281,16 +281,25 @@ impl GlobalCopilotAuth {
         cx.try_global()
     }
 
-    pub fn get_or_init(app_state: Arc<AppState>, cx: &mut App) -> GlobalCopilotAuth {
-        if let Some(copilot) = cx.try_global::<Self>() {
-            copilot.clone()
-        } else {
-            Self::set_global(
+    pub fn try_get_or_init(app_state: Arc<AppState>, cx: &mut App) -> Option<GlobalCopilotAuth> {
+        let ai_enabled = !DisableAiSettings::get(None, cx).disable_ai;
+
+        if let Some(copilot) = cx.try_global::<Self>().cloned() {
+            if ai_enabled {
+                Some(copilot)
+            } else {
+                cx.remove_global::<Self>();
+                None
+            }
+        } else if ai_enabled {
+            Some(Self::set_global(
                 app_state.languages.next_language_server_id(),
                 app_state.fs.clone(),
                 app_state.node_runtime.clone(),
                 cx,
-            )
+            ))
+        } else {
+            None
         }
     }
 }

crates/copilot_ui/Cargo.toml 🔗

@@ -27,6 +27,7 @@ language.workspace = true
 log.workspace = true
 lsp.workspace = true
 menu.workspace = true
+project.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 ui.workspace = true

crates/copilot_ui/src/copilot_ui.rs 🔗

@@ -5,6 +5,7 @@ use std::sync::Arc;
 use copilot::GlobalCopilotAuth;
 use gpui::AppContext;
 use language::language_settings::AllLanguageSettings;
+use project::DisableAiSettings;
 use settings::SettingsStore;
 pub use sign_in::{
     ConfigurationMode, ConfigurationView, CopilotCodeVerification, initiate_sign_in,
@@ -14,13 +15,16 @@ use ui::App;
 use workspace::AppState;
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut App) {
+    let disable_ai = cx.read_global(|settings: &SettingsStore, _| {
+        settings.get::<DisableAiSettings>(None).disable_ai
+    });
     let provider = cx.read_global(|settings: &SettingsStore, _| {
         settings
             .get::<AllLanguageSettings>(None)
             .edit_predictions
             .provider
     });
-    if provider == settings::EditPredictionProvider::Copilot {
+    if !disable_ai && provider == settings::EditPredictionProvider::Copilot {
         GlobalCopilotAuth::set_global(
             app_state.languages.next_language_server_id(),
             app_state.fs.clone(),

crates/copilot_ui/src/sign_in.rs 🔗

@@ -475,7 +475,7 @@ impl ConfigurationView {
     ) -> Self {
         let copilot = AppState::try_global(cx)
             .and_then(|state| state.upgrade())
-            .map(|state| GlobalCopilotAuth::get_or_init(state, cx));
+            .and_then(|state| GlobalCopilotAuth::try_get_or_init(state, cx));
 
         Self {
             copilot_status: copilot.as_ref().map(|copilot| copilot.0.read(cx).status()),
@@ -569,8 +569,9 @@ impl ConfigurationView {
             .icon_position(IconPosition::Start)
             .icon_size(IconSize::Small)
             .on_click(|_, window, cx| {
-                if let Some(app_state) = AppState::global(cx).upgrade() {
-                    let copilot = GlobalCopilotAuth::get_or_init(app_state, cx);
+                if let Some(app_state) = AppState::global(cx).upgrade()
+                    && let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx)
+                {
                     initiate_sign_in(copilot.0, window, cx)
                 }
             })
@@ -597,8 +598,9 @@ impl ConfigurationView {
             .icon_position(IconPosition::Start)
             .icon_size(IconSize::Small)
             .on_click(|_, window, cx| {
-                if let Some(app_state) = AppState::global(cx).upgrade() {
-                    let copilot = GlobalCopilotAuth::get_or_init(app_state, cx);
+                if let Some(app_state) = AppState::global(cx).upgrade()
+                    && let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx)
+                {
                     reinstall_and_sign_in(copilot.0, window, cx);
                 }
             })

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -30,11 +30,11 @@ use language::language_settings::all_language_settings;
 use language::{Anchor, Buffer, File, Point, TextBufferSnapshot, ToOffset, ToPoint};
 use language::{BufferSnapshot, OffsetRangeExt};
 use language_model::{LlmApiToken, NeedsLlmTokenRefresh, RefreshLlmTokenListener};
-use project::{Project, ProjectPath, WorktreeId};
+use project::{DisableAiSettings, Project, ProjectPath, WorktreeId};
 use release_channel::AppVersion;
 use semver::Version;
 use serde::de::DeserializeOwned;
-use settings::{EditPredictionProvider, update_settings_file};
+use settings::{EditPredictionProvider, Settings as _, update_settings_file};
 use std::collections::{VecDeque, hash_map};
 use text::Edit;
 use workspace::Workspace;
@@ -263,7 +263,7 @@ struct ProjectState {
     context: Entity<RelatedExcerptStore>,
     license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
     user_actions: VecDeque<UserActionRecord>,
-    _subscription: gpui::Subscription,
+    _subscriptions: [gpui::Subscription; 2],
     copilot: Option<Entity<Copilot>>,
 }
 
@@ -725,6 +725,9 @@ impl EditPredictionStore {
         project: &Entity<Project>,
         cx: &mut Context<Self>,
     ) -> Option<Entity<Copilot>> {
+        if DisableAiSettings::get(None, cx).disable_ai {
+            return None;
+        }
         let state = self.get_or_init_project(project, cx);
 
         if state.copilot.is_some() {
@@ -815,7 +818,13 @@ impl EditPredictionStore {
                 last_prediction_refresh: None,
                 license_detection_watchers: HashMap::default(),
                 user_actions: VecDeque::with_capacity(USER_ACTION_HISTORY_SIZE),
-                _subscription: cx.subscribe(&project, Self::handle_project_event),
+                _subscriptions: [
+                    cx.subscribe(&project, Self::handle_project_event),
+                    cx.observe_release(&project, move |this, _, cx| {
+                        this.projects.remove(&entity_id);
+                        cx.notify();
+                    }),
+                ],
                 copilot: None,
             })
     }

crates/edit_prediction_ui/src/edit_prediction_button.rs 🔗

@@ -1294,10 +1294,8 @@ pub fn get_available_providers(cx: &mut App) -> Vec<EditPredictionProvider> {
     }
 
     if let Some(app_state) = workspace::AppState::global(cx).upgrade()
-        && copilot::GlobalCopilotAuth::get_or_init(app_state, cx)
-            .0
-            .read(cx)
-            .is_authenticated()
+        && copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
+            .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
     {
         providers.push(EditPredictionProvider::Copilot);
     };

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

@@ -391,8 +391,8 @@ fn render_github_copilot_provider(window: &mut Window, cx: &mut App) -> Option<i
         copilot_ui::ConfigurationView::new(
             move |cx| {
                 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()
+                    copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
+                        .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
                 } else {
                     false
                 }