edit_prediction_button.rs

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