use anyhow::Result;use client::{Client, UserStore, zed_urls};use cloud_llm_client::UsageLimit;use codestral::{self, CodestralEditPredictionDelegate};use copilot::Status;use edit_prediction::EditPredictionStore;use edit_prediction_types::EditPredictionDelegateHandle;use editor::{ Editor, MultiBufferOffset, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll,};use feature_flags::FeatureFlagAppExt;use fs::Fs;use gpui::{ Action, Anchor, Animation, AnimationExt, App, AsyncWindowContext, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Subscription, TaskExt, WeakEntity, actions, div, ease_in_out, pulsating_between,};use indoc::indoc;use language::{ EditPredictionsMode, File, Language, language_settings::{ AllLanguageSettings, EditPredictionProvider, LanguageSettings, all_language_settings, },};use project::{DisableAiSettings, Project};use regex::Regex;use settings::{Settings, SettingsStore, update_settings_file};use std::{ rc::Rc, sync::{Arc, LazyLock}, time::Duration,};use ui::{ Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,};use util::ResultExt as _; use workspace::{ HideStatusItem, StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle, notifications::NotificationId,};use zed_actions::{OpenBrowser, OpenSettingsAt}; use crate::{RatePredictions, rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag}; actions!( edit_prediction, [ /// Toggles the edit prediction menu. ToggleMenu ]); const COPILOT_SETTINGS_PATH: &str = "/settings/copilot";const COPILOT_SETTINGS_URL: &str = concat!("https://github.com", "/settings/copilot");const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security"; struct CopilotErrorToast; pub struct EditPredictionButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option<bool>, editor_show_predictions: bool, editor_focus_handle: Option<FocusHandle>, language: Option<Arc<Language>>, file: Option<Arc<dyn File>>, edit_prediction_provider: Option<Arc<dyn EditPredictionDelegateHandle>>, fs: Arc<dyn Fs>, user_store: Entity<UserStore>, popover_menu_handle: PopoverMenuHandle<ContextMenu>, project: WeakEntity<Project>,} impl Render for EditPredictionButton { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { // Return empty div if AI is disabled if DisableAiSettings::get_global(cx).disable_ai { return div().hidden(); } let language_settings = all_language_settings(None, cx); match language_settings.edit_predictions.provider { EditPredictionProvider::Copilot => { let Some(copilot) = EditPredictionStore::try_global(cx) .and_then(|store| store.read(cx).copilot_for_project(&self.project.upgrade()?)) else { return div().hidden(); }; let status = copilot.read(cx).status(); let enabled = self.editor_enabled.unwrap_or(false); let icon = match status { Status::Error(_) => IconName::CopilotError, Status::Authorized => { if enabled { IconName::Copilot } else { IconName::CopilotDisabled } } _ => IconName::CopilotInit, }; if let Status::Error(e) = status { return div().child( IconButton::new("copilot-error", icon) .icon_size(IconSize::Small) .tab_index(0isize) .aria_label("GitHub Copilot") .on_click(cx.listener(move |_, _, window, cx| { if let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { let copilot = copilot.clone(); workspace.show_toast( Toast::new( NotificationId::unique::<CopilotErrorToast>(), format!("Copilot can't be started: {}", e), ) .on_click( "Reinstall Copilot", move |window, cx| { copilot_ui::reinstall_and_sign_in( copilot.clone(), window, cx, ) }, ), cx, ); }); } })) .tooltip(|_window, cx| { Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx) }), ); } let this = cx.weak_entity(); let project = self.project.clone(); let file = self.file.clone(); let language = self.language.clone(); div().child( PopoverMenu::new("copilot") .on_open({ let file = file.clone(); let language = language; let project = project.clone(); Rc::new(move |_window, cx| { emit_edit_prediction_menu_opened( "copilot", &file, &language, &project, cx, ); }) }) .menu(move |window, cx| { let current_status = EditPredictionStore::try_global(cx) .and_then(|store| { store.read(cx).copilot_for_project(&project.upgrade()?) })? .read(cx) .status(); match current_status { Status::Authorized => this.update(cx, |this, cx| { this.build_copilot_context_menu(window, cx) }), _ => this.update(cx, |this, cx| { this.build_copilot_start_menu(window, cx) }), } .ok() }) .anchor(Anchor::BottomRight) .trigger_with_tooltip( IconButton::new("copilot-icon", icon) .tab_index(0isize) .aria_label("GitHub Copilot"), |_window, cx| Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx), ) .with_handle(self.popover_menu_handle.clone()), ) } EditPredictionProvider::Codestral => { let enabled = self.editor_enabled.unwrap_or(true); let has_api_key = codestral::codestral_api_key(cx).is_some(); let this = cx.weak_entity(); let file = self.file.clone(); let language = self.language.clone(); let project = self.project.clone(); let tooltip_meta = if has_api_key { "Powered by Codestral" } else { "Missing API key for Codestral" }; div().child( PopoverMenu::new("codestral") .on_open({ let file = file.clone(); let language = language; let project = project; Rc::new(move |_window, cx| { emit_edit_prediction_menu_opened( "codestral", &file, &language, &project, cx, ); }) }) .menu(move |window, cx| { this.update(cx, |this, cx| { this.build_codestral_context_menu(window, cx) }) .ok() }) .anchor(Anchor::BottomRight) .trigger_with_tooltip( IconButton::new("codestral-icon", IconName::AiMistral) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Prediction") .when(!has_api_key, |this| { this.indicator(Indicator::dot().color(Color::Error)) .indicator_border_color(Some( cx.theme().colors().status_bar_background, )) }) .when(has_api_key && !enabled, |this| { this.indicator(Indicator::dot().color(Color::Ignored)) .indicator_border_color(Some( cx.theme().colors().status_bar_background, )) }), move |_window, cx| { Tooltip::with_meta( "Edit Prediction", Some(&ToggleMenu), tooltip_meta, cx, ) }, ) .with_handle(self.popover_menu_handle.clone()), ) } EditPredictionProvider::OpenAiCompatibleApi => { let enabled = self.editor_enabled.unwrap_or(true); let this = cx.weak_entity(); div().child( PopoverMenu::new("openai-compatible-api") .menu(move |window, cx| { this.update(cx, |this, cx| { this.build_edit_prediction_context_menu( EditPredictionProvider::OpenAiCompatibleApi, window, cx, ) }) .ok() }) .anchor(Anchor::BottomRight) .trigger( IconButton::new("openai-compatible-api-icon", IconName::AiOpenAiCompat) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Prediction") .when(!enabled, |this| { this.indicator(Indicator::dot().color(Color::Ignored)) .indicator_border_color(Some( cx.theme().colors().status_bar_background, )) }), ) .with_handle(self.popover_menu_handle.clone()), ) } EditPredictionProvider::Ollama => { let enabled = self.editor_enabled.unwrap_or(true); let this = cx.weak_entity(); div().child( PopoverMenu::new("ollama") .menu(move |window, cx| { this.update(cx, |this, cx| { this.build_edit_prediction_context_menu( EditPredictionProvider::Ollama, window, cx, ) }) .ok() }) .anchor(Anchor::BottomRight) .trigger_with_tooltip( IconButton::new("ollama-icon", IconName::AiOllama) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Prediction") .when(!enabled, |this| { this.indicator(Indicator::dot().color(Color::Ignored)) .indicator_border_color(Some( cx.theme().colors().status_bar_background, )) }), move |_window, cx| { let settings = all_language_settings(None, cx); let tooltip_meta = match settings.edit_predictions.ollama.as_ref() { Some(settings) if !settings.model.trim().is_empty() => { format!("Powered by Ollama ({})", settings.model) } _ => { "Ollama model not configured — configure a model before use" .to_string() } }; Tooltip::with_meta( "Edit Prediction", Some(&ToggleMenu), tooltip_meta, cx, ) }, ) .with_handle(self.popover_menu_handle.clone()), ) } provider @ (EditPredictionProvider::Zed | EditPredictionProvider::Mercury) => { let enabled = self.editor_enabled.unwrap_or(true); let file = self.file.clone(); let language = self.language.clone(); let project = self.project.clone(); let provider_name: &'static str = match provider { EditPredictionProvider::Zed => "zed", _ => "unknown", }; let icons = self .edit_prediction_provider .as_ref() .map(|p| p.icons(cx)) .unwrap_or_else(|| { edit_prediction_types::EditPredictionIconSet::new(IconName::ZedPredict) }); let ep_icon; let tooltip_meta; let mut missing_token = false; match provider { EditPredictionProvider::Mercury => { ep_icon = if enabled { icons.base } else { icons.disabled }; let mercury_has_error = edit_prediction::EditPredictionStore::try_global(cx).is_some_and( |ep_store| ep_store.read(cx).mercury_has_payment_required_error(), ); missing_token = edit_prediction::EditPredictionStore::try_global(cx) .is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token(cx)); tooltip_meta = if missing_token { "Missing API key for Mercury" } else if mercury_has_error { "Mercury free tier limit reached" } else { "Powered by Mercury" }; } _ => { ep_icon = if enabled { icons.base } else { icons.disabled }; tooltip_meta = "Powered by Zeta" } }; if edit_prediction::should_show_upsell_modal(cx) { let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { "Choose a Plan" } else { "Configure a Provider" }; return div().child( IconButton::new("zed-predict-pending-button", ep_icon) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Predictions") .indicator(Indicator::dot().color(Color::Muted)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)) .tooltip(move |_window, cx| { Tooltip::with_meta("Edit Predictions", None, tooltip_meta, cx) }) .on_click(cx.listener(move |_, _, window, cx| { telemetry::event!( "Pending ToS Clicked", source = "Edit Prediction Status Button" ); window.dispatch_action( zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx, ); })), ); } let mut over_limit = false; if let Some(usage) = self .edit_prediction_provider .as_ref() .and_then(|provider| provider.usage(cx)) { over_limit = usage.over_limit() } let show_editor_predictions = self.editor_show_predictions; let user = self.user_store.read(cx).current_user(); let mercury_has_error = matches!(provider, EditPredictionProvider::Mercury) && edit_prediction::EditPredictionStore::try_global(cx).is_some_and( |ep_store| ep_store.read(cx).mercury_has_payment_required_error(), ); let indicator_color = if missing_token || mercury_has_error { Some(Color::Error) } else if enabled && (!show_editor_predictions || over_limit) { Some(if over_limit { Color::Error } else { Color::Muted }) } else { None }; let zed_cloud_needs_sign_in = matches!(provider, EditPredictionProvider::Zed) && user.is_none(); let provider_unavailable = missing_token || mercury_has_error || zed_cloud_needs_sign_in; let icon_button = IconButton::new("zed-predict-pending-button", ep_icon) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Prediction") .when_some(indicator_color, |this, color| { this.indicator(Indicator::dot().color(color)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)) }) .when(!self.popover_menu_handle.is_deployed(), |element| { element.tooltip(move |_window, cx| { let description = if !enabled { "Disabled For This File" } else if zed_cloud_needs_sign_in { "Sign In Or Configure a Provider" } else if provider_unavailable || show_editor_predictions { tooltip_meta } else { "Enable to Use" }; Tooltip::with_meta( "Edit Prediction", Some(&ToggleMenu), description, cx, ) }) }); let this = cx.weak_entity(); let mut popover_menu = PopoverMenu::new("edit-prediction") .on_open({ let file = file.clone(); let language = language; let project = project; Rc::new(move |_window, cx| { emit_edit_prediction_menu_opened( provider_name, &file, &language, &project, cx, ); }) }) .map(|popover_menu| { let this = this.clone(); popover_menu.menu(move |window, cx| { this.update(cx, |this, cx| { this.build_edit_prediction_context_menu(provider, window, cx) }) .ok() }) }) .anchor(Anchor::BottomRight) .with_handle(self.popover_menu_handle.clone()); Lines 1–500 of 1740. Load more
use anyhow::Result; use client::{Client, UserStore, zed_urls}; use cloud_llm_client::UsageLimit; use codestral::{self, CodestralEditPredictionDelegate}; use copilot::Status; use edit_prediction::EditPredictionStore; use edit_prediction_types::EditPredictionDelegateHandle; use editor::{ Editor, MultiBufferOffset, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll, }; use feature_flags::FeatureFlagAppExt; use fs::Fs; use gpui::{ Action, Anchor, Animation, AnimationExt, App, AsyncWindowContext, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Subscription, TaskExt, WeakEntity, actions, div, ease_in_out, pulsating_between, }; use indoc::indoc; use language::{ EditPredictionsMode, File, Language, language_settings::{ AllLanguageSettings, EditPredictionProvider, LanguageSettings, all_language_settings, }, }; use project::{DisableAiSettings, Project}; use regex::Regex; use settings::{Settings, SettingsStore, update_settings_file}; use std::{ rc::Rc, sync::{Arc, LazyLock}, time::Duration, }; use ui::{ Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ HideStatusItem, StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle, notifications::NotificationId, }; use zed_actions::{OpenBrowser, OpenSettingsAt}; use crate::{RatePredictions, rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag}; actions!( edit_prediction, [ /// Toggles the edit prediction menu. ToggleMenu ] ); const COPILOT_SETTINGS_PATH: &str = "/settings/copilot"; const COPILOT_SETTINGS_URL: &str = concat!("https://github.com", "/settings/copilot"); const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security"; struct CopilotErrorToast; pub struct EditPredictionButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option<bool>, editor_show_predictions: bool, editor_focus_handle: Option<FocusHandle>, language: Option<Arc<Language>>, file: Option<Arc<dyn File>>, edit_prediction_provider: Option<Arc<dyn EditPredictionDelegateHandle>>, fs: Arc<dyn Fs>, user_store: Entity<UserStore>, popover_menu_handle: PopoverMenuHandle<ContextMenu>, project: WeakEntity<Project>, } impl Render for EditPredictionButton { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { // Return empty div if AI is disabled if DisableAiSettings::get_global(cx).disable_ai { return div().hidden(); } let language_settings = all_language_settings(None, cx); match language_settings.edit_predictions.provider { EditPredictionProvider::Copilot => { let Some(copilot) = EditPredictionStore::try_global(cx) .and_then(|store| store.read(cx).copilot_for_project(&self.project.upgrade()?)) else { return div().hidden(); }; let status = copilot.read(cx).status(); let enabled = self.editor_enabled.unwrap_or(false); let icon = match status { Status::Error(_) => IconName::CopilotError, Status::Authorized => { if enabled { IconName::Copilot } else { IconName::CopilotDisabled } } _ => IconName::CopilotInit, }; if let Status::Error(e) = status { return div().child( IconButton::new("copilot-error", icon) .icon_size(IconSize::Small) .tab_index(0isize) .aria_label("GitHub Copilot") .on_click(cx.listener(move |_, _, window, cx| { if let Some(workspace) = Workspace::for_window(window, cx) { workspace.update(cx, |workspace, cx| { let copilot = copilot.clone(); workspace.show_toast( Toast::new( NotificationId::unique::<CopilotErrorToast>(), format!("Copilot can't be started: {}", e), ) .on_click( "Reinstall Copilot", move |window, cx| { copilot_ui::reinstall_and_sign_in( copilot.clone(), window, cx, ) }, ), cx, ); }); } })) .tooltip(|_window, cx| { Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx) }), ); } let this = cx.weak_entity(); let project = self.project.clone(); let file = self.file.clone(); let language = self.language.clone(); div().child( PopoverMenu::new("copilot") .on_open({ let file = file.clone(); let language = language; let project = project.clone(); Rc::new(move |_window, cx| { emit_edit_prediction_menu_opened( "copilot", &file, &language, &project, cx, ); }) }) .menu(move |window, cx| { let current_status = EditPredictionStore::try_global(cx) .and_then(|store| { store.read(cx).copilot_for_project(&project.upgrade()?) })? .read(cx) .status(); match current_status { Status::Authorized => this.update(cx, |this, cx| { this.build_copilot_context_menu(window, cx) }), _ => this.update(cx, |this, cx| { this.build_copilot_start_menu(window, cx) }), } .ok() }) .anchor(Anchor::BottomRight) .trigger_with_tooltip( IconButton::new("copilot-icon", icon) .tab_index(0isize) .aria_label("GitHub Copilot"), |_window, cx| Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx), ) .with_handle(self.popover_menu_handle.clone()), ) } EditPredictionProvider::Codestral => { let enabled = self.editor_enabled.unwrap_or(true); let has_api_key = codestral::codestral_api_key(cx).is_some(); let this = cx.weak_entity(); let file = self.file.clone(); let language = self.language.clone(); let project = self.project.clone(); let tooltip_meta = if has_api_key { "Powered by Codestral" } else { "Missing API key for Codestral" }; div().child( PopoverMenu::new("codestral") .on_open({ let file = file.clone(); let language = language; let project = project; Rc::new(move |_window, cx| { emit_edit_prediction_menu_opened( "codestral", &file, &language, &project, cx, ); }) }) .menu(move |window, cx| { this.update(cx, |this, cx| { this.build_codestral_context_menu(window, cx) }) .ok() }) .anchor(Anchor::BottomRight) .trigger_with_tooltip( IconButton::new("codestral-icon", IconName::AiMistral) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Prediction") .when(!has_api_key, |this| { this.indicator(Indicator::dot().color(Color::Error)) .indicator_border_color(Some( cx.theme().colors().status_bar_background, )) }) .when(has_api_key && !enabled, |this| { this.indicator(Indicator::dot().color(Color::Ignored)) .indicator_border_color(Some( cx.theme().colors().status_bar_background, )) }), move |_window, cx| { Tooltip::with_meta( "Edit Prediction", Some(&ToggleMenu), tooltip_meta, cx, ) }, ) .with_handle(self.popover_menu_handle.clone()), ) } EditPredictionProvider::OpenAiCompatibleApi => { let enabled = self.editor_enabled.unwrap_or(true); let this = cx.weak_entity(); div().child( PopoverMenu::new("openai-compatible-api") .menu(move |window, cx| { this.update(cx, |this, cx| { this.build_edit_prediction_context_menu( EditPredictionProvider::OpenAiCompatibleApi, window, cx, ) }) .ok() }) .anchor(Anchor::BottomRight) .trigger( IconButton::new("openai-compatible-api-icon", IconName::AiOpenAiCompat) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Prediction") .when(!enabled, |this| { this.indicator(Indicator::dot().color(Color::Ignored)) .indicator_border_color(Some( cx.theme().colors().status_bar_background, )) }), ) .with_handle(self.popover_menu_handle.clone()), ) } EditPredictionProvider::Ollama => { let enabled = self.editor_enabled.unwrap_or(true); let this = cx.weak_entity(); div().child( PopoverMenu::new("ollama") .menu(move |window, cx| { this.update(cx, |this, cx| { this.build_edit_prediction_context_menu( EditPredictionProvider::Ollama, window, cx, ) }) .ok() }) .anchor(Anchor::BottomRight) .trigger_with_tooltip( IconButton::new("ollama-icon", IconName::AiOllama) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Prediction") .when(!enabled, |this| { this.indicator(Indicator::dot().color(Color::Ignored)) .indicator_border_color(Some( cx.theme().colors().status_bar_background, )) }), move |_window, cx| { let settings = all_language_settings(None, cx); let tooltip_meta = match settings.edit_predictions.ollama.as_ref() { Some(settings) if !settings.model.trim().is_empty() => { format!("Powered by Ollama ({})", settings.model) } _ => { "Ollama model not configured — configure a model before use" .to_string() } }; Tooltip::with_meta( "Edit Prediction", Some(&ToggleMenu), tooltip_meta, cx, ) }, ) .with_handle(self.popover_menu_handle.clone()), ) } provider @ (EditPredictionProvider::Zed | EditPredictionProvider::Mercury) => { let enabled = self.editor_enabled.unwrap_or(true); let file = self.file.clone(); let language = self.language.clone(); let project = self.project.clone(); let provider_name: &'static str = match provider { EditPredictionProvider::Zed => "zed", _ => "unknown", }; let icons = self .edit_prediction_provider .as_ref() .map(|p| p.icons(cx)) .unwrap_or_else(|| { edit_prediction_types::EditPredictionIconSet::new(IconName::ZedPredict) }); let ep_icon; let tooltip_meta; let mut missing_token = false; match provider { EditPredictionProvider::Mercury => { ep_icon = if enabled { icons.base } else { icons.disabled }; let mercury_has_error = edit_prediction::EditPredictionStore::try_global(cx).is_some_and( |ep_store| ep_store.read(cx).mercury_has_payment_required_error(), ); missing_token = edit_prediction::EditPredictionStore::try_global(cx) .is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token(cx)); tooltip_meta = if missing_token { "Missing API key for Mercury" } else if mercury_has_error { "Mercury free tier limit reached" } else { "Powered by Mercury" }; } _ => { ep_icon = if enabled { icons.base } else { icons.disabled }; tooltip_meta = "Powered by Zeta" } }; if edit_prediction::should_show_upsell_modal(cx) { let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { "Choose a Plan" } else { "Configure a Provider" }; return div().child( IconButton::new("zed-predict-pending-button", ep_icon) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Predictions") .indicator(Indicator::dot().color(Color::Muted)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)) .tooltip(move |_window, cx| { Tooltip::with_meta("Edit Predictions", None, tooltip_meta, cx) }) .on_click(cx.listener(move |_, _, window, cx| { telemetry::event!( "Pending ToS Clicked", source = "Edit Prediction Status Button" ); window.dispatch_action( zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx, ); })), ); } let mut over_limit = false; if let Some(usage) = self .edit_prediction_provider .as_ref() .and_then(|provider| provider.usage(cx)) { over_limit = usage.over_limit() } let show_editor_predictions = self.editor_show_predictions; let user = self.user_store.read(cx).current_user(); let mercury_has_error = matches!(provider, EditPredictionProvider::Mercury) && edit_prediction::EditPredictionStore::try_global(cx).is_some_and( |ep_store| ep_store.read(cx).mercury_has_payment_required_error(), ); let indicator_color = if missing_token || mercury_has_error { Some(Color::Error) } else if enabled && (!show_editor_predictions || over_limit) { Some(if over_limit { Color::Error } else { Color::Muted }) } else { None }; let zed_cloud_needs_sign_in = matches!(provider, EditPredictionProvider::Zed) && user.is_none(); let provider_unavailable = missing_token || mercury_has_error || zed_cloud_needs_sign_in; let icon_button = IconButton::new("zed-predict-pending-button", ep_icon) .shape(IconButtonShape::Square) .tab_index(0isize) .aria_label("Edit Prediction") .when_some(indicator_color, |this, color| { this.indicator(Indicator::dot().color(color)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)) }) .when(!self.popover_menu_handle.is_deployed(), |element| { element.tooltip(move |_window, cx| { let description = if !enabled { "Disabled For This File" } else if zed_cloud_needs_sign_in { "Sign In Or Configure a Provider" } else if provider_unavailable || show_editor_predictions { tooltip_meta } else { "Enable to Use" }; Tooltip::with_meta( "Edit Prediction", Some(&ToggleMenu), description, cx, ) }) }); let this = cx.weak_entity(); let mut popover_menu = PopoverMenu::new("edit-prediction") .on_open({ let file = file.clone(); let language = language; let project = project; Rc::new(move |_window, cx| { emit_edit_prediction_menu_opened( provider_name, &file, &language, &project, cx, ); }) }) .map(|popover_menu| { let this = this.clone(); popover_menu.menu(move |window, cx| { this.update(cx, |this, cx| { this.build_edit_prediction_context_menu(provider, window, cx) }) .ok() }) }) .anchor(Anchor::BottomRight) .with_handle(self.popover_menu_handle.clone());