inline_completion_button.rs

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