edit_prediction_button.rs

   1use anyhow::Result;
   2use client::{Client, UserStore, zed_urls};
   3use cloud_llm_client::UsageLimit;
   4use codestral::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) = window.root::<Workspace>().flatten() {
 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 = CodestralEditPredictionDelegate::has_api_key(cx);
 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        client: Arc<Client>,
 604        project: Entity<Project>,
 605        cx: &mut Context<Self>,
 606    ) -> Self {
 607        let copilot = EditPredictionStore::try_global(cx).and_then(|store| {
 608            store.update(cx, |this, cx| this.start_copilot_for_project(&project, cx))
 609        });
 610        if let Some(copilot) = copilot {
 611            cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
 612        }
 613
 614        cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
 615            .detach();
 616
 617        cx.observe_global::<EditPredictionStore>(move |_, cx| cx.notify())
 618            .detach();
 619
 620        edit_prediction::ollama::ensure_authenticated(cx);
 621        let sweep_api_token_task = edit_prediction::sweep_ai::load_sweep_api_token(cx);
 622        let mercury_api_token_task = edit_prediction::mercury::load_mercury_api_token(cx);
 623
 624        cx.spawn(async move |this, cx| {
 625            _ = futures::join!(sweep_api_token_task, mercury_api_token_task);
 626            this.update(cx, |_, cx| {
 627                cx.notify();
 628            })
 629            .ok();
 630        })
 631        .detach();
 632
 633        CodestralEditPredictionDelegate::ensure_api_key_loaded(client.http_client(), cx);
 634
 635        Self {
 636            editor_subscription: None,
 637            editor_enabled: None,
 638            editor_show_predictions: true,
 639            editor_focus_handle: None,
 640            language: None,
 641            file: None,
 642            edit_prediction_provider: None,
 643            user_store,
 644            popover_menu_handle,
 645            project: project.downgrade(),
 646            fs,
 647        }
 648    }
 649
 650    fn add_provider_switching_section(
 651        &self,
 652        mut menu: ContextMenu,
 653        current_provider: EditPredictionProvider,
 654        cx: &mut App,
 655    ) -> ContextMenu {
 656        let available_providers = get_available_providers(cx);
 657
 658        let providers: Vec<_> = available_providers
 659            .into_iter()
 660            .filter(|p| *p != EditPredictionProvider::None)
 661            .collect();
 662
 663        if !providers.is_empty() {
 664            menu = menu.separator().header("Providers");
 665
 666            for provider in providers {
 667                let Some(name) = provider.display_name() else {
 668                    continue;
 669                };
 670                let is_current = provider == current_provider;
 671                let fs = self.fs.clone();
 672
 673                menu = menu.item(
 674                    ContextMenuEntry::new(name)
 675                        .toggleable(IconPosition::Start, is_current)
 676                        .handler(move |_, cx| {
 677                            set_completion_provider(fs.clone(), cx, provider);
 678                        }),
 679                )
 680            }
 681        }
 682
 683        menu
 684    }
 685
 686    pub fn build_copilot_start_menu(
 687        &mut self,
 688        window: &mut Window,
 689        cx: &mut Context<Self>,
 690    ) -> Entity<ContextMenu> {
 691        let fs = self.fs.clone();
 692        let project = self.project.clone();
 693        ContextMenu::build(window, cx, |menu, _, _| {
 694            menu.entry("Sign In to Copilot", None, move |window, cx| {
 695                telemetry::event!(
 696                    "Edit Prediction Menu Action",
 697                    action = "sign_in",
 698                    provider = "copilot",
 699                );
 700                if let Some(copilot) = EditPredictionStore::try_global(cx).and_then(|store| {
 701                    store.update(cx, |this, cx| {
 702                        this.start_copilot_for_project(&project.upgrade()?, cx)
 703                    })
 704                }) {
 705                    copilot_ui::initiate_sign_in(copilot, window, cx);
 706                }
 707            })
 708            .entry("Disable Copilot", None, {
 709                let fs = fs.clone();
 710                move |_window, cx| {
 711                    telemetry::event!(
 712                        "Edit Prediction Menu Action",
 713                        action = "disable_provider",
 714                        provider = "copilot",
 715                    );
 716                    hide_copilot(fs.clone(), cx)
 717                }
 718            })
 719            .separator()
 720            .entry("Use Zed AI", None, {
 721                let fs = fs.clone();
 722                move |_window, cx| {
 723                    set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
 724                }
 725            })
 726        })
 727    }
 728
 729    pub fn build_language_settings_menu(
 730        &self,
 731        mut menu: ContextMenu,
 732        window: &Window,
 733        cx: &mut App,
 734    ) -> ContextMenu {
 735        let fs = self.fs.clone();
 736        let line_height = window.line_height();
 737
 738        menu = menu.header("Show Edit Predictions For");
 739
 740        let language_state = self.language.as_ref().map(|language| {
 741            (
 742                language.clone(),
 743                language_settings::language_settings(Some(language.name()), None, cx)
 744                    .show_edit_predictions,
 745            )
 746        });
 747
 748        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
 749            let entry = ContextMenuEntry::new("This Buffer")
 750                .toggleable(IconPosition::Start, self.editor_show_predictions)
 751                .action(Box::new(editor::actions::ToggleEditPrediction))
 752                .handler(move |window, cx| {
 753                    editor_focus_handle.dispatch_action(
 754                        &editor::actions::ToggleEditPrediction,
 755                        window,
 756                        cx,
 757                    );
 758                });
 759
 760            match language_state.clone() {
 761                Some((language, false)) => {
 762                    menu = menu.item(
 763                        entry
 764                            .disabled(true)
 765                            .documentation_aside(DocumentationSide::Left, move |_cx| {
 766                                Label::new(format!("Edit predictions cannot be toggled for this buffer because they are disabled for {}", language.name()))
 767                                    .into_any_element()
 768                            })
 769                    );
 770                }
 771                Some(_) | None => menu = menu.item(entry),
 772            }
 773        }
 774
 775        if let Some((language, language_enabled)) = language_state {
 776            let fs = fs.clone();
 777            let language_name = language.name();
 778
 779            menu = menu.toggleable_entry(
 780                language_name.clone(),
 781                language_enabled,
 782                IconPosition::Start,
 783                None,
 784                move |_, cx| {
 785                    telemetry::event!(
 786                        "Edit Prediction Setting Changed",
 787                        setting = "language",
 788                        language = language_name.to_string(),
 789                        enabled = !language_enabled,
 790                    );
 791                    toggle_show_edit_predictions_for_language(language.clone(), fs.clone(), cx)
 792                },
 793            );
 794        }
 795
 796        let settings = AllLanguageSettings::get_global(cx);
 797
 798        let globally_enabled = settings.show_edit_predictions(None, cx);
 799        let entry = ContextMenuEntry::new("All Files")
 800            .toggleable(IconPosition::Start, globally_enabled)
 801            .action(workspace::ToggleEditPrediction.boxed_clone())
 802            .handler(|window, cx| {
 803                window.dispatch_action(workspace::ToggleEditPrediction.boxed_clone(), cx)
 804            });
 805        menu = menu.item(entry);
 806
 807        let provider = settings.edit_predictions.provider;
 808        let current_mode = settings.edit_predictions_mode();
 809        let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
 810        let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
 811
 812        menu = menu
 813                .separator()
 814                .header("Display Modes")
 815                .item(
 816                    ContextMenuEntry::new("Eager")
 817                        .toggleable(IconPosition::Start, eager_mode)
 818                        .documentation_aside(DocumentationSide::Left, move |_| {
 819                            Label::new("Display predictions inline when there are no language server completions available.").into_any_element()
 820                        })
 821                        .handler({
 822                            let fs = fs.clone();
 823                            move |_, cx| {
 824                                telemetry::event!(
 825                                    "Edit Prediction Setting Changed",
 826                                    setting = "mode",
 827                                    value = "eager",
 828                                );
 829                                toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Eager, cx)
 830                            }
 831                        }),
 832                )
 833                .item(
 834                    ContextMenuEntry::new("Subtle")
 835                        .toggleable(IconPosition::Start, subtle_mode)
 836                        .documentation_aside(DocumentationSide::Left, move |_| {
 837                            Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element()
 838                        })
 839                        .handler({
 840                            let fs = fs.clone();
 841                            move |_, cx| {
 842                                telemetry::event!(
 843                                    "Edit Prediction Setting Changed",
 844                                    setting = "mode",
 845                                    value = "subtle",
 846                                );
 847                                toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Subtle, cx)
 848                            }
 849                        }),
 850                );
 851
 852        menu = menu.separator().header("Privacy");
 853
 854        if matches!(
 855            provider,
 856            EditPredictionProvider::Zed
 857                | EditPredictionProvider::Experimental(
 858                    EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
 859                )
 860        ) {
 861            if let Some(provider) = &self.edit_prediction_provider {
 862                let data_collection = provider.data_collection_state(cx);
 863
 864                if data_collection.is_supported() {
 865                    let provider = provider.clone();
 866                    let enabled = data_collection.is_enabled();
 867                    let is_open_source = data_collection.is_project_open_source();
 868                    let is_collecting = data_collection.is_enabled();
 869                    let (icon_name, icon_color) = if is_open_source && is_collecting {
 870                        (IconName::Check, Color::Success)
 871                    } else {
 872                        (IconName::Check, Color::Accent)
 873                    };
 874
 875                    menu = menu.item(
 876                        ContextMenuEntry::new("Training Data Collection")
 877                            .toggleable(IconPosition::Start, data_collection.is_enabled())
 878                            .icon(icon_name)
 879                            .icon_color(icon_color)
 880                            .disabled(cx.is_staff())
 881                            .documentation_aside(DocumentationSide::Left, move |cx| {
 882                                let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
 883                                    (true, true) => (
 884                                        "Project identified as open source, and you're sharing data.",
 885                                        Color::Default,
 886                                        IconName::Check,
 887                                        Color::Success,
 888                                    ),
 889                                    (true, false) => (
 890                                        "Project identified as open source, but you're not sharing data.",
 891                                        Color::Muted,
 892                                        IconName::Close,
 893                                        Color::Muted,
 894                                    ),
 895                                    (false, true) => (
 896                                        "Project not identified as open source. No data captured.",
 897                                        Color::Muted,
 898                                        IconName::Close,
 899                                        Color::Muted,
 900                                    ),
 901                                    (false, false) => (
 902                                        "Project not identified as open source, and setting turned off.",
 903                                        Color::Muted,
 904                                        IconName::Close,
 905                                        Color::Muted,
 906                                    ),
 907                                };
 908                                v_flex()
 909                                    .gap_2()
 910                                    .child(
 911                                        Label::new(indoc!{
 912                                            "Help us improve our open dataset model by sharing data from open source repositories. \
 913                                            Zed must detect a license file in your repo for this setting to take effect. \
 914                                            Files with sensitive data and secrets are excluded by default."
 915                                        })
 916                                    )
 917                                    .child(
 918                                        h_flex()
 919                                            .items_start()
 920                                            .pt_2()
 921                                            .pr_1()
 922                                            .flex_1()
 923                                            .gap_1p5()
 924                                            .border_t_1()
 925                                            .border_color(cx.theme().colors().border_variant)
 926                                            .child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)))
 927                                            .child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx)))
 928                                    )
 929                                    .into_any_element()
 930                            })
 931                            .handler(move |_, cx| {
 932                                provider.toggle_data_collection(cx);
 933
 934                                if !enabled {
 935                                    telemetry::event!(
 936                                        "Data Collection Enabled",
 937                                        source = "Edit Prediction Status Menu"
 938                                    );
 939                                } else {
 940                                    telemetry::event!(
 941                                        "Data Collection Disabled",
 942                                        source = "Edit Prediction Status Menu"
 943                                    );
 944                                }
 945                            })
 946                    );
 947
 948                    if is_collecting && !is_open_source {
 949                        menu = menu.item(
 950                            ContextMenuEntry::new("No data captured.")
 951                                .disabled(true)
 952                                .icon(IconName::Close)
 953                                .icon_color(Color::Error)
 954                                .icon_size(IconSize::Small),
 955                        );
 956                    }
 957                }
 958            }
 959        }
 960
 961        menu = menu.item(
 962            ContextMenuEntry::new("Configure Excluded Files")
 963                .icon(IconName::LockOutlined)
 964                .icon_color(Color::Muted)
 965                .documentation_aside(DocumentationSide::Left, |_| {
 966                    Label::new(indoc!{"
 967                        Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
 968                })
 969                .handler(move |window, cx| {
 970                    telemetry::event!(
 971                        "Edit Prediction Menu Action",
 972                        action = "configure_excluded_files",
 973                    );
 974                    if let Some(workspace) = window.root().flatten() {
 975                        let workspace = workspace.downgrade();
 976                        window
 977                            .spawn(cx, async |cx| {
 978                                open_disabled_globs_setting_in_editor(
 979                                    workspace,
 980                                    cx,
 981                                ).await
 982                            })
 983                            .detach_and_log_err(cx);
 984                    }
 985                }),
 986        ).item(
 987            ContextMenuEntry::new("View Docs")
 988                .icon(IconName::FileGeneric)
 989                .icon_color(Color::Muted)
 990                .handler(move |_, cx| {
 991                    telemetry::event!(
 992                        "Edit Prediction Menu Action",
 993                        action = "view_docs",
 994                    );
 995                    cx.open_url(PRIVACY_DOCS);
 996                })
 997        );
 998
 999        if !self.editor_enabled.unwrap_or(true) {
1000            let icons = self
1001                .edit_prediction_provider
1002                .as_ref()
1003                .map(|p| p.icons(cx))
1004                .unwrap_or_else(|| {
1005                    edit_prediction_types::EditPredictionIconSet::new(IconName::ZedPredict)
1006                });
1007            menu = menu.item(
1008                ContextMenuEntry::new("This file is excluded.")
1009                    .disabled(true)
1010                    .icon(icons.disabled)
1011                    .icon_size(IconSize::Small),
1012            );
1013        }
1014
1015        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
1016            menu = menu
1017                .separator()
1018                .header("Actions")
1019                .entry(
1020                    "Predict Edit at Cursor",
1021                    Some(Box::new(ShowEditPrediction)),
1022                    {
1023                        let editor_focus_handle = editor_focus_handle.clone();
1024                        move |window, cx| {
1025                            telemetry::event!(
1026                                "Edit Prediction Menu Action",
1027                                action = "predict_at_cursor",
1028                            );
1029                            editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
1030                        }
1031                    },
1032                )
1033                .context(editor_focus_handle)
1034                .when(
1035                    cx.has_flag::<PredictEditsRatePredictionsFeatureFlag>(),
1036                    |this| {
1037                        this.action("Capture Prediction Example", CaptureExample.boxed_clone())
1038                            .action("Rate Predictions", RatePredictions.boxed_clone())
1039                    },
1040                );
1041        }
1042
1043        menu
1044    }
1045
1046    fn build_copilot_context_menu(
1047        &self,
1048        window: &mut Window,
1049        cx: &mut Context<Self>,
1050    ) -> Entity<ContextMenu> {
1051        let all_language_settings = all_language_settings(None, cx);
1052        let next_edit_suggestions = all_language_settings
1053            .edit_predictions
1054            .copilot
1055            .enable_next_edit_suggestions
1056            .unwrap_or(true);
1057        let copilot_config = copilot_chat::CopilotChatConfiguration {
1058            enterprise_uri: all_language_settings
1059                .edit_predictions
1060                .copilot
1061                .enterprise_uri
1062                .clone(),
1063        };
1064        let settings_url = copilot_settings_url(copilot_config.enterprise_uri.as_deref());
1065
1066        ContextMenu::build(window, cx, |menu, window, cx| {
1067            let menu = self.build_language_settings_menu(menu, window, cx);
1068            let menu =
1069                self.add_provider_switching_section(menu, EditPredictionProvider::Copilot, cx);
1070
1071            menu.separator()
1072                .item(
1073                    ContextMenuEntry::new("Copilot: Next Edit Suggestions")
1074                        .toggleable(IconPosition::Start, next_edit_suggestions)
1075                        .handler({
1076                            let fs = self.fs.clone();
1077                            move |_, cx| {
1078                                update_settings_file(fs.clone(), cx, move |settings, _| {
1079                                    settings
1080                                        .project
1081                                        .all_languages
1082                                        .edit_predictions
1083                                        .get_or_insert_default()
1084                                        .copilot
1085                                        .get_or_insert_default()
1086                                        .enable_next_edit_suggestions =
1087                                        Some(!next_edit_suggestions);
1088                                });
1089                            }
1090                        }),
1091                )
1092                .separator()
1093                .link(
1094                    "Go to Copilot Settings",
1095                    OpenBrowser { url: settings_url }.boxed_clone(),
1096                )
1097                .action("Sign Out", copilot::SignOut.boxed_clone())
1098        })
1099    }
1100
1101    fn build_supermaven_context_menu(
1102        &self,
1103        window: &mut Window,
1104        cx: &mut Context<Self>,
1105    ) -> Entity<ContextMenu> {
1106        ContextMenu::build(window, cx, |menu, window, cx| {
1107            let menu = self.build_language_settings_menu(menu, window, cx);
1108            let menu =
1109                self.add_provider_switching_section(menu, EditPredictionProvider::Supermaven, cx);
1110
1111            menu.separator()
1112                .action("Sign Out", supermaven::SignOut.boxed_clone())
1113        })
1114    }
1115
1116    fn build_codestral_context_menu(
1117        &self,
1118        window: &mut Window,
1119        cx: &mut Context<Self>,
1120    ) -> Entity<ContextMenu> {
1121        ContextMenu::build(window, cx, |menu, window, cx| {
1122            let menu = self.build_language_settings_menu(menu, window, cx);
1123            let menu =
1124                self.add_provider_switching_section(menu, EditPredictionProvider::Codestral, cx);
1125
1126            menu
1127        })
1128    }
1129
1130    fn build_edit_prediction_context_menu(
1131        &self,
1132        provider: EditPredictionProvider,
1133        window: &mut Window,
1134        cx: &mut Context<Self>,
1135    ) -> Entity<ContextMenu> {
1136        ContextMenu::build(window, cx, |mut menu, window, cx| {
1137            let user = self.user_store.read(cx).current_user();
1138
1139            let needs_sign_in = user.is_none()
1140                && matches!(
1141                    provider,
1142                    EditPredictionProvider::None | EditPredictionProvider::Zed
1143                );
1144
1145            if needs_sign_in {
1146                menu = menu
1147                    .custom_row(move |_window, cx| {
1148                        let description = indoc! {
1149                            "You get 2,000 accepted suggestions at every keystroke for free, \
1150                            powered by Zeta, our open-source, open-data model"
1151                        };
1152
1153                        v_flex()
1154                            .max_w_64()
1155                            .h(rems_from_px(148.))
1156                            .child(render_zeta_tab_animation(cx))
1157                            .child(Label::new("Edit Prediction"))
1158                            .child(
1159                                Label::new(description)
1160                                    .color(Color::Muted)
1161                                    .size(LabelSize::Small),
1162                            )
1163                            .into_any_element()
1164                    })
1165                    .separator()
1166                    .entry("Sign In & Start Using", None, |window, cx| {
1167                        telemetry::event!(
1168                            "Edit Prediction Menu Action",
1169                            action = "sign_in",
1170                            provider = "zed",
1171                        );
1172                        let client = Client::global(cx);
1173                        window
1174                            .spawn(cx, async move |cx| {
1175                                client
1176                                    .sign_in_with_optional_connect(true, &cx)
1177                                    .await
1178                                    .log_err();
1179                            })
1180                            .detach();
1181                    })
1182                    .link_with_handler(
1183                        "Learn More",
1184                        OpenBrowser {
1185                            url: zed_urls::edit_prediction_docs(cx),
1186                        }
1187                        .boxed_clone(),
1188                        |_window, _cx| {
1189                            telemetry::event!(
1190                                "Edit Prediction Menu Action",
1191                                action = "view_docs",
1192                                source = "upsell",
1193                            );
1194                        },
1195                    )
1196                    .separator();
1197            } else if let Some(usage) = self
1198                .edit_prediction_provider
1199                .as_ref()
1200                .and_then(|provider| provider.usage(cx))
1201            {
1202                menu = menu.header("Usage");
1203                menu = menu
1204                    .custom_entry(
1205                        move |_window, cx| {
1206                            let used_percentage = match usage.limit {
1207                                UsageLimit::Limited(limit) => {
1208                                    Some((usage.amount as f32 / limit as f32) * 100.)
1209                                }
1210                                UsageLimit::Unlimited => None,
1211                            };
1212
1213                            h_flex()
1214                                .flex_1()
1215                                .gap_1p5()
1216                                .children(
1217                                    used_percentage.map(|percent| {
1218                                        ProgressBar::new("usage", percent, 100., cx)
1219                                    }),
1220                                )
1221                                .child(
1222                                    Label::new(match usage.limit {
1223                                        UsageLimit::Limited(limit) => {
1224                                            format!("{} / {limit}", usage.amount)
1225                                        }
1226                                        UsageLimit::Unlimited => format!("{} / ∞", usage.amount),
1227                                    })
1228                                    .size(LabelSize::Small)
1229                                    .color(Color::Muted),
1230                                )
1231                                .into_any_element()
1232                        },
1233                        move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1234                    )
1235                    .when(usage.over_limit(), |menu| -> ContextMenu {
1236                        menu.entry("Subscribe to increase your limit", None, |_window, cx| {
1237                            telemetry::event!(
1238                                "Edit Prediction Menu Action",
1239                                action = "upsell_clicked",
1240                                reason = "usage_limit",
1241                            );
1242                            cx.open_url(&zed_urls::account_url(cx))
1243                        })
1244                    })
1245                    .separator();
1246            } else if self.user_store.read(cx).account_too_young() {
1247                menu = menu
1248                    .custom_entry(
1249                        |_window, _cx| {
1250                            Label::new("Your GitHub account is less than 30 days old.")
1251                                .size(LabelSize::Small)
1252                                .color(Color::Warning)
1253                                .into_any_element()
1254                        },
1255                        |_window, cx| cx.open_url(&zed_urls::account_url(cx)),
1256                    )
1257                    .entry("Upgrade to Zed Pro or contact us.", None, |_window, cx| {
1258                        telemetry::event!(
1259                            "Edit Prediction Menu Action",
1260                            action = "upsell_clicked",
1261                            reason = "account_age",
1262                        );
1263                        cx.open_url(&zed_urls::account_url(cx))
1264                    })
1265                    .separator();
1266            } else if self.user_store.read(cx).has_overdue_invoices() {
1267                menu = menu
1268                    .custom_entry(
1269                        |_window, _cx| {
1270                            Label::new("You have an outstanding invoice")
1271                                .size(LabelSize::Small)
1272                                .color(Color::Warning)
1273                                .into_any_element()
1274                        },
1275                        |_window, cx| {
1276                            cx.open_url(&zed_urls::account_url(cx))
1277                        },
1278                    )
1279                    .entry(
1280                        "Check your payment status or contact us at billing-support@zed.dev to continue using this feature.",
1281                        None,
1282                        |_window, cx| {
1283                            cx.open_url(&zed_urls::account_url(cx))
1284                        },
1285                    )
1286                    .separator();
1287            }
1288
1289            if !needs_sign_in {
1290                menu = self.build_language_settings_menu(menu, window, cx);
1291            }
1292            menu = self.add_provider_switching_section(menu, provider, cx);
1293            menu = menu.separator().item(
1294                ContextMenuEntry::new("Configure Providers")
1295                    .icon(IconName::Settings)
1296                    .icon_position(IconPosition::Start)
1297                    .icon_color(Color::Muted)
1298                    .handler(move |window, cx| {
1299                        telemetry::event!(
1300                            "Edit Prediction Menu Action",
1301                            action = "configure_providers",
1302                        );
1303                        window.dispatch_action(
1304                            OpenSettingsAt {
1305                                path: "edit_predictions.providers".to_string(),
1306                            }
1307                            .boxed_clone(),
1308                            cx,
1309                        );
1310                    }),
1311            );
1312
1313            menu
1314        })
1315    }
1316
1317    pub fn update_enabled(&mut self, editor: Entity<Editor>, cx: &mut Context<Self>) {
1318        let editor = editor.read(cx);
1319        let snapshot = editor.buffer().read(cx).snapshot(cx);
1320        let suggestion_anchor = editor.selections.newest_anchor().start;
1321        let language = snapshot.language_at(suggestion_anchor);
1322        let file = snapshot.file_at(suggestion_anchor).cloned();
1323        self.editor_enabled = {
1324            let file = file.as_ref();
1325            Some(
1326                file.map(|file| {
1327                    all_language_settings(Some(file), cx)
1328                        .edit_predictions_enabled_for_file(file, cx)
1329                })
1330                .unwrap_or(true),
1331            )
1332        };
1333        self.editor_show_predictions = editor.edit_predictions_enabled();
1334        self.edit_prediction_provider = editor.edit_prediction_provider();
1335        self.language = language.cloned();
1336        self.file = file;
1337        self.editor_focus_handle = Some(editor.focus_handle(cx));
1338
1339        cx.notify();
1340    }
1341}
1342
1343impl StatusItemView for EditPredictionButton {
1344    fn set_active_pane_item(
1345        &mut self,
1346        item: Option<&dyn ItemHandle>,
1347        _: &mut Window,
1348        cx: &mut Context<Self>,
1349    ) {
1350        if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
1351            self.editor_subscription = Some((
1352                cx.observe(&editor, Self::update_enabled),
1353                editor.entity_id().as_u64() as usize,
1354            ));
1355            self.update_enabled(editor, cx);
1356        } else {
1357            self.language = None;
1358            self.editor_subscription = None;
1359            self.editor_enabled = None;
1360        }
1361        cx.notify();
1362    }
1363}
1364
1365impl SupermavenButtonStatus {
1366    fn to_icon(&self) -> IconName {
1367        match self {
1368            SupermavenButtonStatus::Ready => IconName::Supermaven,
1369            SupermavenButtonStatus::Errored(_) => IconName::SupermavenError,
1370            SupermavenButtonStatus::NeedsActivation(_) => IconName::SupermavenInit,
1371            SupermavenButtonStatus::Initializing => IconName::SupermavenInit,
1372        }
1373    }
1374
1375    fn to_tooltip(&self) -> String {
1376        match self {
1377            SupermavenButtonStatus::Ready => "Supermaven is ready".to_string(),
1378            SupermavenButtonStatus::Errored(error) => format!("Supermaven error: {}", error),
1379            SupermavenButtonStatus::NeedsActivation(_) => "Supermaven needs activation".to_string(),
1380            SupermavenButtonStatus::Initializing => "Supermaven initializing".to_string(),
1381        }
1382    }
1383
1384    fn has_menu(&self) -> bool {
1385        match self {
1386            SupermavenButtonStatus::Ready | SupermavenButtonStatus::NeedsActivation(_) => true,
1387            SupermavenButtonStatus::Errored(_) | SupermavenButtonStatus::Initializing => false,
1388        }
1389    }
1390}
1391
1392async fn open_disabled_globs_setting_in_editor(
1393    workspace: WeakEntity<Workspace>,
1394    cx: &mut AsyncWindowContext,
1395) -> Result<()> {
1396    let settings_editor = workspace
1397        .update_in(cx, |_, window, cx| {
1398            create_and_open_local_file(paths::settings_file(), window, cx, || {
1399                settings::initial_user_settings_content().as_ref().into()
1400            })
1401        })?
1402        .await?
1403        .downcast::<Editor>()
1404        .unwrap();
1405
1406    settings_editor
1407        .downgrade()
1408        .update_in(cx, |item, window, cx| {
1409            let text = item.buffer().read(cx).snapshot(cx).text();
1410
1411            let settings = cx.global::<SettingsStore>();
1412
1413            // Ensure that we always have "edit_predictions { "disabled_globs": [] }"
1414            let edits = settings.edits_for_update(&text, |file| {
1415                file.project
1416                    .all_languages
1417                    .edit_predictions
1418                    .get_or_insert_with(Default::default)
1419                    .disabled_globs
1420                    .get_or_insert_with(Vec::new);
1421            });
1422
1423            if !edits.is_empty() {
1424                item.edit(
1425                    edits
1426                        .into_iter()
1427                        .map(|(r, s)| (MultiBufferOffset(r.start)..MultiBufferOffset(r.end), s)),
1428                    cx,
1429                );
1430            }
1431
1432            let text = item.buffer().read(cx).snapshot(cx).text();
1433
1434            static DISABLED_GLOBS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
1435                Regex::new(r#""disabled_globs":\s*\[\s*(?P<content>(?:.|\n)*?)\s*\]"#).unwrap()
1436            });
1437            // Only capture [...]
1438            let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| {
1439                captures
1440                    .name("content")
1441                    .map(|inner_match| inner_match.start()..inner_match.end())
1442            });
1443            if let Some(range) = range {
1444                let range = MultiBufferOffset(range.start)..MultiBufferOffset(range.end);
1445                item.change_selections(
1446                    SelectionEffects::scroll(Autoscroll::newest()),
1447                    window,
1448                    cx,
1449                    |selections| {
1450                        selections.select_ranges(vec![range]);
1451                    },
1452                );
1453            }
1454        })?;
1455
1456    anyhow::Ok(())
1457}
1458
1459pub fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
1460    update_settings_file(fs, cx, move |settings, _| {
1461        settings
1462            .project
1463            .all_languages
1464            .edit_predictions
1465            .get_or_insert_default()
1466            .provider = Some(provider);
1467    });
1468}
1469
1470pub fn get_available_providers(cx: &mut App) -> Vec<EditPredictionProvider> {
1471    let mut providers = Vec::new();
1472
1473    providers.push(EditPredictionProvider::Zed);
1474
1475    if cx.has_flag::<Zeta2FeatureFlag>() {
1476        providers.push(EditPredictionProvider::Experimental(
1477            EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
1478        ));
1479    }
1480
1481    if let Some(app_state) = workspace::AppState::global(cx).upgrade()
1482        && copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
1483            .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
1484    {
1485        providers.push(EditPredictionProvider::Copilot);
1486    };
1487
1488    if let Some(supermaven) = Supermaven::global(cx) {
1489        if let Supermaven::Spawned(agent) = supermaven.read(cx) {
1490            if matches!(agent.account_status, AccountStatus::Ready) {
1491                providers.push(EditPredictionProvider::Supermaven);
1492            }
1493        }
1494    }
1495
1496    if CodestralEditPredictionDelegate::has_api_key(cx) {
1497        providers.push(EditPredictionProvider::Codestral);
1498    }
1499
1500    if edit_prediction::ollama::is_available(cx) {
1501        providers.push(EditPredictionProvider::Ollama);
1502    }
1503
1504    if edit_prediction::sweep_ai::sweep_api_token(cx)
1505        .read(cx)
1506        .has_key()
1507    {
1508        providers.push(EditPredictionProvider::Sweep);
1509    }
1510
1511    if edit_prediction::mercury::mercury_api_token(cx)
1512        .read(cx)
1513        .has_key()
1514    {
1515        providers.push(EditPredictionProvider::Mercury);
1516    }
1517
1518    providers
1519}
1520
1521fn toggle_show_edit_predictions_for_language(
1522    language: Arc<Language>,
1523    fs: Arc<dyn Fs>,
1524    cx: &mut App,
1525) {
1526    let show_edit_predictions =
1527        all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
1528    update_settings_file(fs, cx, move |settings, _| {
1529        settings
1530            .project
1531            .all_languages
1532            .languages
1533            .0
1534            .entry(language.name().0.to_string())
1535            .or_default()
1536            .show_edit_predictions = Some(!show_edit_predictions);
1537    });
1538}
1539
1540fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
1541    update_settings_file(fs, cx, move |settings, _| {
1542        settings
1543            .project
1544            .all_languages
1545            .edit_predictions
1546            .get_or_insert(Default::default())
1547            .provider = Some(EditPredictionProvider::None);
1548    });
1549}
1550
1551fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &mut App) {
1552    let settings = AllLanguageSettings::get_global(cx);
1553    let current_mode = settings.edit_predictions_mode();
1554
1555    if current_mode != mode {
1556        update_settings_file(fs, cx, move |settings, _cx| {
1557            if let Some(edit_predictions) = settings.project.all_languages.edit_predictions.as_mut()
1558            {
1559                edit_predictions.mode = Some(mode);
1560            } else {
1561                settings.project.all_languages.edit_predictions =
1562                    Some(settings::EditPredictionSettingsContent {
1563                        mode: Some(mode),
1564                        ..Default::default()
1565                    });
1566            }
1567        });
1568    }
1569}
1570
1571fn render_zeta_tab_animation(cx: &App) -> impl IntoElement {
1572    let tab = |n: u64, inverted: bool| {
1573        let text_color = cx.theme().colors().text;
1574
1575        h_flex().child(
1576            h_flex()
1577                .text_size(TextSize::XSmall.rems(cx))
1578                .text_color(text_color)
1579                .child("tab")
1580                .with_animation(
1581                    ElementId::Integer(n),
1582                    Animation::new(Duration::from_secs(3)).repeat(),
1583                    move |tab, delta| {
1584                        let n_f32 = n as f32;
1585
1586                        let offset = if inverted {
1587                            0.2 * (4.0 - n_f32)
1588                        } else {
1589                            0.2 * n_f32
1590                        };
1591
1592                        let phase = (delta - offset + 1.0) % 1.0;
1593                        let pulse = if phase < 0.6 {
1594                            let t = phase / 0.6;
1595                            1.0 - (0.5 - t).abs() * 2.0
1596                        } else {
1597                            0.0
1598                        };
1599
1600                        let eased = ease_in_out(pulse);
1601                        let opacity = 0.1 + 0.5 * eased;
1602
1603                        tab.text_color(text_color.opacity(opacity))
1604                    },
1605                ),
1606        )
1607    };
1608
1609    let tab_sequence = |inverted: bool| {
1610        h_flex()
1611            .gap_1()
1612            .child(tab(0, inverted))
1613            .child(tab(1, inverted))
1614            .child(tab(2, inverted))
1615            .child(tab(3, inverted))
1616            .child(tab(4, inverted))
1617    };
1618
1619    h_flex()
1620        .my_1p5()
1621        .p_4()
1622        .justify_center()
1623        .gap_2()
1624        .rounded_xs()
1625        .border_1()
1626        .border_dashed()
1627        .border_color(cx.theme().colors().border)
1628        .bg(gpui::pattern_slash(
1629            cx.theme().colors().border.opacity(0.5),
1630            1.,
1631            8.,
1632        ))
1633        .child(tab_sequence(true))
1634        .child(Icon::new(IconName::ZedPredict))
1635        .child(tab_sequence(false))
1636}
1637
1638fn emit_edit_prediction_menu_opened(
1639    provider: &str,
1640    file: &Option<Arc<dyn File>>,
1641    language: &Option<Arc<Language>>,
1642    project: &WeakEntity<Project>,
1643    cx: &App,
1644) {
1645    let language_name = language.as_ref().map(|l| l.name());
1646    let edit_predictions_enabled_for_language =
1647        language_settings::language_settings(language_name, file.as_ref(), cx)
1648            .show_edit_predictions;
1649    let file_extension = file
1650        .as_ref()
1651        .and_then(|f| {
1652            std::path::Path::new(f.file_name(cx))
1653                .extension()
1654                .and_then(|e| e.to_str())
1655        })
1656        .map(|s| s.to_string());
1657    let is_via_ssh = project
1658        .upgrade()
1659        .map(|p| p.read(cx).is_via_remote_server())
1660        .unwrap_or(false);
1661    telemetry::event!(
1662        "Toolbar Menu Opened",
1663        name = "Edit Predictions",
1664        provider,
1665        file_extension,
1666        edit_predictions_enabled_for_language,
1667        is_via_ssh,
1668    );
1669}
1670
1671fn copilot_settings_url(enterprise_uri: Option<&str>) -> String {
1672    match enterprise_uri {
1673        Some(uri) => {
1674            format!("{}{}", uri.trim_end_matches('/'), COPILOT_SETTINGS_PATH)
1675        }
1676        None => COPILOT_SETTINGS_URL.to_string(),
1677    }
1678}
1679
1680#[cfg(test)]
1681mod tests {
1682    use super::*;
1683    use gpui::TestAppContext;
1684
1685    #[gpui::test]
1686    async fn test_copilot_settings_url_with_enterprise_uri(cx: &mut TestAppContext) {
1687        cx.update(|cx| {
1688            let settings_store = SettingsStore::test(cx);
1689            cx.set_global(settings_store);
1690        });
1691
1692        cx.update_global(|settings_store: &mut SettingsStore, cx| {
1693            settings_store
1694                .set_user_settings(
1695                    r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com"}}}"#,
1696                    cx,
1697                )
1698                .unwrap();
1699        });
1700
1701        let url = cx.update(|cx| {
1702            let all_language_settings = all_language_settings(None, cx);
1703            copilot_settings_url(
1704                all_language_settings
1705                    .edit_predictions
1706                    .copilot
1707                    .enterprise_uri
1708                    .as_deref(),
1709            )
1710        });
1711
1712        assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
1713    }
1714
1715    #[gpui::test]
1716    async fn test_copilot_settings_url_with_enterprise_uri_trailing_slash(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_without_enterprise_uri(cx: &mut TestAppContext) {
1747        cx.update(|cx| {
1748            let settings_store = SettingsStore::test(cx);
1749            cx.set_global(settings_store);
1750        });
1751
1752        let url = cx.update(|cx| {
1753            let all_language_settings = all_language_settings(None, cx);
1754            copilot_settings_url(
1755                all_language_settings
1756                    .edit_predictions
1757                    .copilot
1758                    .enterprise_uri
1759                    .as_deref(),
1760            )
1761        });
1762
1763        assert_eq!(url, "https://github.com/settings/copilot");
1764    }
1765}