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