edit_prediction_button.rs

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