edit_prediction_button.rs

   1use anyhow::Result;
   2use client::{Client, UserStore, zed_urls};
   3use cloud_llm_client::UsageLimit;
   4use codestral::CodestralEditPredictionDelegate;
   5use copilot::{Copilot, Status};
   6use edit_prediction::{
   7    EditPredictionStore, MercuryFeatureFlag, SweepFeatureFlag, Zeta2FeatureFlag,
   8};
   9use edit_prediction_types::EditPredictionDelegateHandle;
  10use editor::{
  11    Editor, MultiBufferOffset, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll,
  12};
  13use feature_flags::FeatureFlagAppExt;
  14use fs::Fs;
  15use gpui::{
  16    Action, Animation, AnimationExt, App, AsyncWindowContext, Corner, Entity, FocusHandle,
  17    Focusable, IntoElement, ParentElement, Render, Subscription, WeakEntity, actions, div,
  18    ease_in_out, pulsating_between,
  19};
  20use indoc::indoc;
  21use language::{
  22    EditPredictionsMode, File, Language,
  23    language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
  24};
  25use ollama::OllamaEditPredictionDelegate;
  26use project::DisableAiSettings;
  27use regex::Regex;
  28use settings::{
  29    EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
  30    EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
  31    EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, Settings, SettingsStore,
  32    update_settings_file,
  33};
  34use std::{
  35    sync::{Arc, LazyLock},
  36    time::Duration,
  37};
  38use supermaven::{AccountStatus, Supermaven};
  39use ui::{
  40    Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
  41    IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
  42};
  43use util::ResultExt as _;
  44use workspace::{
  45    StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
  46    notifications::NotificationId,
  47};
  48use zed_actions::{OpenBrowser, OpenSettingsAt};
  49
  50use crate::{
  51    CaptureExample, RatePredictions, rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag,
  52};
  53
  54actions!(
  55    edit_prediction,
  56    [
  57        /// Toggles the edit prediction menu.
  58        ToggleMenu
  59    ]
  60);
  61
  62const COPILOT_SETTINGS_PATH: &str = "/settings/copilot";
  63const COPILOT_SETTINGS_URL: &str = concat!("https://github.com", "/settings/copilot");
  64const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security";
  65
  66struct CopilotErrorToast;
  67
  68pub struct EditPredictionButton {
  69    editor_subscription: Option<(Subscription, usize)>,
  70    editor_enabled: Option<bool>,
  71    editor_show_predictions: bool,
  72    editor_focus_handle: Option<FocusHandle>,
  73    language: Option<Arc<Language>>,
  74    file: Option<Arc<dyn File>>,
  75    edit_prediction_provider: Option<Arc<dyn EditPredictionDelegateHandle>>,
  76    fs: Arc<dyn Fs>,
  77    user_store: Entity<UserStore>,
  78    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
  79}
  80
  81enum SupermavenButtonStatus {
  82    Ready,
  83    Errored(String),
  84    NeedsActivation(String),
  85    Initializing,
  86}
  87
  88impl Render for EditPredictionButton {
  89    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
  90        // Return empty div if AI is disabled
  91        if DisableAiSettings::get_global(cx).disable_ai {
  92            return div().hidden();
  93        }
  94
  95        let language_settings = all_language_settings(None, cx);
  96
  97        match language_settings.edit_predictions.provider {
  98            EditPredictionProvider::Copilot => {
  99                let Some(copilot) = Copilot::global(cx) else {
 100                    return div().hidden();
 101                };
 102                let status = copilot.read(cx).status();
 103
 104                let enabled = self.editor_enabled.unwrap_or(false);
 105
 106                let icon = match status {
 107                    Status::Error(_) => IconName::CopilotError,
 108                    Status::Authorized => {
 109                        if enabled {
 110                            IconName::Copilot
 111                        } else {
 112                            IconName::CopilotDisabled
 113                        }
 114                    }
 115                    _ => IconName::CopilotInit,
 116                };
 117
 118                if let Status::Error(e) = status {
 119                    return div().child(
 120                        IconButton::new("copilot-error", icon)
 121                            .icon_size(IconSize::Small)
 122                            .on_click(cx.listener(move |_, _, window, cx| {
 123                                if let Some(workspace) = window.root::<Workspace>().flatten() {
 124                                    workspace.update(cx, |workspace, cx| {
 125                                        workspace.show_toast(
 126                                            Toast::new(
 127                                                NotificationId::unique::<CopilotErrorToast>(),
 128                                                format!("Copilot can't be started: {}", e),
 129                                            )
 130                                            .on_click(
 131                                                "Reinstall Copilot",
 132                                                |window, cx| {
 133                                                    copilot::reinstall_and_sign_in(window, cx)
 134                                                },
 135                                            ),
 136                                            cx,
 137                                        );
 138                                    });
 139                                }
 140                            }))
 141                            .tooltip(|_window, cx| {
 142                                Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx)
 143                            }),
 144                    );
 145                }
 146                let this = cx.weak_entity();
 147
 148                div().child(
 149                    PopoverMenu::new("copilot")
 150                        .menu(move |window, cx| {
 151                            let current_status = Copilot::global(cx)?.read(cx).status();
 152                            match current_status {
 153                                Status::Authorized => this.update(cx, |this, cx| {
 154                                    this.build_copilot_context_menu(window, cx)
 155                                }),
 156                                _ => this.update(cx, |this, cx| {
 157                                    this.build_copilot_start_menu(window, cx)
 158                                }),
 159                            }
 160                            .ok()
 161                        })
 162                        .anchor(Corner::BottomRight)
 163                        .trigger_with_tooltip(
 164                            IconButton::new("copilot-icon", icon),
 165                            |_window, cx| Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx),
 166                        )
 167                        .with_handle(self.popover_menu_handle.clone()),
 168                )
 169            }
 170
 171            EditPredictionProvider::Supermaven => {
 172                let Some(supermaven) = Supermaven::global(cx) else {
 173                    return div();
 174                };
 175
 176                let supermaven = supermaven.read(cx);
 177
 178                let status = match supermaven {
 179                    Supermaven::Starting => SupermavenButtonStatus::Initializing,
 180                    Supermaven::FailedDownload { error } => {
 181                        SupermavenButtonStatus::Errored(error.to_string())
 182                    }
 183                    Supermaven::Spawned(agent) => {
 184                        let account_status = agent.account_status.clone();
 185                        match account_status {
 186                            AccountStatus::NeedsActivation { activate_url } => {
 187                                SupermavenButtonStatus::NeedsActivation(activate_url)
 188                            }
 189                            AccountStatus::Unknown => SupermavenButtonStatus::Initializing,
 190                            AccountStatus::Ready => SupermavenButtonStatus::Ready,
 191                        }
 192                    }
 193                    Supermaven::Error { error } => {
 194                        SupermavenButtonStatus::Errored(error.to_string())
 195                    }
 196                };
 197
 198                let icon = status.to_icon();
 199                let tooltip_text = status.to_tooltip();
 200                let has_menu = status.has_menu();
 201                let this = cx.weak_entity();
 202                let fs = self.fs.clone();
 203
 204                div().child(
 205                    PopoverMenu::new("supermaven")
 206                        .menu(move |window, cx| match &status {
 207                            SupermavenButtonStatus::NeedsActivation(activate_url) => {
 208                                Some(ContextMenu::build(window, cx, |menu, _, _| {
 209                                    let fs = fs.clone();
 210                                    let activate_url = activate_url.clone();
 211
 212                                    menu.entry("Sign In", None, move |_, cx| {
 213                                        cx.open_url(activate_url.as_str())
 214                                    })
 215                                    .entry(
 216                                        "Use Zed AI",
 217                                        None,
 218                                        move |_, cx| {
 219                                            set_completion_provider(
 220                                                fs.clone(),
 221                                                cx,
 222                                                EditPredictionProvider::Zed,
 223                                            )
 224                                        },
 225                                    )
 226                                }))
 227                            }
 228                            SupermavenButtonStatus::Ready => this
 229                                .update(cx, |this, cx| {
 230                                    this.build_supermaven_context_menu(window, cx)
 231                                })
 232                                .ok(),
 233                            _ => None,
 234                        })
 235                        .anchor(Corner::BottomRight)
 236                        .trigger_with_tooltip(
 237                            IconButton::new("supermaven-icon", icon),
 238                            move |window, cx| {
 239                                if has_menu {
 240                                    Tooltip::for_action(tooltip_text.clone(), &ToggleMenu, cx)
 241                                } else {
 242                                    Tooltip::text(tooltip_text.clone())(window, cx)
 243                                }
 244                            },
 245                        )
 246                        .with_handle(self.popover_menu_handle.clone()),
 247                )
 248            }
 249
 250            EditPredictionProvider::Codestral => {
 251                let enabled = self.editor_enabled.unwrap_or(true);
 252                let has_api_key = CodestralEditPredictionDelegate::has_api_key(cx);
 253                let this = cx.weak_entity();
 254
 255                let tooltip_meta = if has_api_key {
 256                    "Powered by Codestral"
 257                } else {
 258                    "Missing API key for Codestral"
 259                };
 260
 261                div().child(
 262                    PopoverMenu::new("codestral")
 263                        .menu(move |window, cx| {
 264                            this.update(cx, |this, cx| {
 265                                this.build_codestral_context_menu(window, cx)
 266                            })
 267                            .ok()
 268                        })
 269                        .anchor(Corner::BottomRight)
 270                        .trigger_with_tooltip(
 271                            IconButton::new("codestral-icon", IconName::AiMistral)
 272                                .shape(IconButtonShape::Square)
 273                                .when(!has_api_key, |this| {
 274                                    this.indicator(Indicator::dot().color(Color::Error))
 275                                        .indicator_border_color(Some(
 276                                            cx.theme().colors().status_bar_background,
 277                                        ))
 278                                })
 279                                .when(has_api_key && !enabled, |this| {
 280                                    this.indicator(Indicator::dot().color(Color::Ignored))
 281                                        .indicator_border_color(Some(
 282                                            cx.theme().colors().status_bar_background,
 283                                        ))
 284                                }),
 285                            move |_window, cx| {
 286                                Tooltip::with_meta(
 287                                    "Edit Prediction",
 288                                    Some(&ToggleMenu),
 289                                    tooltip_meta,
 290                                    cx,
 291                                )
 292                            },
 293                        )
 294                        .with_handle(self.popover_menu_handle.clone()),
 295                )
 296            }
 297            EditPredictionProvider::Ollama => {
 298                let enabled = self.editor_enabled.unwrap_or(true);
 299                let this = cx.weak_entity();
 300
 301                div().child(
 302                    PopoverMenu::new("ollama")
 303                        .menu(move |window, cx| {
 304                            this.update(cx, |this, cx| {
 305                                this.build_edit_prediction_context_menu(
 306                                    EditPredictionProvider::Ollama,
 307                                    window,
 308                                    cx,
 309                                )
 310                            })
 311                            .ok()
 312                        })
 313                        .anchor(Corner::BottomRight)
 314                        .trigger_with_tooltip(
 315                            IconButton::new("ollama-icon", IconName::AiOllama)
 316                                .shape(IconButtonShape::Square)
 317                                .when(!enabled, |this| {
 318                                    this.indicator(Indicator::dot().color(Color::Ignored))
 319                                        .indicator_border_color(Some(
 320                                            cx.theme().colors().status_bar_background,
 321                                        ))
 322                                }),
 323                            move |_window, cx| {
 324                                let settings = all_language_settings(None, cx);
 325                                let tooltip_meta = match settings
 326                                    .edit_predictions
 327                                    .ollama
 328                                    .model
 329                                    .as_deref()
 330                                {
 331                                    Some(model) if !model.trim().is_empty() => {
 332                                        format!("Powered by Ollama ({model})")
 333                                    }
 334                                    _ => {
 335                                        "Ollama model not configured — configure a model before use"
 336                                            .to_string()
 337                                    }
 338                                };
 339
 340                                Tooltip::with_meta(
 341                                    "Edit Prediction",
 342                                    Some(&ToggleMenu),
 343                                    tooltip_meta,
 344                                    cx,
 345                                )
 346                            },
 347                        )
 348                        .with_handle(self.popover_menu_handle.clone()),
 349                )
 350            }
 351            provider @ (EditPredictionProvider::Experimental(_) | EditPredictionProvider::Zed) => {
 352                let enabled = self.editor_enabled.unwrap_or(true);
 353
 354                let ep_icon;
 355                let tooltip_meta;
 356                let mut missing_token = false;
 357
 358                match provider {
 359                    EditPredictionProvider::Experimental(
 360                        EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
 361                    ) => {
 362                        ep_icon = IconName::SweepAi;
 363                        tooltip_meta = if missing_token {
 364                            "Missing API key for Sweep"
 365                        } else {
 366                            "Powered by Sweep"
 367                        };
 368                        missing_token = edit_prediction::EditPredictionStore::try_global(cx)
 369                            .is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token(cx));
 370                    }
 371                    EditPredictionProvider::Experimental(
 372                        EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
 373                    ) => {
 374                        ep_icon = IconName::Inception;
 375                        missing_token = edit_prediction::EditPredictionStore::try_global(cx)
 376                            .is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token(cx));
 377                        tooltip_meta = if missing_token {
 378                            "Missing API key for Mercury"
 379                        } else {
 380                            "Powered by Mercury"
 381                        };
 382                    }
 383                    _ => {
 384                        ep_icon = if enabled {
 385                            IconName::ZedPredict
 386                        } else {
 387                            IconName::ZedPredictDisabled
 388                        };
 389                        tooltip_meta = "Powered by Zeta"
 390                    }
 391                };
 392
 393                if edit_prediction::should_show_upsell_modal() {
 394                    let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
 395                        "Choose a Plan"
 396                    } else {
 397                        "Sign In To Use"
 398                    };
 399
 400                    return div().child(
 401                        IconButton::new("zed-predict-pending-button", ep_icon)
 402                            .shape(IconButtonShape::Square)
 403                            .indicator(Indicator::dot().color(Color::Muted))
 404                            .indicator_border_color(Some(cx.theme().colors().status_bar_background))
 405                            .tooltip(move |_window, cx| {
 406                                Tooltip::with_meta("Edit Predictions", None, tooltip_meta, cx)
 407                            })
 408                            .on_click(cx.listener(move |_, _, window, cx| {
 409                                telemetry::event!(
 410                                    "Pending ToS Clicked",
 411                                    source = "Edit Prediction Status Button"
 412                                );
 413                                window.dispatch_action(
 414                                    zed_actions::OpenZedPredictOnboarding.boxed_clone(),
 415                                    cx,
 416                                );
 417                            })),
 418                    );
 419                }
 420
 421                let mut over_limit = false;
 422
 423                if let Some(usage) = self
 424                    .edit_prediction_provider
 425                    .as_ref()
 426                    .and_then(|provider| provider.usage(cx))
 427                {
 428                    over_limit = usage.over_limit()
 429                }
 430
 431                let show_editor_predictions = self.editor_show_predictions;
 432                let user = self.user_store.read(cx).current_user();
 433
 434                let indicator_color = if missing_token {
 435                    Some(Color::Error)
 436                } else if enabled && (!show_editor_predictions || over_limit) {
 437                    Some(if over_limit {
 438                        Color::Error
 439                    } else {
 440                        Color::Muted
 441                    })
 442                } else {
 443                    None
 444                };
 445
 446                let icon_button = IconButton::new("zed-predict-pending-button", ep_icon)
 447                    .shape(IconButtonShape::Square)
 448                    .when_some(indicator_color, |this, color| {
 449                        this.indicator(Indicator::dot().color(color))
 450                            .indicator_border_color(Some(cx.theme().colors().status_bar_background))
 451                    })
 452                    .when(!self.popover_menu_handle.is_deployed(), |element| {
 453                        let user = user.clone();
 454
 455                        element.tooltip(move |_window, cx| {
 456                            let description = if enabled {
 457                                if show_editor_predictions {
 458                                    tooltip_meta
 459                                } else if user.is_none() {
 460                                    "Sign In To Use"
 461                                } else {
 462                                    "Hidden For This File"
 463                                }
 464                            } else {
 465                                "Disabled For This File"
 466                            };
 467
 468                            Tooltip::with_meta(
 469                                "Edit Prediction",
 470                                Some(&ToggleMenu),
 471                                description,
 472                                cx,
 473                            )
 474                        })
 475                    });
 476
 477                let this = cx.weak_entity();
 478
 479                let mut popover_menu = PopoverMenu::new("edit-prediction")
 480                    .when(user.is_some(), |popover_menu| {
 481                        let this = this.clone();
 482
 483                        popover_menu.menu(move |window, cx| {
 484                            this.update(cx, |this, cx| {
 485                                this.build_edit_prediction_context_menu(provider, window, cx)
 486                            })
 487                            .ok()
 488                        })
 489                    })
 490                    .when(user.is_none(), |popover_menu| {
 491                        let this = this.clone();
 492
 493                        popover_menu.menu(move |window, cx| {
 494                            this.update(cx, |this, cx| {
 495                                this.build_zeta_upsell_context_menu(window, cx)
 496                            })
 497                            .ok()
 498                        })
 499                    })
 500                    .anchor(Corner::BottomRight)
 501                    .with_handle(self.popover_menu_handle.clone());
 502
 503                let is_refreshing = self
 504                    .edit_prediction_provider
 505                    .as_ref()
 506                    .is_some_and(|provider| provider.is_refreshing(cx));
 507
 508                if is_refreshing {
 509                    popover_menu = popover_menu.trigger(
 510                        icon_button.with_animation(
 511                            "pulsating-label",
 512                            Animation::new(Duration::from_secs(2))
 513                                .repeat()
 514                                .with_easing(pulsating_between(0.2, 1.0)),
 515                            |icon_button, delta| icon_button.alpha(delta),
 516                        ),
 517                    );
 518                } else {
 519                    popover_menu = popover_menu.trigger(icon_button);
 520                }
 521
 522                div().child(popover_menu.into_any_element())
 523            }
 524
 525            EditPredictionProvider::None => div().hidden(),
 526        }
 527    }
 528}
 529
 530impl EditPredictionButton {
 531    pub fn new(
 532        fs: Arc<dyn Fs>,
 533        user_store: Entity<UserStore>,
 534        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
 535        client: Arc<Client>,
 536        cx: &mut Context<Self>,
 537    ) -> Self {
 538        if let Some(copilot) = Copilot::global(cx) {
 539            cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
 540        }
 541
 542        cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
 543            .detach();
 544
 545        cx.observe_global::<EditPredictionStore>(move |_, cx| cx.notify())
 546            .detach();
 547
 548        let sweep_api_token_task = edit_prediction::sweep_ai::load_sweep_api_token(cx);
 549        let mercury_api_token_task = edit_prediction::mercury::load_mercury_api_token(cx);
 550
 551        cx.spawn(async move |this, cx| {
 552            _ = futures::join!(sweep_api_token_task, mercury_api_token_task);
 553            this.update(cx, |_, cx| {
 554                cx.notify();
 555            })
 556            .ok();
 557        })
 558        .detach();
 559
 560        CodestralEditPredictionDelegate::ensure_api_key_loaded(client.http_client(), cx);
 561
 562        Self {
 563            editor_subscription: None,
 564            editor_enabled: None,
 565            editor_show_predictions: true,
 566            editor_focus_handle: None,
 567            language: None,
 568            file: None,
 569            edit_prediction_provider: None,
 570            user_store,
 571            popover_menu_handle,
 572            fs,
 573        }
 574    }
 575
 576    fn get_available_providers(&self, cx: &mut App) -> Vec<EditPredictionProvider> {
 577        let mut providers = Vec::new();
 578
 579        providers.push(EditPredictionProvider::Zed);
 580
 581        if cx.has_flag::<Zeta2FeatureFlag>() {
 582            providers.push(EditPredictionProvider::Experimental(
 583                EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
 584            ));
 585        }
 586
 587        if let Some(copilot) = Copilot::global(cx) {
 588            if matches!(copilot.read(cx).status(), Status::Authorized) {
 589                providers.push(EditPredictionProvider::Copilot);
 590            }
 591        }
 592
 593        if let Some(supermaven) = Supermaven::global(cx) {
 594            if let Supermaven::Spawned(agent) = supermaven.read(cx) {
 595                if matches!(agent.account_status, AccountStatus::Ready) {
 596                    providers.push(EditPredictionProvider::Supermaven);
 597                }
 598            }
 599        }
 600
 601        if CodestralEditPredictionDelegate::has_api_key(cx) {
 602            providers.push(EditPredictionProvider::Codestral);
 603        }
 604
 605        if OllamaEditPredictionDelegate::is_available(cx) {
 606            providers.push(EditPredictionProvider::Ollama);
 607        }
 608
 609        if cx.has_flag::<SweepFeatureFlag>()
 610            && edit_prediction::sweep_ai::sweep_api_token(cx)
 611                .read(cx)
 612                .has_key()
 613        {
 614            providers.push(EditPredictionProvider::Experimental(
 615                EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
 616            ));
 617        }
 618
 619        if cx.has_flag::<MercuryFeatureFlag>()
 620            && edit_prediction::mercury::mercury_api_token(cx)
 621                .read(cx)
 622                .has_key()
 623        {
 624            providers.push(EditPredictionProvider::Experimental(
 625                EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
 626            ));
 627        }
 628
 629        providers
 630    }
 631
 632    fn add_provider_switching_section(
 633        &self,
 634        mut menu: ContextMenu,
 635        current_provider: EditPredictionProvider,
 636        cx: &mut App,
 637    ) -> ContextMenu {
 638        let available_providers = self.get_available_providers(cx);
 639
 640        let providers: Vec<_> = available_providers
 641            .into_iter()
 642            .filter(|p| *p != EditPredictionProvider::None)
 643            .collect();
 644
 645        if !providers.is_empty() {
 646            menu = menu.separator().header("Providers");
 647
 648            for provider in providers {
 649                let is_current = provider == current_provider;
 650                let fs = self.fs.clone();
 651
 652                let name = match provider {
 653                    EditPredictionProvider::Zed => "Zed AI",
 654                    EditPredictionProvider::Copilot => "GitHub Copilot",
 655                    EditPredictionProvider::Supermaven => "Supermaven",
 656                    EditPredictionProvider::Codestral => "Codestral",
 657                    EditPredictionProvider::Ollama => "Ollama",
 658                    EditPredictionProvider::Experimental(
 659                        EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
 660                    ) => "Sweep",
 661                    EditPredictionProvider::Experimental(
 662                        EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
 663                    ) => "Mercury",
 664                    EditPredictionProvider::Experimental(
 665                        EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
 666                    ) => "Zeta2",
 667                    EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => {
 668                        continue;
 669                    }
 670                };
 671
 672                menu = menu.item(
 673                    ContextMenuEntry::new(name)
 674                        .toggleable(IconPosition::Start, is_current)
 675                        .handler(move |_, cx| {
 676                            set_completion_provider(fs.clone(), cx, provider);
 677                        }),
 678                )
 679            }
 680        }
 681
 682        menu
 683    }
 684
 685    pub fn build_copilot_start_menu(
 686        &mut self,
 687        window: &mut Window,
 688        cx: &mut Context<Self>,
 689    ) -> Entity<ContextMenu> {
 690        let fs = self.fs.clone();
 691        ContextMenu::build(window, cx, |menu, _, _| {
 692            menu.entry("Sign In to Copilot", None, copilot::initiate_sign_in)
 693                .entry("Disable Copilot", None, {
 694                    let fs = fs.clone();
 695                    move |_window, cx| hide_copilot(fs.clone(), cx)
 696                })
 697                .separator()
 698                .entry("Use Zed AI", None, {
 699                    let fs = fs.clone();
 700                    move |_window, cx| {
 701                        set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
 702                    }
 703                })
 704        })
 705    }
 706
 707    pub fn build_language_settings_menu(
 708        &self,
 709        mut menu: ContextMenu,
 710        window: &Window,
 711        cx: &mut App,
 712    ) -> ContextMenu {
 713        let fs = self.fs.clone();
 714        let line_height = window.line_height();
 715
 716        menu = menu.header("Show Edit Predictions For");
 717
 718        let language_state = self.language.as_ref().map(|language| {
 719            (
 720                language.clone(),
 721                language_settings::language_settings(Some(language.name()), None, cx)
 722                    .show_edit_predictions,
 723            )
 724        });
 725
 726        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
 727            let entry = ContextMenuEntry::new("This Buffer")
 728                .toggleable(IconPosition::Start, self.editor_show_predictions)
 729                .action(Box::new(editor::actions::ToggleEditPrediction))
 730                .handler(move |window, cx| {
 731                    editor_focus_handle.dispatch_action(
 732                        &editor::actions::ToggleEditPrediction,
 733                        window,
 734                        cx,
 735                    );
 736                });
 737
 738            match language_state.clone() {
 739                Some((language, false)) => {
 740                    menu = menu.item(
 741                        entry
 742                            .disabled(true)
 743                            .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_cx| {
 744                                Label::new(format!("Edit predictions cannot be toggled for this buffer because they are disabled for {}", language.name()))
 745                                    .into_any_element()
 746                            })
 747                    );
 748                }
 749                Some(_) | None => menu = menu.item(entry),
 750            }
 751        }
 752
 753        if let Some((language, language_enabled)) = language_state {
 754            let fs = fs.clone();
 755
 756            menu = menu.toggleable_entry(
 757                language.name(),
 758                language_enabled,
 759                IconPosition::Start,
 760                None,
 761                move |_, cx| {
 762                    toggle_show_edit_predictions_for_language(language.clone(), fs.clone(), cx)
 763                },
 764            );
 765        }
 766
 767        let settings = AllLanguageSettings::get_global(cx);
 768
 769        let globally_enabled = settings.show_edit_predictions(None, cx);
 770        let entry = ContextMenuEntry::new("All Files")
 771            .toggleable(IconPosition::Start, globally_enabled)
 772            .action(workspace::ToggleEditPrediction.boxed_clone())
 773            .handler(|window, cx| {
 774                window.dispatch_action(workspace::ToggleEditPrediction.boxed_clone(), cx)
 775            });
 776        menu = menu.item(entry);
 777
 778        let provider = settings.edit_predictions.provider;
 779        let current_mode = settings.edit_predictions_mode();
 780        let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
 781        let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
 782
 783        menu = menu
 784                .separator()
 785                .header("Display Modes")
 786                .item(
 787                    ContextMenuEntry::new("Eager")
 788                        .toggleable(IconPosition::Start, eager_mode)
 789                        .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_| {
 790                            Label::new("Display predictions inline when there are no language server completions available.").into_any_element()
 791                        })
 792                        .handler({
 793                            let fs = fs.clone();
 794                            move |_, cx| {
 795                                toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Eager, cx)
 796                            }
 797                        }),
 798                )
 799                .item(
 800                    ContextMenuEntry::new("Subtle")
 801                        .toggleable(IconPosition::Start, subtle_mode)
 802                        .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_| {
 803                            Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element()
 804                        })
 805                        .handler({
 806                            let fs = fs.clone();
 807                            move |_, cx| {
 808                                toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Subtle, cx)
 809                            }
 810                        }),
 811                );
 812
 813        menu = menu.separator().header("Privacy");
 814
 815        if matches!(
 816            provider,
 817            EditPredictionProvider::Zed
 818                | EditPredictionProvider::Experimental(
 819                    EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
 820                )
 821        ) {
 822            if let Some(provider) = &self.edit_prediction_provider {
 823                let data_collection = provider.data_collection_state(cx);
 824
 825                if data_collection.is_supported() {
 826                    let provider = provider.clone();
 827                    let enabled = data_collection.is_enabled();
 828                    let is_open_source = data_collection.is_project_open_source();
 829                    let is_collecting = data_collection.is_enabled();
 830                    let (icon_name, icon_color) = if is_open_source && is_collecting {
 831                        (IconName::Check, Color::Success)
 832                    } else {
 833                        (IconName::Check, Color::Accent)
 834                    };
 835
 836                    menu = menu.item(
 837                        ContextMenuEntry::new("Training Data Collection")
 838                            .toggleable(IconPosition::Start, data_collection.is_enabled())
 839                            .icon(icon_name)
 840                            .icon_color(icon_color)
 841                            .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| {
 842                                let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
 843                                    (true, true) => (
 844                                        "Project identified as open source, and you're sharing data.",
 845                                        Color::Default,
 846                                        IconName::Check,
 847                                        Color::Success,
 848                                    ),
 849                                    (true, false) => (
 850                                        "Project identified as open source, but you're not sharing data.",
 851                                        Color::Muted,
 852                                        IconName::Close,
 853                                        Color::Muted,
 854                                    ),
 855                                    (false, true) => (
 856                                        "Project not identified as open source. No data captured.",
 857                                        Color::Muted,
 858                                        IconName::Close,
 859                                        Color::Muted,
 860                                    ),
 861                                    (false, false) => (
 862                                        "Project not identified as open source, and setting turned off.",
 863                                        Color::Muted,
 864                                        IconName::Close,
 865                                        Color::Muted,
 866                                    ),
 867                                };
 868                                v_flex()
 869                                    .gap_2()
 870                                    .child(
 871                                        Label::new(indoc!{
 872                                            "Help us improve our open dataset model by sharing data from open source repositories. \
 873                                            Zed must detect a license file in your repo for this setting to take effect. \
 874                                            Files with sensitive data and secrets are excluded by default."
 875                                        })
 876                                    )
 877                                    .child(
 878                                        h_flex()
 879                                            .items_start()
 880                                            .pt_2()
 881                                            .pr_1()
 882                                            .flex_1()
 883                                            .gap_1p5()
 884                                            .border_t_1()
 885                                            .border_color(cx.theme().colors().border_variant)
 886                                            .child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)))
 887                                            .child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx)))
 888                                    )
 889                                    .into_any_element()
 890                            })
 891                            .handler(move |_, cx| {
 892                                provider.toggle_data_collection(cx);
 893
 894                                if !enabled {
 895                                    telemetry::event!(
 896                                        "Data Collection Enabled",
 897                                        source = "Edit Prediction Status Menu"
 898                                    );
 899                                } else {
 900                                    telemetry::event!(
 901                                        "Data Collection Disabled",
 902                                        source = "Edit Prediction Status Menu"
 903                                    );
 904                                }
 905                            })
 906                    );
 907
 908                    if is_collecting && !is_open_source {
 909                        menu = menu.item(
 910                            ContextMenuEntry::new("No data captured.")
 911                                .disabled(true)
 912                                .icon(IconName::Close)
 913                                .icon_color(Color::Error)
 914                                .icon_size(IconSize::Small),
 915                        );
 916                    }
 917                }
 918            }
 919        }
 920
 921        menu = menu.item(
 922            ContextMenuEntry::new("Configure Excluded Files")
 923                .icon(IconName::LockOutlined)
 924                .icon_color(Color::Muted)
 925                .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| {
 926                    Label::new(indoc!{"
 927                        Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
 928                })
 929                .handler(move |window, cx| {
 930                    if let Some(workspace) = window.root().flatten() {
 931                        let workspace = workspace.downgrade();
 932                        window
 933                            .spawn(cx, async |cx| {
 934                                open_disabled_globs_setting_in_editor(
 935                                    workspace,
 936                                    cx,
 937                                ).await
 938                            })
 939                            .detach_and_log_err(cx);
 940                    }
 941                }),
 942        ).item(
 943            ContextMenuEntry::new("View Docs")
 944                .icon(IconName::FileGeneric)
 945                .icon_color(Color::Muted)
 946                .handler(move |_, cx| {
 947                    cx.open_url(PRIVACY_DOCS);
 948                })
 949        );
 950
 951        if !self.editor_enabled.unwrap_or(true) {
 952            menu = menu.item(
 953                ContextMenuEntry::new("This file is excluded.")
 954                    .disabled(true)
 955                    .icon(IconName::ZedPredictDisabled)
 956                    .icon_size(IconSize::Small),
 957            );
 958        }
 959
 960        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
 961            menu = menu
 962                .separator()
 963                .header("Actions")
 964                .entry(
 965                    "Predict Edit at Cursor",
 966                    Some(Box::new(ShowEditPrediction)),
 967                    {
 968                        let editor_focus_handle = editor_focus_handle.clone();
 969                        move |window, cx| {
 970                            editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
 971                        }
 972                    },
 973                )
 974                .context(editor_focus_handle)
 975                .when(
 976                    cx.has_flag::<PredictEditsRatePredictionsFeatureFlag>(),
 977                    |this| {
 978                        this.action(
 979                            "Capture Edit Prediction Example",
 980                            CaptureExample.boxed_clone(),
 981                        )
 982                        .action("Rate Predictions", RatePredictions.boxed_clone())
 983                    },
 984                );
 985        }
 986
 987        menu
 988    }
 989
 990    fn build_copilot_context_menu(
 991        &self,
 992        window: &mut Window,
 993        cx: &mut Context<Self>,
 994    ) -> Entity<ContextMenu> {
 995        let all_language_settings = all_language_settings(None, cx);
 996        let copilot_config = copilot::copilot_chat::CopilotChatConfiguration {
 997            enterprise_uri: all_language_settings
 998                .edit_predictions
 999                .copilot
1000                .enterprise_uri
1001                .clone(),
1002        };
1003        let settings_url = copilot_settings_url(copilot_config.enterprise_uri.as_deref());
1004
1005        ContextMenu::build(window, cx, |menu, window, cx| {
1006            let menu = self.build_language_settings_menu(menu, window, cx);
1007            let menu =
1008                self.add_provider_switching_section(menu, EditPredictionProvider::Copilot, cx);
1009
1010            menu.separator()
1011                .link(
1012                    "Go to Copilot Settings",
1013                    OpenBrowser { url: settings_url }.boxed_clone(),
1014                )
1015                .action("Sign Out", copilot::SignOut.boxed_clone())
1016        })
1017    }
1018
1019    fn build_supermaven_context_menu(
1020        &self,
1021        window: &mut Window,
1022        cx: &mut Context<Self>,
1023    ) -> Entity<ContextMenu> {
1024        ContextMenu::build(window, cx, |menu, window, cx| {
1025            let menu = self.build_language_settings_menu(menu, window, cx);
1026            let menu =
1027                self.add_provider_switching_section(menu, EditPredictionProvider::Supermaven, cx);
1028
1029            menu.separator()
1030                .action("Sign Out", supermaven::SignOut.boxed_clone())
1031        })
1032    }
1033
1034    fn build_codestral_context_menu(
1035        &self,
1036        window: &mut Window,
1037        cx: &mut Context<Self>,
1038    ) -> Entity<ContextMenu> {
1039        ContextMenu::build(window, cx, |menu, window, cx| {
1040            let menu = self.build_language_settings_menu(menu, window, cx);
1041            let menu =
1042                self.add_provider_switching_section(menu, EditPredictionProvider::Codestral, cx);
1043
1044            menu
1045        })
1046    }
1047
1048    fn build_edit_prediction_context_menu(
1049        &self,
1050        provider: EditPredictionProvider,
1051        window: &mut Window,
1052        cx: &mut Context<Self>,
1053    ) -> Entity<ContextMenu> {
1054        ContextMenu::build(window, cx, |mut menu, window, cx| {
1055            if let Some(usage) = self
1056                .edit_prediction_provider
1057                .as_ref()
1058                .and_then(|provider| provider.usage(cx))
1059            {
1060                menu = menu.header("Usage");
1061                menu = menu
1062                    .custom_entry(
1063                        move |_window, cx| {
1064                            let used_percentage = match usage.limit {
1065                                UsageLimit::Limited(limit) => {
1066                                    Some((usage.amount as f32 / limit as f32) * 100.)
1067                                }
1068                                UsageLimit::Unlimited => None,
1069                            };
1070
1071                            h_flex()
1072                                .flex_1()
1073                                .gap_1p5()
1074                                .children(
1075                                    used_percentage.map(|percent| {
1076                                        ProgressBar::new("usage", percent, 100., cx)
1077                                    }),
1078                                )
1079                                .child(
1080                                    Label::new(match usage.limit {
1081                                        UsageLimit::Limited(limit) => {
1082                                            format!("{} / {limit}", usage.amount)
1083                                        }
1084                                        UsageLimit::Unlimited => format!("{} / ∞", usage.amount),
1085                                    })
1086                                    .size(LabelSize::Small)
1087                                    .color(Color::Muted),
1088                                )
1089                                .into_any_element()
1090                        },
1091                        move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1092                    )
1093                    .when(usage.over_limit(), |menu| -> ContextMenu {
1094                        menu.entry("Subscribe to increase your limit", None, |_window, cx| {
1095                            cx.open_url(&zed_urls::account_url(cx))
1096                        })
1097                    })
1098                    .separator();
1099            } else if self.user_store.read(cx).account_too_young() {
1100                menu = menu
1101                    .custom_entry(
1102                        |_window, _cx| {
1103                            Label::new("Your GitHub account is less than 30 days old.")
1104                                .size(LabelSize::Small)
1105                                .color(Color::Warning)
1106                                .into_any_element()
1107                        },
1108                        |_window, cx| cx.open_url(&zed_urls::account_url(cx)),
1109                    )
1110                    .entry("Upgrade to Zed Pro or contact us.", None, |_window, cx| {
1111                        cx.open_url(&zed_urls::account_url(cx))
1112                    })
1113                    .separator();
1114            } else if self.user_store.read(cx).has_overdue_invoices() {
1115                menu = menu
1116                    .custom_entry(
1117                        |_window, _cx| {
1118                            Label::new("You have an outstanding invoice")
1119                                .size(LabelSize::Small)
1120                                .color(Color::Warning)
1121                                .into_any_element()
1122                        },
1123                        |_window, cx| {
1124                            cx.open_url(&zed_urls::account_url(cx))
1125                        },
1126                    )
1127                    .entry(
1128                        "Check your payment status or contact us at billing-support@zed.dev to continue using this feature.",
1129                        None,
1130                        |_window, cx| {
1131                            cx.open_url(&zed_urls::account_url(cx))
1132                        },
1133                    )
1134                    .separator();
1135            }
1136
1137            menu = self.build_language_settings_menu(menu, window, cx);
1138
1139            if cx.has_flag::<Zeta2FeatureFlag>() {
1140                let settings = all_language_settings(None, cx);
1141                let context_retrieval = settings.edit_predictions.use_context;
1142                menu = menu.separator().header("Context Retrieval").item(
1143                    ContextMenuEntry::new("Enable Context Retrieval")
1144                        .toggleable(IconPosition::Start, context_retrieval)
1145                        .action(workspace::ToggleEditPrediction.boxed_clone())
1146                        .handler({
1147                            let fs = self.fs.clone();
1148                            move |_, cx| {
1149                                update_settings_file(fs.clone(), cx, move |settings, _| {
1150                                    settings
1151                                        .project
1152                                        .all_languages
1153                                        .features
1154                                        .get_or_insert_default()
1155                                        .experimental_edit_prediction_context_retrieval =
1156                                        Some(!context_retrieval)
1157                                });
1158                            }
1159                        }),
1160                );
1161            }
1162
1163            menu = self.add_provider_switching_section(menu, provider, cx);
1164            menu = menu.separator().item(
1165                ContextMenuEntry::new("Configure Providers")
1166                    .icon(IconName::Settings)
1167                    .icon_position(IconPosition::Start)
1168                    .icon_color(Color::Muted)
1169                    .handler(move |window, cx| {
1170                        window.dispatch_action(
1171                            OpenSettingsAt {
1172                                path: "edit_predictions.providers".to_string(),
1173                            }
1174                            .boxed_clone(),
1175                            cx,
1176                        );
1177                    }),
1178            );
1179
1180            menu
1181        })
1182    }
1183
1184    fn build_zeta_upsell_context_menu(
1185        &self,
1186        window: &mut Window,
1187        cx: &mut Context<Self>,
1188    ) -> Entity<ContextMenu> {
1189        ContextMenu::build(window, cx, |mut menu, _window, cx| {
1190            menu = menu
1191                .custom_row(move |_window, cx| {
1192                    let description = indoc! {
1193                        "You get 2,000 accepted suggestions at every keystroke for free, \
1194                        powered by Zeta, our open-source, open-data model"
1195                    };
1196
1197                    v_flex()
1198                        .max_w_64()
1199                        .h(rems_from_px(148.))
1200                        .child(render_zeta_tab_animation(cx))
1201                        .child(Label::new("Edit Prediction"))
1202                        .child(
1203                            Label::new(description)
1204                                .color(Color::Muted)
1205                                .size(LabelSize::Small),
1206                        )
1207                        .into_any_element()
1208                })
1209                .separator()
1210                .entry("Sign In & Start Using", None, |window, cx| {
1211                    let client = Client::global(cx);
1212                    window
1213                        .spawn(cx, async move |cx| {
1214                            client
1215                                .sign_in_with_optional_connect(true, &cx)
1216                                .await
1217                                .log_err();
1218                        })
1219                        .detach();
1220                })
1221                .link(
1222                    "Learn More",
1223                    OpenBrowser {
1224                        url: zed_urls::edit_prediction_docs(cx),
1225                    }
1226                    .boxed_clone(),
1227                );
1228
1229            menu
1230        })
1231    }
1232
1233    pub fn update_enabled(&mut self, editor: Entity<Editor>, cx: &mut Context<Self>) {
1234        let editor = editor.read(cx);
1235        let snapshot = editor.buffer().read(cx).snapshot(cx);
1236        let suggestion_anchor = editor.selections.newest_anchor().start;
1237        let language = snapshot.language_at(suggestion_anchor);
1238        let file = snapshot.file_at(suggestion_anchor).cloned();
1239        self.editor_enabled = {
1240            let file = file.as_ref();
1241            Some(
1242                file.map(|file| {
1243                    all_language_settings(Some(file), cx)
1244                        .edit_predictions_enabled_for_file(file, cx)
1245                })
1246                .unwrap_or(true),
1247            )
1248        };
1249        self.editor_show_predictions = editor.edit_predictions_enabled();
1250        self.edit_prediction_provider = editor.edit_prediction_provider();
1251        self.language = language.cloned();
1252        self.file = file;
1253        self.editor_focus_handle = Some(editor.focus_handle(cx));
1254
1255        cx.notify();
1256    }
1257}
1258
1259impl StatusItemView for EditPredictionButton {
1260    fn set_active_pane_item(
1261        &mut self,
1262        item: Option<&dyn ItemHandle>,
1263        _: &mut Window,
1264        cx: &mut Context<Self>,
1265    ) {
1266        if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
1267            self.editor_subscription = Some((
1268                cx.observe(&editor, Self::update_enabled),
1269                editor.entity_id().as_u64() as usize,
1270            ));
1271            self.update_enabled(editor, cx);
1272        } else {
1273            self.language = None;
1274            self.editor_subscription = None;
1275            self.editor_enabled = None;
1276        }
1277        cx.notify();
1278    }
1279}
1280
1281impl SupermavenButtonStatus {
1282    fn to_icon(&self) -> IconName {
1283        match self {
1284            SupermavenButtonStatus::Ready => IconName::Supermaven,
1285            SupermavenButtonStatus::Errored(_) => IconName::SupermavenError,
1286            SupermavenButtonStatus::NeedsActivation(_) => IconName::SupermavenInit,
1287            SupermavenButtonStatus::Initializing => IconName::SupermavenInit,
1288        }
1289    }
1290
1291    fn to_tooltip(&self) -> String {
1292        match self {
1293            SupermavenButtonStatus::Ready => "Supermaven is ready".to_string(),
1294            SupermavenButtonStatus::Errored(error) => format!("Supermaven error: {}", error),
1295            SupermavenButtonStatus::NeedsActivation(_) => "Supermaven needs activation".to_string(),
1296            SupermavenButtonStatus::Initializing => "Supermaven initializing".to_string(),
1297        }
1298    }
1299
1300    fn has_menu(&self) -> bool {
1301        match self {
1302            SupermavenButtonStatus::Ready | SupermavenButtonStatus::NeedsActivation(_) => true,
1303            SupermavenButtonStatus::Errored(_) | SupermavenButtonStatus::Initializing => false,
1304        }
1305    }
1306}
1307
1308async fn open_disabled_globs_setting_in_editor(
1309    workspace: WeakEntity<Workspace>,
1310    cx: &mut AsyncWindowContext,
1311) -> Result<()> {
1312    let settings_editor = workspace
1313        .update_in(cx, |_, window, cx| {
1314            create_and_open_local_file(paths::settings_file(), window, cx, || {
1315                settings::initial_user_settings_content().as_ref().into()
1316            })
1317        })?
1318        .await?
1319        .downcast::<Editor>()
1320        .unwrap();
1321
1322    settings_editor
1323        .downgrade()
1324        .update_in(cx, |item, window, cx| {
1325            let text = item.buffer().read(cx).snapshot(cx).text();
1326
1327            let settings = cx.global::<SettingsStore>();
1328
1329            // Ensure that we always have "edit_predictions { "disabled_globs": [] }"
1330            let edits = settings.edits_for_update(&text, |file| {
1331                file.project
1332                    .all_languages
1333                    .edit_predictions
1334                    .get_or_insert_with(Default::default)
1335                    .disabled_globs
1336                    .get_or_insert_with(Vec::new);
1337            });
1338
1339            if !edits.is_empty() {
1340                item.edit(
1341                    edits
1342                        .into_iter()
1343                        .map(|(r, s)| (MultiBufferOffset(r.start)..MultiBufferOffset(r.end), s)),
1344                    cx,
1345                );
1346            }
1347
1348            let text = item.buffer().read(cx).snapshot(cx).text();
1349
1350            static DISABLED_GLOBS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
1351                Regex::new(r#""disabled_globs":\s*\[\s*(?P<content>(?:.|\n)*?)\s*\]"#).unwrap()
1352            });
1353            // Only capture [...]
1354            let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| {
1355                captures
1356                    .name("content")
1357                    .map(|inner_match| inner_match.start()..inner_match.end())
1358            });
1359            if let Some(range) = range {
1360                let range = MultiBufferOffset(range.start)..MultiBufferOffset(range.end);
1361                item.change_selections(
1362                    SelectionEffects::scroll(Autoscroll::newest()),
1363                    window,
1364                    cx,
1365                    |selections| {
1366                        selections.select_ranges(vec![range]);
1367                    },
1368                );
1369            }
1370        })?;
1371
1372    anyhow::Ok(())
1373}
1374
1375fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
1376    update_settings_file(fs, cx, move |settings, _| {
1377        settings
1378            .project
1379            .all_languages
1380            .features
1381            .get_or_insert_default()
1382            .edit_prediction_provider = Some(provider);
1383    });
1384}
1385
1386fn toggle_show_edit_predictions_for_language(
1387    language: Arc<Language>,
1388    fs: Arc<dyn Fs>,
1389    cx: &mut App,
1390) {
1391    let show_edit_predictions =
1392        all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
1393    update_settings_file(fs, cx, move |settings, _| {
1394        settings
1395            .project
1396            .all_languages
1397            .languages
1398            .0
1399            .entry(language.name().0)
1400            .or_default()
1401            .show_edit_predictions = Some(!show_edit_predictions);
1402    });
1403}
1404
1405fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
1406    update_settings_file(fs, cx, move |settings, _| {
1407        settings
1408            .project
1409            .all_languages
1410            .features
1411            .get_or_insert(Default::default())
1412            .edit_prediction_provider = Some(EditPredictionProvider::None);
1413    });
1414}
1415
1416fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &mut App) {
1417    let settings = AllLanguageSettings::get_global(cx);
1418    let current_mode = settings.edit_predictions_mode();
1419
1420    if current_mode != mode {
1421        update_settings_file(fs, cx, move |settings, _cx| {
1422            if let Some(edit_predictions) = settings.project.all_languages.edit_predictions.as_mut()
1423            {
1424                edit_predictions.mode = Some(mode);
1425            } else {
1426                settings.project.all_languages.edit_predictions =
1427                    Some(settings::EditPredictionSettingsContent {
1428                        mode: Some(mode),
1429                        ..Default::default()
1430                    });
1431            }
1432        });
1433    }
1434}
1435
1436fn render_zeta_tab_animation(cx: &App) -> impl IntoElement {
1437    let tab = |n: u64, inverted: bool| {
1438        let text_color = cx.theme().colors().text;
1439
1440        h_flex().child(
1441            h_flex()
1442                .text_size(TextSize::XSmall.rems(cx))
1443                .text_color(text_color)
1444                .child("tab")
1445                .with_animation(
1446                    ElementId::Integer(n),
1447                    Animation::new(Duration::from_secs(3)).repeat(),
1448                    move |tab, delta| {
1449                        let n_f32 = n as f32;
1450
1451                        let offset = if inverted {
1452                            0.2 * (4.0 - n_f32)
1453                        } else {
1454                            0.2 * n_f32
1455                        };
1456
1457                        let phase = (delta - offset + 1.0) % 1.0;
1458                        let pulse = if phase < 0.6 {
1459                            let t = phase / 0.6;
1460                            1.0 - (0.5 - t).abs() * 2.0
1461                        } else {
1462                            0.0
1463                        };
1464
1465                        let eased = ease_in_out(pulse);
1466                        let opacity = 0.1 + 0.5 * eased;
1467
1468                        tab.text_color(text_color.opacity(opacity))
1469                    },
1470                ),
1471        )
1472    };
1473
1474    let tab_sequence = |inverted: bool| {
1475        h_flex()
1476            .gap_1()
1477            .child(tab(0, inverted))
1478            .child(tab(1, inverted))
1479            .child(tab(2, inverted))
1480            .child(tab(3, inverted))
1481            .child(tab(4, inverted))
1482    };
1483
1484    h_flex()
1485        .my_1p5()
1486        .p_4()
1487        .justify_center()
1488        .gap_2()
1489        .rounded_xs()
1490        .border_1()
1491        .border_dashed()
1492        .border_color(cx.theme().colors().border)
1493        .bg(gpui::pattern_slash(
1494            cx.theme().colors().border.opacity(0.5),
1495            1.,
1496            8.,
1497        ))
1498        .child(tab_sequence(true))
1499        .child(Icon::new(IconName::ZedPredict))
1500        .child(tab_sequence(false))
1501}
1502
1503fn copilot_settings_url(enterprise_uri: Option<&str>) -> String {
1504    match enterprise_uri {
1505        Some(uri) => {
1506            format!("{}{}", uri.trim_end_matches('/'), COPILOT_SETTINGS_PATH)
1507        }
1508        None => COPILOT_SETTINGS_URL.to_string(),
1509    }
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514    use super::*;
1515    use gpui::TestAppContext;
1516
1517    #[gpui::test]
1518    async fn test_copilot_settings_url_with_enterprise_uri(cx: &mut TestAppContext) {
1519        cx.update(|cx| {
1520            let settings_store = SettingsStore::test(cx);
1521            cx.set_global(settings_store);
1522        });
1523
1524        cx.update_global(|settings_store: &mut SettingsStore, cx| {
1525            settings_store
1526                .set_user_settings(
1527                    r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com"}}}"#,
1528                    cx,
1529                )
1530                .unwrap();
1531        });
1532
1533        let url = cx.update(|cx| {
1534            let all_language_settings = all_language_settings(None, cx);
1535            copilot_settings_url(
1536                all_language_settings
1537                    .edit_predictions
1538                    .copilot
1539                    .enterprise_uri
1540                    .as_deref(),
1541            )
1542        });
1543
1544        assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
1545    }
1546
1547    #[gpui::test]
1548    async fn test_copilot_settings_url_with_enterprise_uri_trailing_slash(cx: &mut TestAppContext) {
1549        cx.update(|cx| {
1550            let settings_store = SettingsStore::test(cx);
1551            cx.set_global(settings_store);
1552        });
1553
1554        cx.update_global(|settings_store: &mut SettingsStore, cx| {
1555            settings_store
1556                .set_user_settings(
1557                    r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com/"}}}"#,
1558                    cx,
1559                )
1560                .unwrap();
1561        });
1562
1563        let url = cx.update(|cx| {
1564            let all_language_settings = all_language_settings(None, cx);
1565            copilot_settings_url(
1566                all_language_settings
1567                    .edit_predictions
1568                    .copilot
1569                    .enterprise_uri
1570                    .as_deref(),
1571            )
1572        });
1573
1574        assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
1575    }
1576
1577    #[gpui::test]
1578    async fn test_copilot_settings_url_without_enterprise_uri(cx: &mut TestAppContext) {
1579        cx.update(|cx| {
1580            let settings_store = SettingsStore::test(cx);
1581            cx.set_global(settings_store);
1582        });
1583
1584        let url = cx.update(|cx| {
1585            let all_language_settings = all_language_settings(None, cx);
1586            copilot_settings_url(
1587                all_language_settings
1588                    .edit_predictions
1589                    .copilot
1590                    .enterprise_uri
1591                    .as_deref(),
1592            )
1593        });
1594
1595        assert_eq!(url, "https://github.com/settings/copilot");
1596    }
1597}