inline_completion_button.rs

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