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