inline_completion_button.rs

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