1use anyhow::Result;
   2use client::{UserStore, zed_urls};
   3use cloud_llm_client::UsageLimit;
   4use codestral::CodestralCompletionProvider;
   5use copilot::{Copilot, Status};
   6use editor::{Editor, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll};
   7use feature_flags::{FeatureFlagAppExt, PredictEditsRateCompletionsFeatureFlag};
   8use fs::Fs;
   9use gpui::{
  10    Action, Animation, AnimationExt, App, AsyncWindowContext, Corner, Entity, FocusHandle,
  11    Focusable, IntoElement, ParentElement, Render, Subscription, WeakEntity, actions, div,
  12    pulsating_between,
  13};
  14use indoc::indoc;
  15use language::{
  16    EditPredictionsMode, File, Language, Rope,
  17    language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
  18};
  19use project::DisableAiSettings;
  20use regex::Regex;
  21use settings::{Settings, SettingsStore, update_settings_file};
  22use std::{
  23    sync::{Arc, LazyLock},
  24    time::Duration,
  25};
  26use supermaven::{AccountStatus, Supermaven};
  27use ui::{
  28    Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
  29    IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
  30};
  31use workspace::{
  32    StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
  33    notifications::NotificationId,
  34};
  35use zed_actions::OpenBrowser;
  36use zeta::RateCompletions;
  37
  38actions!(
  39    edit_prediction,
  40    [
  41        /// Toggles the edit prediction menu.
  42        ToggleMenu
  43    ]
  44);
  45
  46const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
  47const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security";
  48
  49struct CopilotErrorToast;
  50
  51pub struct EditPredictionButton {
  52    editor_subscription: Option<(Subscription, usize)>,
  53    editor_enabled: Option<bool>,
  54    editor_show_predictions: bool,
  55    editor_focus_handle: Option<FocusHandle>,
  56    language: Option<Arc<Language>>,
  57    file: Option<Arc<dyn File>>,
  58    edit_prediction_provider: Option<Arc<dyn edit_prediction::EditPredictionProviderHandle>>,
  59    fs: Arc<dyn Fs>,
  60    user_store: Entity<UserStore>,
  61    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
  62}
  63
  64enum SupermavenButtonStatus {
  65    Ready,
  66    Errored(String),
  67    NeedsActivation(String),
  68    Initializing,
  69}
  70
  71impl Render for EditPredictionButton {
  72    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
  73        // Return empty div if AI is disabled
  74        if DisableAiSettings::get_global(cx).disable_ai {
  75            return div().hidden();
  76        }
  77
  78        let all_language_settings = all_language_settings(None, cx);
  79
  80        match all_language_settings.edit_predictions.provider {
  81            EditPredictionProvider::None => div().hidden(),
  82
  83            EditPredictionProvider::Copilot => {
  84                let Some(copilot) = Copilot::global(cx) else {
  85                    return div().hidden();
  86                };
  87                let status = copilot.read(cx).status();
  88
  89                let enabled = self.editor_enabled.unwrap_or(false);
  90
  91                let icon = match status {
  92                    Status::Error(_) => IconName::CopilotError,
  93                    Status::Authorized => {
  94                        if enabled {
  95                            IconName::Copilot
  96                        } else {
  97                            IconName::CopilotDisabled
  98                        }
  99                    }
 100                    _ => IconName::CopilotInit,
 101                };
 102
 103                if let Status::Error(e) = status {
 104                    return div().child(
 105                        IconButton::new("copilot-error", icon)
 106                            .icon_size(IconSize::Small)
 107                            .on_click(cx.listener(move |_, _, window, cx| {
 108                                if let Some(workspace) = window.root::<Workspace>().flatten() {
 109                                    workspace.update(cx, |workspace, cx| {
 110                                        workspace.show_toast(
 111                                            Toast::new(
 112                                                NotificationId::unique::<CopilotErrorToast>(),
 113                                                format!("Copilot can't be started: {}", e),
 114                                            )
 115                                            .on_click(
 116                                                "Reinstall Copilot",
 117                                                |window, cx| {
 118                                                    copilot::reinstall_and_sign_in(window, cx)
 119                                                },
 120                                            ),
 121                                            cx,
 122                                        );
 123                                    });
 124                                }
 125                            }))
 126                            .tooltip(|_window, cx| {
 127                                Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx)
 128                            }),
 129                    );
 130                }
 131                let this = cx.entity();
 132
 133                div().child(
 134                    PopoverMenu::new("copilot")
 135                        .menu(move |window, cx| {
 136                            let current_status = Copilot::global(cx)?.read(cx).status();
 137                            Some(match current_status {
 138                                Status::Authorized => this.update(cx, |this, cx| {
 139                                    this.build_copilot_context_menu(window, cx)
 140                                }),
 141                                _ => this.update(cx, |this, cx| {
 142                                    this.build_copilot_start_menu(window, cx)
 143                                }),
 144                            })
 145                        })
 146                        .anchor(Corner::BottomRight)
 147                        .trigger_with_tooltip(
 148                            IconButton::new("copilot-icon", icon),
 149                            |_window, cx| Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx),
 150                        )
 151                        .with_handle(self.popover_menu_handle.clone()),
 152                )
 153            }
 154
 155            EditPredictionProvider::Supermaven => {
 156                let Some(supermaven) = Supermaven::global(cx) else {
 157                    return div();
 158                };
 159
 160                let supermaven = supermaven.read(cx);
 161
 162                let status = match supermaven {
 163                    Supermaven::Starting => SupermavenButtonStatus::Initializing,
 164                    Supermaven::FailedDownload { error } => {
 165                        SupermavenButtonStatus::Errored(error.to_string())
 166                    }
 167                    Supermaven::Spawned(agent) => {
 168                        let account_status = agent.account_status.clone();
 169                        match account_status {
 170                            AccountStatus::NeedsActivation { activate_url } => {
 171                                SupermavenButtonStatus::NeedsActivation(activate_url)
 172                            }
 173                            AccountStatus::Unknown => SupermavenButtonStatus::Initializing,
 174                            AccountStatus::Ready => SupermavenButtonStatus::Ready,
 175                        }
 176                    }
 177                    Supermaven::Error { error } => {
 178                        SupermavenButtonStatus::Errored(error.to_string())
 179                    }
 180                };
 181
 182                let icon = status.to_icon();
 183                let tooltip_text = status.to_tooltip();
 184                let has_menu = status.has_menu();
 185                let this = cx.entity();
 186                let fs = self.fs.clone();
 187
 188                div().child(
 189                    PopoverMenu::new("supermaven")
 190                        .menu(move |window, cx| match &status {
 191                            SupermavenButtonStatus::NeedsActivation(activate_url) => {
 192                                Some(ContextMenu::build(window, cx, |menu, _, _| {
 193                                    let fs = fs.clone();
 194                                    let activate_url = activate_url.clone();
 195                                    menu.entry("Sign In", None, move |_, cx| {
 196                                        cx.open_url(activate_url.as_str())
 197                                    })
 198                                    .entry(
 199                                        "Use Zed AI",
 200                                        None,
 201                                        move |_, cx| {
 202                                            set_completion_provider(
 203                                                fs.clone(),
 204                                                cx,
 205                                                EditPredictionProvider::Zed,
 206                                            )
 207                                        },
 208                                    )
 209                                }))
 210                            }
 211                            SupermavenButtonStatus::Ready => Some(this.update(cx, |this, cx| {
 212                                this.build_supermaven_context_menu(window, cx)
 213                            })),
 214                            _ => None,
 215                        })
 216                        .anchor(Corner::BottomRight)
 217                        .trigger_with_tooltip(
 218                            IconButton::new("supermaven-icon", icon),
 219                            move |window, cx| {
 220                                if has_menu {
 221                                    Tooltip::for_action(tooltip_text.clone(), &ToggleMenu, cx)
 222                                } else {
 223                                    Tooltip::text(tooltip_text.clone())(window, cx)
 224                                }
 225                            },
 226                        )
 227                        .with_handle(self.popover_menu_handle.clone()),
 228                )
 229            }
 230
 231            EditPredictionProvider::Codestral => {
 232                let enabled = self.editor_enabled.unwrap_or(true);
 233                let has_api_key = CodestralCompletionProvider::has_api_key(cx);
 234                let fs = self.fs.clone();
 235                let this = cx.entity();
 236
 237                div().child(
 238                    PopoverMenu::new("codestral")
 239                        .menu(move |window, cx| {
 240                            if has_api_key {
 241                                Some(this.update(cx, |this, cx| {
 242                                    this.build_codestral_context_menu(window, cx)
 243                                }))
 244                            } else {
 245                                Some(ContextMenu::build(window, cx, |menu, _, _| {
 246                                    let fs = fs.clone();
 247                                    menu.entry("Use Zed AI instead", None, move |_, cx| {
 248                                        set_completion_provider(
 249                                            fs.clone(),
 250                                            cx,
 251                                            EditPredictionProvider::Zed,
 252                                        )
 253                                    })
 254                                    .separator()
 255                                    .entry(
 256                                        "Configure Codestral API Key",
 257                                        None,
 258                                        move |window, cx| {
 259                                            window.dispatch_action(
 260                                                zed_actions::agent::OpenSettings.boxed_clone(),
 261                                                cx,
 262                                            );
 263                                        },
 264                                    )
 265                                }))
 266                            }
 267                        })
 268                        .anchor(Corner::BottomRight)
 269                        .trigger_with_tooltip(
 270                            IconButton::new("codestral-icon", IconName::AiMistral)
 271                                .shape(IconButtonShape::Square)
 272                                .when(!has_api_key, |this| {
 273                                    this.indicator(Indicator::dot().color(Color::Error))
 274                                        .indicator_border_color(Some(
 275                                            cx.theme().colors().status_bar_background,
 276                                        ))
 277                                })
 278                                .when(has_api_key && !enabled, |this| {
 279                                    this.indicator(Indicator::dot().color(Color::Ignored))
 280                                        .indicator_border_color(Some(
 281                                            cx.theme().colors().status_bar_background,
 282                                        ))
 283                                }),
 284                            move |_window, cx| Tooltip::for_action("Codestral", &ToggleMenu, cx),
 285                        )
 286                        .with_handle(self.popover_menu_handle.clone()),
 287                )
 288            }
 289
 290            EditPredictionProvider::Zed => {
 291                let enabled = self.editor_enabled.unwrap_or(true);
 292
 293                let zeta_icon = if enabled {
 294                    IconName::ZedPredict
 295                } else {
 296                    IconName::ZedPredictDisabled
 297                };
 298
 299                if zeta::should_show_upsell_modal() {
 300                    let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
 301                        "Choose a Plan"
 302                    } else {
 303                        "Sign In"
 304                    };
 305
 306                    return div().child(
 307                        IconButton::new("zed-predict-pending-button", zeta_icon)
 308                            .shape(IconButtonShape::Square)
 309                            .indicator(Indicator::dot().color(Color::Muted))
 310                            .indicator_border_color(Some(cx.theme().colors().status_bar_background))
 311                            .tooltip(move |_window, cx| {
 312                                Tooltip::with_meta("Edit Predictions", None, tooltip_meta, cx)
 313                            })
 314                            .on_click(cx.listener(move |_, _, window, cx| {
 315                                telemetry::event!(
 316                                    "Pending ToS Clicked",
 317                                    source = "Edit Prediction Status Button"
 318                                );
 319                                window.dispatch_action(
 320                                    zed_actions::OpenZedPredictOnboarding.boxed_clone(),
 321                                    cx,
 322                                );
 323                            })),
 324                    );
 325                }
 326
 327                let mut over_limit = false;
 328
 329                if let Some(usage) = self
 330                    .edit_prediction_provider
 331                    .as_ref()
 332                    .and_then(|provider| provider.usage(cx))
 333                {
 334                    over_limit = usage.over_limit()
 335                }
 336
 337                let show_editor_predictions = self.editor_show_predictions;
 338
 339                let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
 340                    .shape(IconButtonShape::Square)
 341                    .when(
 342                        enabled && (!show_editor_predictions || over_limit),
 343                        |this| {
 344                            this.indicator(Indicator::dot().when_else(
 345                                over_limit,
 346                                |dot| dot.color(Color::Error),
 347                                |dot| dot.color(Color::Muted),
 348                            ))
 349                            .indicator_border_color(Some(cx.theme().colors().status_bar_background))
 350                        },
 351                    )
 352                    .when(!self.popover_menu_handle.is_deployed(), |element| {
 353                        element.tooltip(move |_window, cx| {
 354                            if enabled {
 355                                if show_editor_predictions {
 356                                    Tooltip::for_action("Edit Prediction", &ToggleMenu, cx)
 357                                } else {
 358                                    Tooltip::with_meta(
 359                                        "Edit Prediction",
 360                                        Some(&ToggleMenu),
 361                                        "Hidden For This File",
 362                                        cx,
 363                                    )
 364                                }
 365                            } else {
 366                                Tooltip::with_meta(
 367                                    "Edit Prediction",
 368                                    Some(&ToggleMenu),
 369                                    "Disabled For This File",
 370                                    cx,
 371                                )
 372                            }
 373                        })
 374                    });
 375
 376                let this = cx.entity();
 377
 378                let mut popover_menu = PopoverMenu::new("zeta")
 379                    .menu(move |window, cx| {
 380                        Some(this.update(cx, |this, cx| this.build_zeta_context_menu(window, cx)))
 381                    })
 382                    .anchor(Corner::BottomRight)
 383                    .with_handle(self.popover_menu_handle.clone());
 384
 385                let is_refreshing = self
 386                    .edit_prediction_provider
 387                    .as_ref()
 388                    .is_some_and(|provider| provider.is_refreshing(cx));
 389
 390                if is_refreshing {
 391                    popover_menu = popover_menu.trigger(
 392                        icon_button.with_animation(
 393                            "pulsating-label",
 394                            Animation::new(Duration::from_secs(2))
 395                                .repeat()
 396                                .with_easing(pulsating_between(0.2, 1.0)),
 397                            |icon_button, delta| icon_button.alpha(delta),
 398                        ),
 399                    );
 400                } else {
 401                    popover_menu = popover_menu.trigger(icon_button);
 402                }
 403
 404                div().child(popover_menu.into_any_element())
 405            }
 406        }
 407    }
 408}
 409
 410impl EditPredictionButton {
 411    pub fn new(
 412        fs: Arc<dyn Fs>,
 413        user_store: Entity<UserStore>,
 414        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
 415        cx: &mut Context<Self>,
 416    ) -> Self {
 417        if let Some(copilot) = Copilot::global(cx) {
 418            cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
 419        }
 420
 421        cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
 422            .detach();
 423
 424        Self {
 425            editor_subscription: None,
 426            editor_enabled: None,
 427            editor_show_predictions: true,
 428            editor_focus_handle: None,
 429            language: None,
 430            file: None,
 431            edit_prediction_provider: None,
 432            user_store,
 433            popover_menu_handle,
 434            fs,
 435        }
 436    }
 437
 438    pub fn build_copilot_start_menu(
 439        &mut self,
 440        window: &mut Window,
 441        cx: &mut Context<Self>,
 442    ) -> Entity<ContextMenu> {
 443        let fs = self.fs.clone();
 444        ContextMenu::build(window, cx, |menu, _, _| {
 445            menu.entry("Sign In to Copilot", None, copilot::initiate_sign_in)
 446                .entry("Disable Copilot", None, {
 447                    let fs = fs.clone();
 448                    move |_window, cx| hide_copilot(fs.clone(), cx)
 449                })
 450                .separator()
 451                .entry("Use Zed AI", None, {
 452                    let fs = fs.clone();
 453                    move |_window, cx| {
 454                        set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
 455                    }
 456                })
 457        })
 458    }
 459
 460    pub fn build_language_settings_menu(
 461        &self,
 462        mut menu: ContextMenu,
 463        window: &Window,
 464        cx: &mut App,
 465    ) -> ContextMenu {
 466        let fs = self.fs.clone();
 467        let line_height = window.line_height();
 468
 469        menu = menu.header("Show Edit Predictions For");
 470
 471        let language_state = self.language.as_ref().map(|language| {
 472            (
 473                language.clone(),
 474                language_settings::language_settings(Some(language.name()), None, cx)
 475                    .show_edit_predictions,
 476            )
 477        });
 478
 479        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
 480            let entry = ContextMenuEntry::new("This Buffer")
 481                .toggleable(IconPosition::Start, self.editor_show_predictions)
 482                .action(Box::new(editor::actions::ToggleEditPrediction))
 483                .handler(move |window, cx| {
 484                    editor_focus_handle.dispatch_action(
 485                        &editor::actions::ToggleEditPrediction,
 486                        window,
 487                        cx,
 488                    );
 489                });
 490
 491            match language_state.clone() {
 492                Some((language, false)) => {
 493                    menu = menu.item(
 494                        entry
 495                            .disabled(true)
 496                            .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_cx| {
 497                                Label::new(format!("Edit predictions cannot be toggled for this buffer because they are disabled for {}", language.name()))
 498                                    .into_any_element()
 499                            })
 500                    );
 501                }
 502                Some(_) | None => menu = menu.item(entry),
 503            }
 504        }
 505
 506        if let Some((language, language_enabled)) = language_state {
 507            let fs = fs.clone();
 508
 509            menu = menu.toggleable_entry(
 510                language.name(),
 511                language_enabled,
 512                IconPosition::Start,
 513                None,
 514                move |_, cx| {
 515                    toggle_show_edit_predictions_for_language(language.clone(), fs.clone(), cx)
 516                },
 517            );
 518        }
 519
 520        let settings = AllLanguageSettings::get_global(cx);
 521
 522        let globally_enabled = settings.show_edit_predictions(None, cx);
 523        let entry = ContextMenuEntry::new("All Files")
 524            .toggleable(IconPosition::Start, globally_enabled)
 525            .action(workspace::ToggleEditPrediction.boxed_clone())
 526            .handler(|window, cx| {
 527                window.dispatch_action(workspace::ToggleEditPrediction.boxed_clone(), cx)
 528            });
 529        menu = menu.item(entry);
 530
 531        let provider = settings.edit_predictions.provider;
 532        let current_mode = settings.edit_predictions_mode();
 533        let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
 534        let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
 535
 536        if matches!(
 537            provider,
 538            EditPredictionProvider::Zed
 539                | EditPredictionProvider::Copilot
 540                | EditPredictionProvider::Supermaven
 541                | EditPredictionProvider::Codestral
 542        ) {
 543            menu = menu
 544                .separator()
 545                .header("Display Modes")
 546                .item(
 547                    ContextMenuEntry::new("Eager")
 548                        .toggleable(IconPosition::Start, eager_mode)
 549                        .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_| {
 550                            Label::new("Display predictions inline when there are no language server completions available.").into_any_element()
 551                        })
 552                        .handler({
 553                            let fs = fs.clone();
 554                            move |_, cx| {
 555                                toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Eager, cx)
 556                            }
 557                        }),
 558                )
 559                .item(
 560                    ContextMenuEntry::new("Subtle")
 561                        .toggleable(IconPosition::Start, subtle_mode)
 562                        .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_| {
 563                            Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element()
 564                        })
 565                        .handler({
 566                            let fs = fs.clone();
 567                            move |_, cx| {
 568                                toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Subtle, cx)
 569                            }
 570                        }),
 571                );
 572        }
 573
 574        menu = menu.separator().header("Privacy");
 575        if let Some(provider) = &self.edit_prediction_provider {
 576            let data_collection = provider.data_collection_state(cx);
 577            if data_collection.is_supported() {
 578                let provider = provider.clone();
 579                let enabled = data_collection.is_enabled();
 580                let is_open_source = data_collection.is_project_open_source();
 581                let is_collecting = data_collection.is_enabled();
 582                let (icon_name, icon_color) = if is_open_source && is_collecting {
 583                    (IconName::Check, Color::Success)
 584                } else {
 585                    (IconName::Check, Color::Accent)
 586                };
 587
 588                menu = menu.item(
 589                    ContextMenuEntry::new("Training Data Collection")
 590                        .toggleable(IconPosition::Start, data_collection.is_enabled())
 591                        .icon(icon_name)
 592                        .icon_color(icon_color)
 593                        .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| {
 594                            let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
 595                                (true, true) => (
 596                                    "Project identified as open source, and you're sharing data.",
 597                                    Color::Default,
 598                                    IconName::Check,
 599                                    Color::Success,
 600                                ),
 601                                (true, false) => (
 602                                    "Project identified as open source, but you're not sharing data.",
 603                                    Color::Muted,
 604                                    IconName::Close,
 605                                    Color::Muted,
 606                                ),
 607                                (false, true) => (
 608                                    "Project not identified as open source. No data captured.",
 609                                    Color::Muted,
 610                                    IconName::Close,
 611                                    Color::Muted,
 612                                ),
 613                                (false, false) => (
 614                                    "Project not identified as open source, and setting turned off.",
 615                                    Color::Muted,
 616                                    IconName::Close,
 617                                    Color::Muted,
 618                                ),
 619                            };
 620                            v_flex()
 621                                .gap_2()
 622                                .child(
 623                                    Label::new(indoc!{
 624                                        "Help us improve our open dataset model by sharing data from open source repositories. \
 625                                        Zed must detect a license file in your repo for this setting to take effect. \
 626                                        Files with sensitive data and secrets are excluded by default."
 627                                    })
 628                                )
 629                                .child(
 630                                    h_flex()
 631                                        .items_start()
 632                                        .pt_2()
 633                                        .pr_1()
 634                                        .flex_1()
 635                                        .gap_1p5()
 636                                        .border_t_1()
 637                                        .border_color(cx.theme().colors().border_variant)
 638                                        .child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)))
 639                                        .child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx)))
 640                                )
 641                                .into_any_element()
 642                        })
 643                        .handler(move |_, cx| {
 644                            provider.toggle_data_collection(cx);
 645
 646                            if !enabled {
 647                                telemetry::event!(
 648                                    "Data Collection Enabled",
 649                                    source = "Edit Prediction Status Menu"
 650                                );
 651                            } else {
 652                                telemetry::event!(
 653                                    "Data Collection Disabled",
 654                                    source = "Edit Prediction Status Menu"
 655                                );
 656                            }
 657                        })
 658                );
 659
 660                if is_collecting && !is_open_source {
 661                    menu = menu.item(
 662                        ContextMenuEntry::new("No data captured.")
 663                            .disabled(true)
 664                            .icon(IconName::Close)
 665                            .icon_color(Color::Error)
 666                            .icon_size(IconSize::Small),
 667                    );
 668                }
 669            }
 670        }
 671
 672        menu = menu.item(
 673            ContextMenuEntry::new("Configure Excluded Files")
 674                .icon(IconName::LockOutlined)
 675                .icon_color(Color::Muted)
 676                .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| {
 677                    Label::new(indoc!{"
 678                        Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
 679                })
 680                .handler(move |window, cx| {
 681                    if let Some(workspace) = window.root().flatten() {
 682                        let workspace = workspace.downgrade();
 683                        window
 684                            .spawn(cx, async |cx| {
 685                                open_disabled_globs_setting_in_editor(
 686                                    workspace,
 687                                    cx,
 688                                ).await
 689                            })
 690                            .detach_and_log_err(cx);
 691                    }
 692                }),
 693        ).item(
 694            ContextMenuEntry::new("View Documentation")
 695                .icon(IconName::FileGeneric)
 696                .icon_color(Color::Muted)
 697                .handler(move |_, cx| {
 698                    cx.open_url(PRIVACY_DOCS);
 699                })
 700        );
 701
 702        if !self.editor_enabled.unwrap_or(true) {
 703            menu = menu.item(
 704                ContextMenuEntry::new("This file is excluded.")
 705                    .disabled(true)
 706                    .icon(IconName::ZedPredictDisabled)
 707                    .icon_size(IconSize::Small),
 708            );
 709        }
 710
 711        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
 712            menu = menu
 713                .separator()
 714                .entry(
 715                    "Predict Edit at Cursor",
 716                    Some(Box::new(ShowEditPrediction)),
 717                    {
 718                        let editor_focus_handle = editor_focus_handle.clone();
 719                        move |window, cx| {
 720                            editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
 721                        }
 722                    },
 723                )
 724                .context(editor_focus_handle);
 725        }
 726
 727        menu
 728    }
 729
 730    fn build_copilot_context_menu(
 731        &self,
 732        window: &mut Window,
 733        cx: &mut Context<Self>,
 734    ) -> Entity<ContextMenu> {
 735        ContextMenu::build(window, cx, |menu, window, cx| {
 736            self.build_language_settings_menu(menu, window, cx)
 737                .separator()
 738                .entry("Use Zed AI instead", None, {
 739                    let fs = self.fs.clone();
 740                    move |_window, cx| {
 741                        set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
 742                    }
 743                })
 744                .separator()
 745                .link(
 746                    "Go to Copilot Settings",
 747                    OpenBrowser {
 748                        url: COPILOT_SETTINGS_URL.to_string(),
 749                    }
 750                    .boxed_clone(),
 751                )
 752                .action("Sign Out", copilot::SignOut.boxed_clone())
 753        })
 754    }
 755
 756    fn build_supermaven_context_menu(
 757        &self,
 758        window: &mut Window,
 759        cx: &mut Context<Self>,
 760    ) -> Entity<ContextMenu> {
 761        ContextMenu::build(window, cx, |menu, window, cx| {
 762            self.build_language_settings_menu(menu, window, cx)
 763                .separator()
 764                .action("Sign Out", supermaven::SignOut.boxed_clone())
 765        })
 766    }
 767
 768    fn build_codestral_context_menu(
 769        &self,
 770        window: &mut Window,
 771        cx: &mut Context<Self>,
 772    ) -> Entity<ContextMenu> {
 773        let fs = self.fs.clone();
 774        ContextMenu::build(window, cx, |menu, window, cx| {
 775            self.build_language_settings_menu(menu, window, cx)
 776                .separator()
 777                .entry("Use Zed AI instead", None, move |_, cx| {
 778                    set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
 779                })
 780                .separator()
 781                .entry("Configure Codestral API Key", None, move |window, cx| {
 782                    window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
 783                })
 784        })
 785    }
 786
 787    fn build_zeta_context_menu(
 788        &self,
 789        window: &mut Window,
 790        cx: &mut Context<Self>,
 791    ) -> Entity<ContextMenu> {
 792        ContextMenu::build(window, cx, |mut menu, window, cx| {
 793            if let Some(usage) = self
 794                .edit_prediction_provider
 795                .as_ref()
 796                .and_then(|provider| provider.usage(cx))
 797            {
 798                menu = menu.header("Usage");
 799                menu = menu
 800                    .custom_entry(
 801                        move |_window, cx| {
 802                            let used_percentage = match usage.limit {
 803                                UsageLimit::Limited(limit) => {
 804                                    Some((usage.amount as f32 / limit as f32) * 100.)
 805                                }
 806                                UsageLimit::Unlimited => None,
 807                            };
 808
 809                            h_flex()
 810                                .flex_1()
 811                                .gap_1p5()
 812                                .children(
 813                                    used_percentage.map(|percent| {
 814                                        ProgressBar::new("usage", percent, 100., cx)
 815                                    }),
 816                                )
 817                                .child(
 818                                    Label::new(match usage.limit {
 819                                        UsageLimit::Limited(limit) => {
 820                                            format!("{} / {limit}", usage.amount)
 821                                        }
 822                                        UsageLimit::Unlimited => format!("{} / ∞", usage.amount),
 823                                    })
 824                                    .size(LabelSize::Small)
 825                                    .color(Color::Muted),
 826                                )
 827                                .into_any_element()
 828                        },
 829                        move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
 830                    )
 831                    .when(usage.over_limit(), |menu| -> ContextMenu {
 832                        menu.entry("Subscribe to increase your limit", None, |_window, cx| {
 833                            cx.open_url(&zed_urls::account_url(cx))
 834                        })
 835                    })
 836                    .separator();
 837            } else if self.user_store.read(cx).account_too_young() {
 838                menu = menu
 839                    .custom_entry(
 840                        |_window, _cx| {
 841                            Label::new("Your GitHub account is less than 30 days old.")
 842                                .size(LabelSize::Small)
 843                                .color(Color::Warning)
 844                                .into_any_element()
 845                        },
 846                        |_window, cx| cx.open_url(&zed_urls::account_url(cx)),
 847                    )
 848                    .entry("Upgrade to Zed Pro or contact us.", None, |_window, cx| {
 849                        cx.open_url(&zed_urls::account_url(cx))
 850                    })
 851                    .separator();
 852            } else if self.user_store.read(cx).has_overdue_invoices() {
 853                menu = menu
 854                    .custom_entry(
 855                        |_window, _cx| {
 856                            Label::new("You have an outstanding invoice")
 857                                .size(LabelSize::Small)
 858                                .color(Color::Warning)
 859                                .into_any_element()
 860                        },
 861                        |_window, cx| {
 862                            cx.open_url(&zed_urls::account_url(cx))
 863                        },
 864                    )
 865                    .entry(
 866                        "Check your payment status or contact us at billing-support@zed.dev to continue using this feature.",
 867                        None,
 868                        |_window, cx| {
 869                            cx.open_url(&zed_urls::account_url(cx))
 870                        },
 871                    )
 872                    .separator();
 873            }
 874
 875            self.build_language_settings_menu(menu, window, cx).when(
 876                cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
 877                |this| this.action("Rate Completions", RateCompletions.boxed_clone()),
 878            )
 879        })
 880    }
 881
 882    pub fn update_enabled(&mut self, editor: Entity<Editor>, cx: &mut Context<Self>) {
 883        let editor = editor.read(cx);
 884        let snapshot = editor.buffer().read(cx).snapshot(cx);
 885        let suggestion_anchor = editor.selections.newest_anchor().start;
 886        let language = snapshot.language_at(suggestion_anchor);
 887        let file = snapshot.file_at(suggestion_anchor).cloned();
 888        self.editor_enabled = {
 889            let file = file.as_ref();
 890            Some(
 891                file.map(|file| {
 892                    all_language_settings(Some(file), cx)
 893                        .edit_predictions_enabled_for_file(file, cx)
 894                })
 895                .unwrap_or(true),
 896            )
 897        };
 898        self.editor_show_predictions = editor.edit_predictions_enabled();
 899        self.edit_prediction_provider = editor.edit_prediction_provider();
 900        self.language = language.cloned();
 901        self.file = file;
 902        self.editor_focus_handle = Some(editor.focus_handle(cx));
 903
 904        cx.notify();
 905    }
 906}
 907
 908impl StatusItemView for EditPredictionButton {
 909    fn set_active_pane_item(
 910        &mut self,
 911        item: Option<&dyn ItemHandle>,
 912        _: &mut Window,
 913        cx: &mut Context<Self>,
 914    ) {
 915        if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
 916            self.editor_subscription = Some((
 917                cx.observe(&editor, Self::update_enabled),
 918                editor.entity_id().as_u64() as usize,
 919            ));
 920            self.update_enabled(editor, cx);
 921        } else {
 922            self.language = None;
 923            self.editor_subscription = None;
 924            self.editor_enabled = None;
 925        }
 926        cx.notify();
 927    }
 928}
 929
 930impl SupermavenButtonStatus {
 931    fn to_icon(&self) -> IconName {
 932        match self {
 933            SupermavenButtonStatus::Ready => IconName::Supermaven,
 934            SupermavenButtonStatus::Errored(_) => IconName::SupermavenError,
 935            SupermavenButtonStatus::NeedsActivation(_) => IconName::SupermavenInit,
 936            SupermavenButtonStatus::Initializing => IconName::SupermavenInit,
 937        }
 938    }
 939
 940    fn to_tooltip(&self) -> String {
 941        match self {
 942            SupermavenButtonStatus::Ready => "Supermaven is ready".to_string(),
 943            SupermavenButtonStatus::Errored(error) => format!("Supermaven error: {}", error),
 944            SupermavenButtonStatus::NeedsActivation(_) => "Supermaven needs activation".to_string(),
 945            SupermavenButtonStatus::Initializing => "Supermaven initializing".to_string(),
 946        }
 947    }
 948
 949    fn has_menu(&self) -> bool {
 950        match self {
 951            SupermavenButtonStatus::Ready | SupermavenButtonStatus::NeedsActivation(_) => true,
 952            SupermavenButtonStatus::Errored(_) | SupermavenButtonStatus::Initializing => false,
 953        }
 954    }
 955}
 956
 957async fn open_disabled_globs_setting_in_editor(
 958    workspace: WeakEntity<Workspace>,
 959    cx: &mut AsyncWindowContext,
 960) -> Result<()> {
 961    let settings_editor = workspace
 962        .update_in(cx, |_, window, cx| {
 963            create_and_open_local_file(paths::settings_file(), window, cx, |cx| {
 964                Rope::from_str(
 965                    settings::initial_user_settings_content().as_ref(),
 966                    cx.background_executor(),
 967                )
 968            })
 969        })?
 970        .await?
 971        .downcast::<Editor>()
 972        .unwrap();
 973
 974    settings_editor
 975        .downgrade()
 976        .update_in(cx, |item, window, cx| {
 977            let text = item.buffer().read(cx).snapshot(cx).text();
 978
 979            let settings = cx.global::<SettingsStore>();
 980
 981            // Ensure that we always have "edit_predictions { "disabled_globs": [] }"
 982            let edits = settings.edits_for_update(&text, |file| {
 983                file.project
 984                    .all_languages
 985                    .edit_predictions
 986                    .get_or_insert_with(Default::default)
 987                    .disabled_globs
 988                    .get_or_insert_with(Vec::new);
 989            });
 990
 991            if !edits.is_empty() {
 992                item.edit(edits, cx);
 993            }
 994
 995            let text = item.buffer().read(cx).snapshot(cx).text();
 996
 997            static DISABLED_GLOBS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
 998                Regex::new(r#""disabled_globs":\s*\[\s*(?P<content>(?:.|\n)*?)\s*\]"#).unwrap()
 999            });
1000            // Only capture [...]
1001            let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| {
1002                captures
1003                    .name("content")
1004                    .map(|inner_match| inner_match.start()..inner_match.end())
1005            });
1006            if let Some(range) = range {
1007                item.change_selections(
1008                    SelectionEffects::scroll(Autoscroll::newest()),
1009                    window,
1010                    cx,
1011                    |selections| {
1012                        selections.select_ranges(vec![range]);
1013                    },
1014                );
1015            }
1016        })?;
1017
1018    anyhow::Ok(())
1019}
1020
1021fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
1022    update_settings_file(fs, cx, move |settings, _| {
1023        settings
1024            .project
1025            .all_languages
1026            .features
1027            .get_or_insert_default()
1028            .edit_prediction_provider = Some(provider);
1029    });
1030}
1031
1032fn toggle_show_edit_predictions_for_language(
1033    language: Arc<Language>,
1034    fs: Arc<dyn Fs>,
1035    cx: &mut App,
1036) {
1037    let show_edit_predictions =
1038        all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
1039    update_settings_file(fs, cx, move |settings, _| {
1040        settings
1041            .project
1042            .all_languages
1043            .languages
1044            .0
1045            .entry(language.name().0)
1046            .or_default()
1047            .show_edit_predictions = Some(!show_edit_predictions);
1048    });
1049}
1050
1051fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
1052    update_settings_file(fs, cx, move |settings, _| {
1053        settings
1054            .project
1055            .all_languages
1056            .features
1057            .get_or_insert(Default::default())
1058            .edit_prediction_provider = Some(EditPredictionProvider::None);
1059    });
1060}
1061
1062fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &mut App) {
1063    let settings = AllLanguageSettings::get_global(cx);
1064    let current_mode = settings.edit_predictions_mode();
1065
1066    if current_mode != mode {
1067        update_settings_file(fs, cx, move |settings, _cx| {
1068            if let Some(edit_predictions) = settings.project.all_languages.edit_predictions.as_mut()
1069            {
1070                edit_predictions.mode = Some(mode);
1071            } else {
1072                settings.project.all_languages.edit_predictions =
1073                    Some(settings::EditPredictionSettingsContent {
1074                        mode: Some(mode),
1075                        ..Default::default()
1076                    });
1077            }
1078        });
1079    }
1080}