inline_completion_button.rs

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