edit_prediction_button.rs

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