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(IconButton::new("copilot-icon", icon).tooltip(|window, cx| {
146 Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
147 }))
148 .with_handle(self.popover_menu_handle.clone()),
149 )
150 }
151
152 EditPredictionProvider::Supermaven => {
153 let Some(supermaven) = Supermaven::global(cx) else {
154 return div();
155 };
156
157 let supermaven = supermaven.read(cx);
158
159 let status = match supermaven {
160 Supermaven::Starting => SupermavenButtonStatus::Initializing,
161 Supermaven::FailedDownload { error } => {
162 SupermavenButtonStatus::Errored(error.to_string())
163 }
164 Supermaven::Spawned(agent) => {
165 let account_status = agent.account_status.clone();
166 match account_status {
167 AccountStatus::NeedsActivation { activate_url } => {
168 SupermavenButtonStatus::NeedsActivation(activate_url.clone())
169 }
170 AccountStatus::Unknown => SupermavenButtonStatus::Initializing,
171 AccountStatus::Ready => SupermavenButtonStatus::Ready,
172 }
173 }
174 Supermaven::Error { error } => {
175 SupermavenButtonStatus::Errored(error.to_string())
176 }
177 };
178
179 let icon = status.to_icon();
180 let tooltip_text = status.to_tooltip();
181 let has_menu = status.has_menu();
182 let this = cx.entity().clone();
183 let fs = self.fs.clone();
184
185 return div().child(
186 PopoverMenu::new("supermaven")
187 .menu(move |window, cx| match &status {
188 SupermavenButtonStatus::NeedsActivation(activate_url) => {
189 Some(ContextMenu::build(window, cx, |menu, _, _| {
190 let fs = fs.clone();
191 let activate_url = activate_url.clone();
192 menu.entry("Sign In", None, move |_, cx| {
193 cx.open_url(activate_url.as_str())
194 })
195 .entry(
196 "Use Copilot",
197 None,
198 move |_, cx| {
199 set_completion_provider(
200 fs.clone(),
201 cx,
202 EditPredictionProvider::Copilot,
203 )
204 },
205 )
206 }))
207 }
208 SupermavenButtonStatus::Ready => Some(this.update(cx, |this, cx| {
209 this.build_supermaven_context_menu(window, cx)
210 })),
211 _ => None,
212 })
213 .anchor(Corner::BottomRight)
214 .trigger(IconButton::new("supermaven-icon", icon).tooltip(
215 move |window, cx| {
216 if has_menu {
217 Tooltip::for_action(
218 tooltip_text.clone(),
219 &ToggleMenu,
220 window,
221 cx,
222 )
223 } else {
224 Tooltip::text(tooltip_text.clone())(window, cx)
225 }
226 },
227 ))
228 .with_handle(self.popover_menu_handle.clone()),
229 );
230 }
231
232 EditPredictionProvider::Zed => {
233 if !cx.has_flag::<PredictEditsFeatureFlag>() {
234 return div();
235 }
236
237 let enabled = self.editor_enabled.unwrap_or(true);
238
239 let zeta_icon = if enabled {
240 IconName::ZedPredict
241 } else {
242 IconName::ZedPredictDisabled
243 };
244
245 let current_user_terms_accepted =
246 self.user_store.read(cx).current_user_has_accepted_terms();
247
248 if !current_user_terms_accepted.unwrap_or(false) {
249 let signed_in = current_user_terms_accepted.is_some();
250 let tooltip_meta = if signed_in {
251 "Read Terms of Service"
252 } else {
253 "Sign in to use"
254 };
255
256 return div().child(
257 IconButton::new("zed-predict-pending-button", zeta_icon)
258 .shape(IconButtonShape::Square)
259 .indicator(Indicator::dot().color(Color::Error))
260 .indicator_border_color(Some(cx.theme().colors().status_bar_background))
261 .tooltip(move |window, cx| {
262 Tooltip::with_meta(
263 "Edit Predictions",
264 None,
265 tooltip_meta,
266 window,
267 cx,
268 )
269 })
270 .on_click(cx.listener(move |_, _, window, cx| {
271 telemetry::event!(
272 "Pending ToS Clicked",
273 source = "Edit Prediction Status Button"
274 );
275 window.dispatch_action(
276 zed_actions::OpenZedPredictOnboarding.boxed_clone(),
277 cx,
278 );
279 })),
280 );
281 }
282
283 let show_editor_predictions = self.editor_show_predictions;
284
285 let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
286 .shape(IconButtonShape::Square)
287 .when(enabled && !show_editor_predictions, |this| {
288 this.indicator(Indicator::dot().color(Color::Muted))
289 .indicator_border_color(Some(cx.theme().colors().status_bar_background))
290 })
291 .when(!self.popover_menu_handle.is_deployed(), |element| {
292 element.tooltip(move |window, cx| {
293 if enabled {
294 if show_editor_predictions {
295 Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
296 } else {
297 Tooltip::with_meta(
298 "Edit Prediction",
299 Some(&ToggleMenu),
300 "Hidden For This File",
301 window,
302 cx,
303 )
304 }
305 } else {
306 Tooltip::with_meta(
307 "Edit Prediction",
308 Some(&ToggleMenu),
309 "Disabled For This File",
310 window,
311 cx,
312 )
313 }
314 })
315 });
316
317 let this = cx.entity().clone();
318
319 let mut popover_menu = PopoverMenu::new("zeta")
320 .menu(move |window, cx| {
321 Some(this.update(cx, |this, cx| this.build_zeta_context_menu(window, cx)))
322 })
323 .anchor(Corner::BottomRight)
324 .with_handle(self.popover_menu_handle.clone());
325
326 let is_refreshing = self
327 .edit_prediction_provider
328 .as_ref()
329 .map_or(false, |provider| provider.is_refreshing(cx));
330
331 if is_refreshing {
332 popover_menu = popover_menu.trigger(
333 icon_button.with_animation(
334 "pulsating-label",
335 Animation::new(Duration::from_secs(2))
336 .repeat()
337 .with_easing(pulsating_between(0.2, 1.0)),
338 |icon_button, delta| icon_button.alpha(delta),
339 ),
340 );
341 } else {
342 popover_menu = popover_menu.trigger(icon_button);
343 }
344
345 div().child(popover_menu.into_any_element())
346 }
347 }
348 }
349}
350
351impl InlineCompletionButton {
352 pub fn new(
353 workspace: WeakEntity<Workspace>,
354 fs: Arc<dyn Fs>,
355 user_store: Entity<UserStore>,
356 popover_menu_handle: PopoverMenuHandle<ContextMenu>,
357 cx: &mut Context<Self>,
358 ) -> Self {
359 if let Some(copilot) = Copilot::global(cx) {
360 cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
361 }
362
363 cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
364 .detach();
365
366 Self {
367 editor_subscription: None,
368 editor_enabled: None,
369 editor_show_predictions: true,
370 editor_focus_handle: None,
371 language: None,
372 file: None,
373 edit_prediction_provider: None,
374 popover_menu_handle,
375 workspace,
376 fs,
377 user_store,
378 }
379 }
380
381 pub fn build_copilot_start_menu(
382 &mut self,
383 window: &mut Window,
384 cx: &mut Context<Self>,
385 ) -> Entity<ContextMenu> {
386 let fs = self.fs.clone();
387 ContextMenu::build(window, cx, |menu, _, _| {
388 menu.entry("Sign In", None, copilot::initiate_sign_in)
389 .entry("Disable Copilot", None, {
390 let fs = fs.clone();
391 move |_window, cx| hide_copilot(fs.clone(), cx)
392 })
393 .entry("Use Supermaven", None, {
394 let fs = fs.clone();
395 move |_window, cx| {
396 set_completion_provider(fs.clone(), cx, EditPredictionProvider::Supermaven)
397 }
398 })
399 })
400 }
401
402 pub fn build_language_settings_menu(&self, mut menu: ContextMenu, cx: &mut App) -> ContextMenu {
403 let fs = self.fs.clone();
404
405 menu = menu.header("Show Edit Predictions For");
406
407 if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
408 menu = menu.toggleable_entry(
409 "This File",
410 self.editor_show_predictions,
411 IconPosition::Start,
412 Some(Box::new(ToggleEditPrediction)),
413 {
414 let editor_focus_handle = editor_focus_handle.clone();
415 move |window, cx| {
416 editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx);
417 }
418 },
419 );
420 }
421
422 if let Some(language) = self.language.clone() {
423 let fs = fs.clone();
424 let language_enabled =
425 language_settings::language_settings(Some(language.name()), None, cx)
426 .show_edit_predictions;
427
428 menu = menu.toggleable_entry(
429 language.name(),
430 language_enabled,
431 IconPosition::Start,
432 None,
433 move |_, cx| {
434 toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx)
435 },
436 );
437 }
438
439 let settings = AllLanguageSettings::get_global(cx);
440 let globally_enabled = settings.show_inline_completions(None, cx);
441 menu = menu.toggleable_entry(
442 "All Files",
443 globally_enabled,
444 IconPosition::Start,
445 None,
446 move |_, cx| toggle_inline_completions_globally(fs.clone(), cx),
447 );
448 menu = menu.separator().header("Privacy Settings");
449
450 if let Some(provider) = &self.edit_prediction_provider {
451 let data_collection = provider.data_collection_state(cx);
452 if data_collection.is_supported() {
453 let provider = provider.clone();
454 let enabled = data_collection.is_enabled();
455
456 menu = menu.item(
457 // TODO: We want to add something later that communicates whether
458 // the current project is open-source.
459 ContextMenuEntry::new("Share Training Data")
460 .toggleable(IconPosition::Start, data_collection.is_enabled())
461 .documentation_aside(|_| {
462 Label::new(indoc!{"
463 Help us improve our open model by sharing data from open source repositories. \
464 Zed must detect a license file in your repo for this setting to take effect.\
465 "}).into_any_element()
466 })
467 .handler(move |_, cx| {
468 provider.toggle_data_collection(cx);
469
470 if !enabled {
471 telemetry::event!(
472 "Data Collection Enabled",
473 source = "Edit Prediction Status Menu"
474 );
475 } else {
476 telemetry::event!(
477 "Data Collection Disabled",
478 source = "Edit Prediction Status Menu"
479 );
480 }
481 })
482 )
483 }
484 }
485
486 menu = menu.item(
487 ContextMenuEntry::new("Configure Excluded Files")
488 .icon(IconName::LockOutlined)
489 .icon_color(Color::Muted)
490 .documentation_aside(|_| {
491 Label::new(indoc!{"
492 Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
493 })
494 .handler(move |window, cx| {
495 if let Some(workspace) = window.root().flatten() {
496 let workspace = workspace.downgrade();
497 window
498 .spawn(cx, |cx| {
499 open_disabled_globs_setting_in_editor(
500 workspace,
501 cx,
502 )
503 })
504 .detach_and_log_err(cx);
505 }
506 }),
507 );
508
509 if !self.editor_enabled.unwrap_or(true) {
510 menu = menu.item(
511 ContextMenuEntry::new("This file is excluded.")
512 .disabled(true)
513 .icon(IconName::ZedPredictDisabled)
514 .icon_size(IconSize::Small),
515 );
516 }
517
518 if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
519 menu = menu
520 .separator()
521 .entry(
522 "Predict Edit at Cursor",
523 Some(Box::new(ShowEditPrediction)),
524 {
525 let editor_focus_handle = editor_focus_handle.clone();
526 move |window, cx| {
527 editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
528 }
529 },
530 )
531 .context(editor_focus_handle);
532 }
533
534 menu
535 }
536
537 fn build_copilot_context_menu(
538 &self,
539 window: &mut Window,
540 cx: &mut Context<Self>,
541 ) -> Entity<ContextMenu> {
542 ContextMenu::build(window, cx, |menu, _, cx| {
543 self.build_language_settings_menu(menu, cx)
544 .separator()
545 .link(
546 "Go to Copilot Settings",
547 OpenBrowser {
548 url: COPILOT_SETTINGS_URL.to_string(),
549 }
550 .boxed_clone(),
551 )
552 .action("Sign Out", copilot::SignOut.boxed_clone())
553 })
554 }
555
556 fn build_supermaven_context_menu(
557 &self,
558 window: &mut Window,
559 cx: &mut Context<Self>,
560 ) -> Entity<ContextMenu> {
561 ContextMenu::build(window, cx, |menu, _, cx| {
562 self.build_language_settings_menu(menu, cx)
563 .separator()
564 .action("Sign Out", supermaven::SignOut.boxed_clone())
565 })
566 }
567
568 fn build_zeta_context_menu(
569 &self,
570 window: &mut Window,
571 cx: &mut Context<Self>,
572 ) -> Entity<ContextMenu> {
573 let workspace = self.workspace.clone();
574 ContextMenu::build(window, cx, |menu, _window, cx| {
575 self.build_language_settings_menu(menu, cx).when(
576 cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
577 |this| {
578 this.entry(
579 "Rate Completions",
580 Some(RateCompletions.boxed_clone()),
581 move |window, cx| {
582 workspace
583 .update(cx, |workspace, cx| {
584 RateCompletionModal::toggle(workspace, window, cx)
585 })
586 .ok();
587 },
588 )
589 },
590 )
591 })
592 }
593
594 pub fn update_enabled(&mut self, editor: Entity<Editor>, cx: &mut Context<Self>) {
595 let editor = editor.read(cx);
596 let snapshot = editor.buffer().read(cx).snapshot(cx);
597 let suggestion_anchor = editor.selections.newest_anchor().start;
598 let language = snapshot.language_at(suggestion_anchor);
599 let file = snapshot.file_at(suggestion_anchor).cloned();
600 self.editor_enabled = {
601 let file = file.as_ref();
602 Some(
603 file.map(|file| {
604 all_language_settings(Some(file), cx)
605 .inline_completions_enabled_for_path(file.path())
606 })
607 .unwrap_or(true),
608 )
609 };
610 self.editor_show_predictions = editor.should_show_inline_completions(cx);
611 self.edit_prediction_provider = editor.edit_prediction_provider();
612 self.language = language.cloned();
613 self.file = file;
614 self.editor_focus_handle = Some(editor.focus_handle(cx));
615
616 cx.notify();
617 }
618
619 pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
620 self.popover_menu_handle.toggle(window, cx);
621 }
622}
623
624impl StatusItemView for InlineCompletionButton {
625 fn set_active_pane_item(
626 &mut self,
627 item: Option<&dyn ItemHandle>,
628 _: &mut Window,
629 cx: &mut Context<Self>,
630 ) {
631 if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
632 self.editor_subscription = Some((
633 cx.observe(&editor, Self::update_enabled),
634 editor.entity_id().as_u64() as usize,
635 ));
636 self.update_enabled(editor, cx);
637 } else {
638 self.language = None;
639 self.editor_subscription = None;
640 self.editor_enabled = None;
641 }
642 cx.notify();
643 }
644}
645
646impl SupermavenButtonStatus {
647 fn to_icon(&self) -> IconName {
648 match self {
649 SupermavenButtonStatus::Ready => IconName::Supermaven,
650 SupermavenButtonStatus::Errored(_) => IconName::SupermavenError,
651 SupermavenButtonStatus::NeedsActivation(_) => IconName::SupermavenInit,
652 SupermavenButtonStatus::Initializing => IconName::SupermavenInit,
653 }
654 }
655
656 fn to_tooltip(&self) -> String {
657 match self {
658 SupermavenButtonStatus::Ready => "Supermaven is ready".to_string(),
659 SupermavenButtonStatus::Errored(error) => format!("Supermaven error: {}", error),
660 SupermavenButtonStatus::NeedsActivation(_) => "Supermaven needs activation".to_string(),
661 SupermavenButtonStatus::Initializing => "Supermaven initializing".to_string(),
662 }
663 }
664
665 fn has_menu(&self) -> bool {
666 match self {
667 SupermavenButtonStatus::Ready | SupermavenButtonStatus::NeedsActivation(_) => true,
668 SupermavenButtonStatus::Errored(_) | SupermavenButtonStatus::Initializing => false,
669 }
670 }
671}
672
673async fn open_disabled_globs_setting_in_editor(
674 workspace: WeakEntity<Workspace>,
675 mut cx: AsyncWindowContext,
676) -> Result<()> {
677 let settings_editor = workspace
678 .update_in(&mut cx, |_, window, cx| {
679 create_and_open_local_file(paths::settings_file(), window, cx, || {
680 settings::initial_user_settings_content().as_ref().into()
681 })
682 })?
683 .await?
684 .downcast::<Editor>()
685 .unwrap();
686
687 settings_editor
688 .downgrade()
689 .update_in(&mut cx, |item, window, cx| {
690 let text = item.buffer().read(cx).snapshot(cx).text();
691
692 let settings = cx.global::<SettingsStore>();
693
694 // Ensure that we always have "inline_completions { "disabled_globs": [] }"
695 let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
696 file.edit_predictions
697 .get_or_insert_with(Default::default)
698 .disabled_globs
699 .get_or_insert_with(Vec::new);
700 });
701
702 if !edits.is_empty() {
703 item.edit(edits.iter().cloned(), cx);
704 }
705
706 let text = item.buffer().read(cx).snapshot(cx).text();
707
708 static DISABLED_GLOBS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
709 Regex::new(r#""disabled_globs":\s*\[\s*(?P<content>(?:.|\n)*?)\s*\]"#).unwrap()
710 });
711 // Only capture [...]
712 let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| {
713 captures
714 .name("content")
715 .map(|inner_match| inner_match.start()..inner_match.end())
716 });
717 if let Some(range) = range {
718 item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
719 selections.select_ranges(vec![range]);
720 });
721 }
722 })?;
723
724 anyhow::Ok(())
725}
726
727fn toggle_inline_completions_globally(fs: Arc<dyn Fs>, cx: &mut App) {
728 let show_edit_predictions = all_language_settings(None, cx).show_inline_completions(None, cx);
729 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
730 file.defaults.show_edit_predictions = Some(!show_edit_predictions)
731 });
732}
733
734fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
735 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
736 file.features
737 .get_or_insert(Default::default())
738 .edit_prediction_provider = Some(provider);
739 });
740}
741
742fn toggle_show_inline_completions_for_language(
743 language: Arc<Language>,
744 fs: Arc<dyn Fs>,
745 cx: &mut App,
746) {
747 let show_edit_predictions =
748 all_language_settings(None, cx).show_inline_completions(Some(&language), cx);
749 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
750 file.languages
751 .entry(language.name())
752 .or_default()
753 .show_edit_predictions = Some(!show_edit_predictions);
754 });
755}
756
757fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
758 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
759 file.features
760 .get_or_insert(Default::default())
761 .edit_prediction_provider = Some(EditPredictionProvider::None);
762 });
763}