1use anyhow::Result;
2use client::UserStore;
3use copilot::{Copilot, Status};
4use editor::{scroll::Autoscroll, Editor};
5use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
6use fs::Fs;
7use gpui::{
8 actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext,
9 AsyncWindowContext, Corner, Entity, IntoElement, Model, ParentElement, Render, Subscription,
10 View, ViewContext, WeakView, WindowContext,
11};
12use language::{
13 language_settings::{
14 self, all_language_settings, AllLanguageSettings, InlineCompletionProvider,
15 },
16 File, Language,
17};
18use settings::{update_settings_file, Settings, SettingsStore};
19use std::{path::Path, sync::Arc, time::Duration};
20use supermaven::{AccountStatus, Supermaven};
21use ui::{ActiveTheme as _, ButtonLike, Color, Icon, IconWithIndicator, Indicator};
22use workspace::{
23 create_and_open_local_file,
24 item::ItemHandle,
25 notifications::NotificationId,
26 ui::{
27 ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, PopoverMenu, Tooltip,
28 },
29 StatusItemView, Toast, Workspace,
30};
31use zed_actions::OpenBrowser;
32use zed_predict_tos::ZedPredictTos;
33use zeta::RateCompletionModal;
34
35actions!(zeta, [RateCompletions]);
36
37const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
38
39struct CopilotErrorToast;
40
41pub struct InlineCompletionButton {
42 editor_subscription: Option<(Subscription, usize)>,
43 editor_enabled: Option<bool>,
44 language: Option<Arc<Language>>,
45 file: Option<Arc<dyn File>>,
46 inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
47 fs: Arc<dyn Fs>,
48 workspace: WeakView<Workspace>,
49 user_store: Model<UserStore>,
50}
51
52enum SupermavenButtonStatus {
53 Ready,
54 Errored(String),
55 NeedsActivation(String),
56 Initializing,
57}
58
59impl Render for InlineCompletionButton {
60 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
61 let all_language_settings = all_language_settings(None, cx);
62
63 match all_language_settings.inline_completions.provider {
64 InlineCompletionProvider::None => div(),
65
66 InlineCompletionProvider::Copilot => {
67 let Some(copilot) = Copilot::global(cx) else {
68 return div();
69 };
70 let status = copilot.read(cx).status();
71
72 let enabled = self.editor_enabled.unwrap_or_else(|| {
73 all_language_settings.inline_completions_enabled(None, None, cx)
74 });
75
76 let icon = match status {
77 Status::Error(_) => IconName::CopilotError,
78 Status::Authorized => {
79 if enabled {
80 IconName::Copilot
81 } else {
82 IconName::CopilotDisabled
83 }
84 }
85 _ => IconName::CopilotInit,
86 };
87
88 if let Status::Error(e) = status {
89 return div().child(
90 IconButton::new("copilot-error", icon)
91 .icon_size(IconSize::Small)
92 .on_click(cx.listener(move |_, _, cx| {
93 if let Some(workspace) = cx.window_handle().downcast::<Workspace>()
94 {
95 workspace
96 .update(cx, |workspace, cx| {
97 workspace.show_toast(
98 Toast::new(
99 NotificationId::unique::<CopilotErrorToast>(),
100 format!("Copilot can't be started: {}", e),
101 )
102 .on_click("Reinstall Copilot", |cx| {
103 if let Some(copilot) = Copilot::global(cx) {
104 copilot
105 .update(cx, |copilot, cx| {
106 copilot.reinstall(cx)
107 })
108 .detach();
109 }
110 }),
111 cx,
112 );
113 })
114 .ok();
115 }
116 }))
117 .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
118 );
119 }
120 let this = cx.view().clone();
121
122 div().child(
123 PopoverMenu::new("copilot")
124 .menu(move |cx| {
125 Some(match status {
126 Status::Authorized => {
127 this.update(cx, |this, cx| this.build_copilot_context_menu(cx))
128 }
129 _ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
130 })
131 })
132 .anchor(Corner::BottomRight)
133 .trigger(
134 IconButton::new("copilot-icon", icon)
135 .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
136 ),
137 )
138 }
139
140 InlineCompletionProvider::Supermaven => {
141 let Some(supermaven) = Supermaven::global(cx) else {
142 return div();
143 };
144
145 let supermaven = supermaven.read(cx);
146
147 let status = match supermaven {
148 Supermaven::Starting => SupermavenButtonStatus::Initializing,
149 Supermaven::FailedDownload { error } => {
150 SupermavenButtonStatus::Errored(error.to_string())
151 }
152 Supermaven::Spawned(agent) => {
153 let account_status = agent.account_status.clone();
154 match account_status {
155 AccountStatus::NeedsActivation { activate_url } => {
156 SupermavenButtonStatus::NeedsActivation(activate_url.clone())
157 }
158 AccountStatus::Unknown => SupermavenButtonStatus::Initializing,
159 AccountStatus::Ready => SupermavenButtonStatus::Ready,
160 }
161 }
162 Supermaven::Error { error } => {
163 SupermavenButtonStatus::Errored(error.to_string())
164 }
165 };
166
167 let icon = status.to_icon();
168 let tooltip_text = status.to_tooltip();
169 let this = cx.view().clone();
170 let fs = self.fs.clone();
171
172 return div().child(
173 PopoverMenu::new("supermaven")
174 .menu(move |cx| match &status {
175 SupermavenButtonStatus::NeedsActivation(activate_url) => {
176 Some(ContextMenu::build(cx, |menu, _| {
177 let fs = fs.clone();
178 let activate_url = activate_url.clone();
179 menu.entry("Sign In", None, move |cx| {
180 cx.open_url(activate_url.as_str())
181 })
182 .entry(
183 "Use Copilot",
184 None,
185 move |cx| {
186 set_completion_provider(
187 fs.clone(),
188 cx,
189 InlineCompletionProvider::Copilot,
190 )
191 },
192 )
193 }))
194 }
195 SupermavenButtonStatus::Ready => Some(
196 this.update(cx, |this, cx| this.build_supermaven_context_menu(cx)),
197 ),
198 _ => None,
199 })
200 .anchor(Corner::BottomRight)
201 .trigger(
202 IconButton::new("supermaven-icon", icon)
203 .tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx)),
204 ),
205 );
206 }
207
208 InlineCompletionProvider::Zed => {
209 if !cx.has_flag::<PredictEditsFeatureFlag>() {
210 return div();
211 }
212
213 if !self
214 .user_store
215 .read(cx)
216 .current_user_has_accepted_terms()
217 .unwrap_or(false)
218 {
219 let workspace = self.workspace.clone();
220 let user_store = self.user_store.clone();
221
222 return div().child(
223 ButtonLike::new("zeta-pending-tos-icon")
224 .child(
225 IconWithIndicator::new(
226 Icon::new(IconName::ZedPredict),
227 Some(Indicator::dot().color(Color::Error)),
228 )
229 .indicator_border_color(Some(
230 cx.theme().colors().status_bar_background,
231 ))
232 .into_any_element(),
233 )
234 .tooltip(|cx| {
235 Tooltip::with_meta(
236 "Edit Predictions",
237 None,
238 "Read Terms of Service",
239 cx,
240 )
241 })
242 .on_click(cx.listener(move |_, _, cx| {
243 let user_store = user_store.clone();
244
245 if let Some(workspace) = workspace.upgrade() {
246 ZedPredictTos::toggle(workspace, user_store, cx);
247 }
248 })),
249 );
250 }
251
252 let this = cx.view().clone();
253 let button = IconButton::new("zeta", IconName::ZedPredict)
254 .tooltip(|cx| Tooltip::text("Edit Prediction", cx));
255
256 let is_refreshing = self
257 .inline_completion_provider
258 .as_ref()
259 .map_or(false, |provider| provider.is_refreshing(cx));
260
261 let mut popover_menu = PopoverMenu::new("zeta")
262 .menu(move |cx| {
263 Some(this.update(cx, |this, cx| this.build_zeta_context_menu(cx)))
264 })
265 .anchor(Corner::BottomRight);
266 if is_refreshing {
267 popover_menu = popover_menu.trigger(
268 button.with_animation(
269 "pulsating-label",
270 Animation::new(Duration::from_secs(2))
271 .repeat()
272 .with_easing(pulsating_between(0.2, 1.0)),
273 |icon_button, delta| icon_button.alpha(delta),
274 ),
275 );
276 } else {
277 popover_menu = popover_menu.trigger(button);
278 }
279
280 div().child(popover_menu.into_any_element())
281 }
282 }
283 }
284}
285
286impl InlineCompletionButton {
287 pub fn new(
288 workspace: WeakView<Workspace>,
289 fs: Arc<dyn Fs>,
290 user_store: Model<UserStore>,
291 cx: &mut ViewContext<Self>,
292 ) -> Self {
293 if let Some(copilot) = Copilot::global(cx) {
294 cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
295 }
296
297 cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
298 .detach();
299
300 Self {
301 editor_subscription: None,
302 editor_enabled: None,
303 language: None,
304 file: None,
305 inline_completion_provider: None,
306 workspace,
307 fs,
308 user_store,
309 }
310 }
311
312 pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
313 let fs = self.fs.clone();
314 ContextMenu::build(cx, |menu, _| {
315 menu.entry("Sign In", None, copilot::initiate_sign_in)
316 .entry("Disable Copilot", None, {
317 let fs = fs.clone();
318 move |cx| hide_copilot(fs.clone(), cx)
319 })
320 .entry("Use Supermaven", None, {
321 let fs = fs.clone();
322 move |cx| {
323 set_completion_provider(
324 fs.clone(),
325 cx,
326 InlineCompletionProvider::Supermaven,
327 )
328 }
329 })
330 })
331 }
332
333 pub fn build_language_settings_menu(
334 &self,
335 mut menu: ContextMenu,
336 cx: &mut WindowContext,
337 ) -> ContextMenu {
338 let fs = self.fs.clone();
339
340 if let Some(language) = self.language.clone() {
341 let fs = fs.clone();
342 let language_enabled =
343 language_settings::language_settings(Some(language.name()), None, cx)
344 .show_inline_completions;
345
346 menu = menu.entry(
347 format!(
348 "{} Inline Completions for {}",
349 if language_enabled { "Hide" } else { "Show" },
350 language.name()
351 ),
352 None,
353 move |cx| toggle_inline_completions_for_language(language.clone(), fs.clone(), cx),
354 );
355 }
356
357 let settings = AllLanguageSettings::get_global(cx);
358
359 if let Some(file) = &self.file {
360 let path = file.path().clone();
361 let path_enabled = settings.inline_completions_enabled_for_path(&path);
362
363 menu = menu.entry(
364 format!(
365 "{} Inline Completions for This Path",
366 if path_enabled { "Hide" } else { "Show" }
367 ),
368 None,
369 move |cx| {
370 if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
371 if let Ok(workspace) = workspace.root_view(cx) {
372 let workspace = workspace.downgrade();
373 cx.spawn(|cx| {
374 configure_disabled_globs(
375 workspace,
376 path_enabled.then_some(path.clone()),
377 cx,
378 )
379 })
380 .detach_and_log_err(cx);
381 }
382 }
383 },
384 );
385 }
386
387 let globally_enabled = settings.inline_completions_enabled(None, None, cx);
388 menu.entry(
389 if globally_enabled {
390 "Hide Inline Completions for All Files"
391 } else {
392 "Show Inline Completions for All Files"
393 },
394 None,
395 move |cx| toggle_inline_completions_globally(fs.clone(), cx),
396 )
397 }
398
399 fn build_copilot_context_menu(&self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
400 ContextMenu::build(cx, |menu, cx| {
401 self.build_language_settings_menu(menu, cx)
402 .separator()
403 .link(
404 "Go to Copilot Settings",
405 OpenBrowser {
406 url: COPILOT_SETTINGS_URL.to_string(),
407 }
408 .boxed_clone(),
409 )
410 .action("Sign Out", copilot::SignOut.boxed_clone())
411 })
412 }
413
414 fn build_supermaven_context_menu(&self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
415 ContextMenu::build(cx, |menu, cx| {
416 self.build_language_settings_menu(menu, cx)
417 .separator()
418 .action("Sign Out", supermaven::SignOut.boxed_clone())
419 })
420 }
421
422 fn build_zeta_context_menu(&self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
423 let workspace = self.workspace.clone();
424 ContextMenu::build(cx, |menu, cx| {
425 self.build_language_settings_menu(menu, cx)
426 .separator()
427 .entry(
428 "Rate Completions",
429 Some(RateCompletions.boxed_clone()),
430 move |cx| {
431 workspace
432 .update(cx, |workspace, cx| {
433 RateCompletionModal::toggle(workspace, cx)
434 })
435 .ok();
436 },
437 )
438 })
439 }
440
441 pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
442 let editor = editor.read(cx);
443 let snapshot = editor.buffer().read(cx).snapshot(cx);
444 let suggestion_anchor = editor.selections.newest_anchor().start;
445 let language = snapshot.language_at(suggestion_anchor);
446 let file = snapshot.file_at(suggestion_anchor).cloned();
447 self.editor_enabled = {
448 let file = file.as_ref();
449 Some(
450 file.map(|file| !file.is_private()).unwrap_or(true)
451 && all_language_settings(file, cx).inline_completions_enabled(
452 language,
453 file.map(|file| file.path().as_ref()),
454 cx,
455 ),
456 )
457 };
458 self.inline_completion_provider = editor.inline_completion_provider();
459 self.language = language.cloned();
460 self.file = file;
461
462 cx.notify()
463 }
464}
465
466impl StatusItemView for InlineCompletionButton {
467 fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
468 if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
469 self.editor_subscription = Some((
470 cx.observe(&editor, Self::update_enabled),
471 editor.entity_id().as_u64() as usize,
472 ));
473 self.update_enabled(editor, cx);
474 } else {
475 self.language = None;
476 self.editor_subscription = None;
477 self.editor_enabled = None;
478 }
479 cx.notify();
480 }
481}
482
483impl SupermavenButtonStatus {
484 fn to_icon(&self) -> IconName {
485 match self {
486 SupermavenButtonStatus::Ready => IconName::Supermaven,
487 SupermavenButtonStatus::Errored(_) => IconName::SupermavenError,
488 SupermavenButtonStatus::NeedsActivation(_) => IconName::SupermavenInit,
489 SupermavenButtonStatus::Initializing => IconName::SupermavenInit,
490 }
491 }
492
493 fn to_tooltip(&self) -> String {
494 match self {
495 SupermavenButtonStatus::Ready => "Supermaven is ready".to_string(),
496 SupermavenButtonStatus::Errored(error) => format!("Supermaven error: {}", error),
497 SupermavenButtonStatus::NeedsActivation(_) => "Supermaven needs activation".to_string(),
498 SupermavenButtonStatus::Initializing => "Supermaven initializing".to_string(),
499 }
500 }
501}
502
503async fn configure_disabled_globs(
504 workspace: WeakView<Workspace>,
505 path_to_disable: Option<Arc<Path>>,
506 mut cx: AsyncWindowContext,
507) -> Result<()> {
508 let settings_editor = workspace
509 .update(&mut cx, |_, cx| {
510 create_and_open_local_file(paths::settings_file(), cx, || {
511 settings::initial_user_settings_content().as_ref().into()
512 })
513 })?
514 .await?
515 .downcast::<Editor>()
516 .unwrap();
517
518 settings_editor.downgrade().update(&mut cx, |item, cx| {
519 let text = item.buffer().read(cx).snapshot(cx).text();
520
521 let settings = cx.global::<SettingsStore>();
522 let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
523 let copilot = file.inline_completions.get_or_insert_with(Default::default);
524 let globs = copilot.disabled_globs.get_or_insert_with(|| {
525 settings
526 .get::<AllLanguageSettings>(None)
527 .inline_completions
528 .disabled_globs
529 .iter()
530 .map(|glob| glob.glob().to_string())
531 .collect()
532 });
533
534 if let Some(path_to_disable) = &path_to_disable {
535 globs.push(path_to_disable.to_string_lossy().into_owned());
536 } else {
537 globs.clear();
538 }
539 });
540
541 if !edits.is_empty() {
542 item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
543 selections.select_ranges(edits.iter().map(|e| e.0.clone()));
544 });
545
546 // When *enabling* a path, don't actually perform an edit, just select the range.
547 if path_to_disable.is_some() {
548 item.edit(edits.iter().cloned(), cx);
549 }
550 }
551 })?;
552
553 anyhow::Ok(())
554}
555
556fn toggle_inline_completions_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
557 let show_inline_completions =
558 all_language_settings(None, cx).inline_completions_enabled(None, None, cx);
559 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
560 file.defaults.show_inline_completions = Some(!show_inline_completions)
561 });
562}
563
564fn set_completion_provider(
565 fs: Arc<dyn Fs>,
566 cx: &mut AppContext,
567 provider: InlineCompletionProvider,
568) {
569 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
570 file.features
571 .get_or_insert(Default::default())
572 .inline_completion_provider = Some(provider);
573 });
574}
575
576fn toggle_inline_completions_for_language(
577 language: Arc<Language>,
578 fs: Arc<dyn Fs>,
579 cx: &mut AppContext,
580) {
581 let show_inline_completions =
582 all_language_settings(None, cx).inline_completions_enabled(Some(&language), None, cx);
583 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
584 file.languages
585 .entry(language.name())
586 .or_default()
587 .show_inline_completions = Some(!show_inline_completions);
588 });
589}
590
591fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
592 update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
593 file.features
594 .get_or_insert(Default::default())
595 .inline_completion_provider = Some(InlineCompletionProvider::None);
596 });
597}