1use anyhow::Result;
2use client::UserStore;
3use copilot::{Copilot, Status};
4use editor::{
5 actions::{ShowEditPrediction, ToggleEditPrediction},
6 scroll::Autoscroll,
7 Editor,
8};
9use feature_flags::{
10 FeatureFlagAppExt, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag,
11};
12use fs::Fs;
13use gpui::{
14 actions, div, pulsating_between, Action, Animation, AnimationExt, App, AsyncWindowContext,
15 Corner, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Subscription,
16 WeakEntity,
17};
18use indoc::indoc;
19use language::{
20 language_settings::{self, all_language_settings, AllLanguageSettings, EditPredictionProvider},
21 File, Language,
22};
23use regex::Regex;
24use settings::{update_settings_file, Settings, SettingsStore};
25use std::{
26 sync::{Arc, LazyLock},
27 time::Duration,
28};
29use supermaven::{AccountStatus, Supermaven};
30use ui::{
31 prelude::*, Clickable, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, Indicator,
32 PopoverMenu, PopoverMenuHandle, Tooltip,
33};
34use workspace::{
35 create_and_open_local_file, item::ItemHandle, notifications::NotificationId, StatusItemView,
36 Toast, Workspace,
37};
38use zed_actions::OpenBrowser;
39use zeta::RateCompletionModal;
40
41actions!(zeta, [RateCompletions]);
42actions!(edit_prediction, [ToggleMenu]);
43
44const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
45
46struct CopilotErrorToast;
47
48pub struct InlineCompletionButton {
49 editor_subscription: Option<(Subscription, usize)>,
50 editor_enabled: Option<bool>,
51 editor_show_predictions: bool,
52 editor_focus_handle: Option<FocusHandle>,
53 language: Option<Arc<Language>>,
54 file: Option<Arc<dyn File>>,
55 edit_prediction_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
56 fs: Arc<dyn Fs>,
57 workspace: WeakEntity<Workspace>,
58 user_store: Entity<UserStore>,
59 popover_menu_handle: PopoverMenuHandle<ContextMenu>,
60}
61
62enum SupermavenButtonStatus {
63 Ready,
64 Errored(String),
65 NeedsActivation(String),
66 Initializing,
67}
68
69impl Render for InlineCompletionButton {
70 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
71 let all_language_settings = all_language_settings(None, cx);
72
73 match all_language_settings.edit_predictions.provider {
74 EditPredictionProvider::None => div(),
75
76 EditPredictionProvider::Copilot => {
77 let Some(copilot) = Copilot::global(cx) else {
78 return div();
79 };
80 let status = copilot.read(cx).status();
81
82 let enabled = self.editor_enabled.unwrap_or(false);
83
84 let icon = match status {
85 Status::Error(_) => IconName::CopilotError,
86 Status::Authorized => {
87 if enabled {
88 IconName::Copilot
89 } else {
90 IconName::CopilotDisabled
91 }
92 }
93 _ => IconName::CopilotInit,
94 };
95
96 if let Status::Error(e) = status {
97 return div().child(
98 IconButton::new("copilot-error", icon)
99 .icon_size(IconSize::Small)
100 .on_click(cx.listener(move |_, _, window, cx| {
101 if let Some(workspace) = window.root::<Workspace>().flatten() {
102 workspace.update(cx, |workspace, cx| {
103 workspace.show_toast(
104 Toast::new(
105 NotificationId::unique::<CopilotErrorToast>(),
106 format!("Copilot can't be started: {}", e),
107 )
108 .on_click(
109 "Reinstall Copilot",
110 |_, cx| {
111 if let Some(copilot) = Copilot::global(cx) {
112 copilot
113 .update(cx, |copilot, cx| {
114 copilot.reinstall(cx)
115 })
116 .detach();
117 }
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().clone();
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.clone())
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().clone();
186 let fs = self.fs.clone();
187
188 return 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 Copilot",
200 None,
201 move |_, cx| {
202 set_completion_provider(
203 fs.clone(),
204 cx,
205 EditPredictionProvider::Copilot,
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 if !cx.has_flag::<PredictEditsFeatureFlag>() {
238 return div();
239 }
240
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 let current_user_terms_accepted =
250 self.user_store.read(cx).current_user_has_accepted_terms();
251
252 if !current_user_terms_accepted.unwrap_or(false) {
253 let signed_in = current_user_terms_accepted.is_some();
254 let tooltip_meta = if signed_in {
255 "Read Terms of Service"
256 } else {
257 "Sign in to use"
258 };
259
260 return div().child(
261 IconButton::new("zed-predict-pending-button", zeta_icon)
262 .shape(IconButtonShape::Square)
263 .indicator(Indicator::dot().color(Color::Error))
264 .indicator_border_color(Some(cx.theme().colors().status_bar_background))
265 .tooltip(move |window, cx| {
266 Tooltip::with_meta(
267 "Edit Predictions",
268 None,
269 tooltip_meta,
270 window,
271 cx,
272 )
273 })
274 .on_click(cx.listener(move |_, _, window, cx| {
275 telemetry::event!(
276 "Pending ToS Clicked",
277 source = "Edit Prediction Status Button"
278 );
279 window.dispatch_action(
280 zed_actions::OpenZedPredictOnboarding.boxed_clone(),
281 cx,
282 );
283 })),
284 );
285 }
286
287 let show_editor_predictions = self.editor_show_predictions;
288
289 let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
290 .shape(IconButtonShape::Square)
291 .when(enabled && !show_editor_predictions, |this| {
292 this.indicator(Indicator::dot().color(Color::Muted))
293 .indicator_border_color(Some(cx.theme().colors().status_bar_background))
294 })
295 .when(!self.popover_menu_handle.is_deployed(), |element| {
296 element.tooltip(move |window, cx| {
297 if enabled {
298 if show_editor_predictions {
299 Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
300 } else {
301 Tooltip::with_meta(
302 "Edit Prediction",
303 Some(&ToggleMenu),
304 "Hidden For This File",
305 window,
306 cx,
307 )
308 }
309 } else {
310 Tooltip::with_meta(
311 "Edit Prediction",
312 Some(&ToggleMenu),
313 "Disabled For This File",
314 window,
315 cx,
316 )
317 }
318 })
319 });
320
321 let this = cx.entity().clone();
322
323 let mut popover_menu = PopoverMenu::new("zeta")
324 .menu(move |window, cx| {
325 Some(this.update(cx, |this, cx| this.build_zeta_context_menu(window, cx)))
326 })
327 .anchor(Corner::BottomRight)
328 .with_handle(self.popover_menu_handle.clone());
329
330 let is_refreshing = self
331 .edit_prediction_provider
332 .as_ref()
333 .map_or(false, |provider| provider.is_refreshing(cx));
334
335 if is_refreshing {
336 popover_menu = popover_menu.trigger(
337 icon_button.with_animation(
338 "pulsating-label",
339 Animation::new(Duration::from_secs(2))
340 .repeat()
341 .with_easing(pulsating_between(0.2, 1.0)),
342 |icon_button, delta| icon_button.alpha(delta),
343 ),
344 );
345 } else {
346 popover_menu = popover_menu.trigger(icon_button);
347 }
348
349 div().child(popover_menu.into_any_element())
350 }
351 }
352 }
353}
354
355impl InlineCompletionButton {
356 pub fn new(
357 workspace: WeakEntity<Workspace>,
358 fs: Arc<dyn Fs>,
359 user_store: Entity<UserStore>,
360 popover_menu_handle: PopoverMenuHandle<ContextMenu>,
361 cx: &mut Context<Self>,
362 ) -> Self {
363 if let Some(copilot) = Copilot::global(cx) {
364 cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
365 }
366
367 cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
368 .detach();
369
370 Self {
371 editor_subscription: None,
372 editor_enabled: None,
373 editor_show_predictions: true,
374 editor_focus_handle: None,
375 language: None,
376 file: None,
377 edit_prediction_provider: None,
378 popover_menu_handle,
379 workspace,
380 fs,
381 user_store,
382 }
383 }
384
385 pub fn build_copilot_start_menu(
386 &mut self,
387 window: &mut Window,
388 cx: &mut Context<Self>,
389 ) -> Entity<ContextMenu> {
390 let fs = self.fs.clone();
391 ContextMenu::build(window, cx, |menu, _, _| {
392 menu.entry("Sign In", None, copilot::initiate_sign_in)
393 .entry("Disable Copilot", None, {
394 let fs = fs.clone();
395 move |_window, cx| hide_copilot(fs.clone(), cx)
396 })
397 .entry("Use Supermaven", None, {
398 let fs = fs.clone();
399 move |_window, cx| {
400 set_completion_provider(fs.clone(), cx, EditPredictionProvider::Supermaven)
401 }
402 })
403 })
404 }
405
406 pub fn build_language_settings_menu(&self, mut menu: ContextMenu, cx: &mut App) -> ContextMenu {
407 let fs = self.fs.clone();
408
409 menu = menu.header("Show Edit Predictions For");
410
411 if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
412 menu = menu.toggleable_entry(
413 "This File",
414 self.editor_show_predictions,
415 IconPosition::Start,
416 Some(Box::new(ToggleEditPrediction)),
417 {
418 let editor_focus_handle = editor_focus_handle.clone();
419 move |window, cx| {
420 editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx);
421 }
422 },
423 );
424 }
425
426 if let Some(language) = self.language.clone() {
427 let fs = fs.clone();
428 let language_enabled =
429 language_settings::language_settings(Some(language.name()), None, cx)
430 .show_edit_predictions;
431
432 menu = menu.toggleable_entry(
433 language.name(),
434 language_enabled,
435 IconPosition::Start,
436 None,
437 move |_, cx| {
438 toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx)
439 },
440 );
441 }
442
443 let settings = AllLanguageSettings::get_global(cx);
444 let globally_enabled = settings.show_inline_completions(None, cx);
445 menu = menu.toggleable_entry(
446 "All Files",
447 globally_enabled,
448 IconPosition::Start,
449 None,
450 move |_, cx| toggle_inline_completions_globally(fs.clone(), cx),
451 );
452 menu = menu.separator().header("Privacy Settings");
453
454 if let Some(provider) = &self.edit_prediction_provider {
455 let data_collection = provider.data_collection_state(cx);
456 if data_collection.is_supported() {
457 let provider = provider.clone();
458 let enabled = data_collection.is_enabled();
459
460 menu = menu.item(
461 // TODO: We want to add something later that communicates whether
462 // the current project is open-source.
463 ContextMenuEntry::new("Share Training Data")
464 .toggleable(IconPosition::Start, data_collection.is_enabled())
465 .documentation_aside(|_| {
466 Label::new(indoc!{"
467 Help us improve our open model by sharing data from open source repositories. \
468 Zed must detect a license file in your repo for this setting to take effect.\
469 "}).into_any_element()
470 })
471 .handler(move |_, cx| {
472 provider.toggle_data_collection(cx);
473
474 if !enabled {
475 telemetry::event!(
476 "Data Collection Enabled",
477 source = "Edit Prediction Status Menu"
478 );
479 } else {
480 telemetry::event!(
481 "Data Collection Disabled",
482 source = "Edit Prediction Status Menu"
483 );
484 }
485 })
486 )
487 }
488 }
489
490 menu = menu.item(
491 ContextMenuEntry::new("Configure Excluded Files")
492 .icon(IconName::LockOutlined)
493 .icon_color(Color::Muted)
494 .documentation_aside(|_| {
495 Label::new(indoc!{"
496 Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
497 })
498 .handler(move |window, cx| {
499 if let Some(workspace) = window.root().flatten() {
500 let workspace = workspace.downgrade();
501 window
502 .spawn(cx, |cx| {
503 open_disabled_globs_setting_in_editor(
504 workspace,
505 cx,
506 )
507 })
508 .detach_and_log_err(cx);
509 }
510 }),
511 );
512
513 if !self.editor_enabled.unwrap_or(true) {
514 menu = menu.item(
515 ContextMenuEntry::new("This file is excluded.")
516 .disabled(true)
517 .icon(IconName::ZedPredictDisabled)
518 .icon_size(IconSize::Small),
519 );
520 }
521
522 if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
523 menu = menu
524 .separator()
525 .entry(
526 "Predict Edit at Cursor",
527 Some(Box::new(ShowEditPrediction)),
528 {
529 let editor_focus_handle = editor_focus_handle.clone();
530 move |window, cx| {
531 editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
532 }
533 },
534 )
535 .context(editor_focus_handle);
536 }
537
538 menu
539 }
540
541 fn build_copilot_context_menu(
542 &self,
543 window: &mut Window,
544 cx: &mut Context<Self>,
545 ) -> Entity<ContextMenu> {
546 ContextMenu::build(window, cx, |menu, _, cx| {
547 self.build_language_settings_menu(menu, cx)
548 .separator()
549 .link(
550 "Go to Copilot Settings",
551 OpenBrowser {
552 url: COPILOT_SETTINGS_URL.to_string(),
553 }
554 .boxed_clone(),
555 )
556 .action("Sign Out", copilot::SignOut.boxed_clone())
557 })
558 }
559
560 fn build_supermaven_context_menu(
561 &self,
562 window: &mut Window,
563 cx: &mut Context<Self>,
564 ) -> Entity<ContextMenu> {
565 ContextMenu::build(window, cx, |menu, _, cx| {
566 self.build_language_settings_menu(menu, cx)
567 .separator()
568 .action("Sign Out", supermaven::SignOut.boxed_clone())
569 })
570 }
571
572 fn build_zeta_context_menu(
573 &self,
574 window: &mut Window,
575 cx: &mut Context<Self>,
576 ) -> Entity<ContextMenu> {
577 let workspace = self.workspace.clone();
578 ContextMenu::build(window, cx, |menu, _window, cx| {
579 self.build_language_settings_menu(menu, cx).when(
580 cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
581 |this| {
582 this.entry(
583 "Rate Completions",
584 Some(RateCompletions.boxed_clone()),
585 move |window, cx| {
586 workspace
587 .update(cx, |workspace, cx| {
588 RateCompletionModal::toggle(workspace, window, cx)
589 })
590 .ok();
591 },
592 )
593 },
594 )
595 })
596 }
597
598 pub fn update_enabled(&mut self, editor: Entity<Editor>, cx: &mut Context<Self>) {
599 let editor = editor.read(cx);
600 let snapshot = editor.buffer().read(cx).snapshot(cx);
601 let suggestion_anchor = editor.selections.newest_anchor().start;
602 let language = snapshot.language_at(suggestion_anchor);
603 let file = snapshot.file_at(suggestion_anchor).cloned();
604 self.editor_enabled = {
605 let file = file.as_ref();
606 Some(
607 file.map(|file| {
608 all_language_settings(Some(file), cx)
609 .inline_completions_enabled_for_path(file.path())
610 })
611 .unwrap_or(true),
612 )
613 };
614 self.editor_show_predictions = editor.should_show_inline_completions(cx);
615 self.edit_prediction_provider = editor.edit_prediction_provider();
616 self.language = language.cloned();
617 self.file = file;
618 self.editor_focus_handle = Some(editor.focus_handle(cx));
619
620 cx.notify();
621 }
622
623 pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
624 self.popover_menu_handle.toggle(window, cx);
625 }
626}
627
628impl StatusItemView for InlineCompletionButton {
629 fn set_active_pane_item(
630 &mut self,
631 item: Option<&dyn ItemHandle>,
632 _: &mut Window,
633 cx: &mut Context<Self>,
634 ) {
635 if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
636 self.editor_subscription = Some((
637 cx.observe(&editor, Self::update_enabled),
638 editor.entity_id().as_u64() as usize,
639 ));
640 self.update_enabled(editor, cx);
641 } else {
642 self.language = None;
643 self.editor_subscription = None;
644 self.editor_enabled = None;
645 }
646 cx.notify();
647 }
648}
649
650impl SupermavenButtonStatus {
651 fn to_icon(&self) -> IconName {
652 match self {
653 SupermavenButtonStatus::Ready => IconName::Supermaven,
654 SupermavenButtonStatus::Errored(_) => IconName::SupermavenError,
655 SupermavenButtonStatus::NeedsActivation(_) => IconName::SupermavenInit,
656 SupermavenButtonStatus::Initializing => IconName::SupermavenInit,
657 }
658 }
659
660 fn to_tooltip(&self) -> String {
661 match self {
662 SupermavenButtonStatus::Ready => "Supermaven is ready".to_string(),
663 SupermavenButtonStatus::Errored(error) => format!("Supermaven error: {}", error),
664 SupermavenButtonStatus::NeedsActivation(_) => "Supermaven needs activation".to_string(),
665 SupermavenButtonStatus::Initializing => "Supermaven initializing".to_string(),
666 }
667 }
668
669 fn has_menu(&self) -> bool {
670 match self {
671 SupermavenButtonStatus::Ready | SupermavenButtonStatus::NeedsActivation(_) => true,
672 SupermavenButtonStatus::Errored(_) | SupermavenButtonStatus::Initializing => false,
673 }
674 }
675}
676
677async fn open_disabled_globs_setting_in_editor(
678 workspace: WeakEntity<Workspace>,
679 mut cx: AsyncWindowContext,
680) -> Result<()> {
681 let settings_editor = workspace
682 .update_in(&mut cx, |_, window, cx| {
683 create_and_open_local_file(paths::settings_file(), window, cx, || {
684 settings::initial_user_settings_content().as_ref().into()
685 })
686 })?
687 .await?
688 .downcast::<Editor>()
689 .unwrap();
690
691 settings_editor
692 .downgrade()
693 .update_in(&mut cx, |item, window, cx| {
694 let text = item.buffer().read(cx).snapshot(cx).text();
695
696 let settings = cx.global::<SettingsStore>();
697
698 // Ensure that we always have "inline_completions { "disabled_globs": [] }"
699 let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
700 file.edit_predictions
701 .get_or_insert_with(Default::default)
702 .disabled_globs
703 .get_or_insert_with(Vec::new);
704 });
705
706 if !edits.is_empty() {
707 item.edit(edits.iter().cloned(), cx);
708 }
709
710 let text = item.buffer().read(cx).snapshot(cx).text();
711
712 static DISABLED_GLOBS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
713 Regex::new(r#""disabled_globs":\s*\[\s*(?P<content>(?:.|\n)*?)\s*\]"#).unwrap()
714 });
715 // Only capture [...]
716 let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| {
717 captures
718 .name("content")
719 .map(|inner_match| inner_match.start()..inner_match.end())
720 });
721 if let Some(range) = range {
722 item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
723 selections.select_ranges(vec![range]);
724 });
725 }
726 })?;
727
728 anyhow::Ok(())
729}
730
731fn toggle_inline_completions_globally(fs: Arc<dyn Fs>, cx: &mut App) {
732 let show_edit_predictions = all_language_settings(None, cx).show_inline_completions(None, cx);
733 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
734 file.defaults.show_edit_predictions = Some(!show_edit_predictions)
735 });
736}
737
738fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
739 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
740 file.features
741 .get_or_insert(Default::default())
742 .edit_prediction_provider = Some(provider);
743 });
744}
745
746fn toggle_show_inline_completions_for_language(
747 language: Arc<Language>,
748 fs: Arc<dyn Fs>,
749 cx: &mut App,
750) {
751 let show_edit_predictions =
752 all_language_settings(None, cx).show_inline_completions(Some(&language), cx);
753 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
754 file.languages
755 .entry(language.name())
756 .or_default()
757 .show_edit_predictions = Some(!show_edit_predictions);
758 });
759}
760
761fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
762 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
763 file.features
764 .get_or_insert(Default::default())
765 .edit_prediction_provider = Some(EditPredictionProvider::None);
766 });
767}