1mod copilot_edit_prediction_delegate;
2pub mod request;
3
4use crate::request::{
5 DidFocus, DidFocusParams, FormattingOptions, InlineCompletionContext,
6 InlineCompletionTriggerKind, InlineCompletions, NextEditSuggestions,
7};
8use ::fs::Fs;
9use anyhow::{Context as _, Result, anyhow};
10use collections::{HashMap, HashSet};
11use command_palette_hooks::CommandPaletteFilter;
12use futures::future;
13use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared, select_biased};
14use gpui::{
15 App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Subscription,
16 Task, WeakEntity, actions,
17};
18use language::language_settings::{AllLanguageSettings, CopilotSettings};
19use language::{
20 Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
21 language_settings::{EditPredictionProvider, all_language_settings},
22 point_from_lsp, point_to_lsp,
23};
24use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
25use node_runtime::{NodeRuntime, VersionStrategy};
26use parking_lot::Mutex;
27use project::project_settings::ProjectSettings;
28use project::{DisableAiSettings, Project};
29use request::DidChangeStatus;
30use semver::Version;
31use serde_json::json;
32use settings::{Settings, SettingsStore};
33use std::{
34 any::TypeId,
35 collections::hash_map::Entry,
36 env,
37 ffi::OsString,
38 mem,
39 ops::Range,
40 path::{Path, PathBuf},
41 sync::Arc,
42};
43use sum_tree::Dimensions;
44use util::{ResultExt, fs::remove_matching};
45use workspace::AppState;
46
47pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
48
49actions!(
50 copilot,
51 [
52 /// Requests a code completion suggestion from Copilot.
53 Suggest,
54 /// Cycles to the next Copilot suggestion.
55 NextSuggestion,
56 /// Cycles to the previous Copilot suggestion.
57 PreviousSuggestion,
58 /// Reinstalls the Copilot language server.
59 Reinstall,
60 /// Signs in to GitHub Copilot.
61 SignIn,
62 /// Signs out of GitHub Copilot.
63 SignOut
64 ]
65);
66
67enum CopilotServer {
68 Disabled,
69 Starting { task: Shared<Task<()>> },
70 Error(Arc<str>),
71 Running(RunningCopilotServer),
72}
73
74impl CopilotServer {
75 fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
76 let server = self.as_running()?;
77 anyhow::ensure!(
78 matches!(server.sign_in_status, SignInStatus::Authorized),
79 "must sign in before using copilot"
80 );
81 Ok(server)
82 }
83
84 fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
85 match self {
86 CopilotServer::Starting { .. } => anyhow::bail!("copilot is still starting"),
87 CopilotServer::Disabled => anyhow::bail!("copilot is disabled"),
88 CopilotServer::Error(error) => {
89 anyhow::bail!("copilot was not started because of an error: {error}")
90 }
91 CopilotServer::Running(server) => Ok(server),
92 }
93 }
94}
95
96struct RunningCopilotServer {
97 lsp: Arc<LanguageServer>,
98 sign_in_status: SignInStatus,
99 registered_buffers: HashMap<EntityId, RegisteredBuffer>,
100}
101
102#[derive(Clone, Debug)]
103enum SignInStatus {
104 Authorized,
105 Unauthorized,
106 SigningIn {
107 prompt: Option<request::PromptUserDeviceFlow>,
108 task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
109 },
110 SignedOut {
111 awaiting_signing_in: bool,
112 },
113}
114
115#[derive(Debug, Clone)]
116pub enum Status {
117 Starting {
118 task: Shared<Task<()>>,
119 },
120 Error(Arc<str>),
121 Disabled,
122 SignedOut {
123 awaiting_signing_in: bool,
124 },
125 SigningIn {
126 prompt: Option<request::PromptUserDeviceFlow>,
127 },
128 Unauthorized,
129 Authorized,
130}
131
132impl Status {
133 pub fn is_authorized(&self) -> bool {
134 matches!(self, Status::Authorized)
135 }
136
137 pub fn is_configured(&self) -> bool {
138 matches!(
139 self,
140 Status::Starting { .. }
141 | Status::Error(_)
142 | Status::SigningIn { .. }
143 | Status::Authorized
144 )
145 }
146}
147
148struct RegisteredBuffer {
149 uri: lsp::Uri,
150 language_id: String,
151 snapshot: BufferSnapshot,
152 snapshot_version: i32,
153 _subscriptions: [gpui::Subscription; 2],
154 pending_buffer_change: Task<Option<()>>,
155}
156
157impl RegisteredBuffer {
158 fn report_changes(
159 &mut self,
160 buffer: &Entity<Buffer>,
161 cx: &mut Context<Copilot>,
162 ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
163 let (done_tx, done_rx) = oneshot::channel();
164
165 if buffer.read(cx).version() == self.snapshot.version {
166 let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
167 } else {
168 let buffer = buffer.downgrade();
169 let id = buffer.entity_id();
170 let prev_pending_change =
171 mem::replace(&mut self.pending_buffer_change, Task::ready(None));
172 self.pending_buffer_change = cx.spawn(async move |copilot, cx| {
173 prev_pending_change.await;
174
175 let old_version = copilot
176 .update(cx, |copilot, _| {
177 let server = copilot.server.as_authenticated().log_err()?;
178 let buffer = server.registered_buffers.get_mut(&id)?;
179 Some(buffer.snapshot.version.clone())
180 })
181 .ok()??;
182 let new_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()).ok()?;
183
184 let content_changes = cx
185 .background_spawn({
186 let new_snapshot = new_snapshot.clone();
187 async move {
188 new_snapshot
189 .edits_since::<Dimensions<PointUtf16, usize>>(&old_version)
190 .map(|edit| {
191 let edit_start = edit.new.start.0;
192 let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
193 let new_text = new_snapshot
194 .text_for_range(edit.new.start.1..edit.new.end.1)
195 .collect();
196 lsp::TextDocumentContentChangeEvent {
197 range: Some(lsp::Range::new(
198 point_to_lsp(edit_start),
199 point_to_lsp(edit_end),
200 )),
201 range_length: None,
202 text: new_text,
203 }
204 })
205 .collect::<Vec<_>>()
206 }
207 })
208 .await;
209
210 copilot
211 .update(cx, |copilot, _| {
212 let server = copilot.server.as_authenticated().log_err()?;
213 let buffer = server.registered_buffers.get_mut(&id)?;
214 if !content_changes.is_empty() {
215 buffer.snapshot_version += 1;
216 buffer.snapshot = new_snapshot;
217 server
218 .lsp
219 .notify::<lsp::notification::DidChangeTextDocument>(
220 lsp::DidChangeTextDocumentParams {
221 text_document: lsp::VersionedTextDocumentIdentifier::new(
222 buffer.uri.clone(),
223 buffer.snapshot_version,
224 ),
225 content_changes,
226 },
227 )
228 .ok();
229 }
230 let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
231 Some(())
232 })
233 .ok()?;
234
235 Some(())
236 });
237 }
238
239 done_rx
240 }
241}
242
243#[derive(Debug)]
244pub struct Completion {
245 pub uuid: String,
246 pub range: Range<Anchor>,
247 pub text: String,
248}
249
250pub struct Copilot {
251 fs: Arc<dyn Fs>,
252 node_runtime: NodeRuntime,
253 server: CopilotServer,
254 buffers: HashSet<WeakEntity<Buffer>>,
255 server_id: LanguageServerId,
256 _subscriptions: Vec<Subscription>,
257}
258
259pub enum Event {
260 CopilotAuthSignedIn,
261 CopilotAuthSignedOut,
262}
263
264impl EventEmitter<Event> for Copilot {}
265
266#[derive(Clone)]
267pub struct GlobalCopilotAuth(pub Entity<Copilot>);
268
269impl GlobalCopilotAuth {
270 pub fn set_global(
271 server_id: LanguageServerId,
272 fs: Arc<dyn Fs>,
273 node_runtime: NodeRuntime,
274 cx: &mut App,
275 ) -> GlobalCopilotAuth {
276 let auth =
277 GlobalCopilotAuth(cx.new(|cx| Copilot::new(None, server_id, fs, node_runtime, cx)));
278 cx.set_global(auth.clone());
279 auth
280 }
281 pub fn try_global(cx: &mut App) -> Option<&GlobalCopilotAuth> {
282 cx.try_global()
283 }
284
285 pub fn try_get_or_init(app_state: Arc<AppState>, cx: &mut App) -> Option<GlobalCopilotAuth> {
286 let ai_enabled = !DisableAiSettings::get(None, cx).disable_ai;
287
288 if let Some(copilot) = cx.try_global::<Self>().cloned() {
289 if ai_enabled {
290 Some(copilot)
291 } else {
292 cx.remove_global::<Self>();
293 None
294 }
295 } else if ai_enabled {
296 Some(Self::set_global(
297 app_state.languages.next_language_server_id(),
298 app_state.fs.clone(),
299 app_state.node_runtime.clone(),
300 cx,
301 ))
302 } else {
303 None
304 }
305 }
306}
307impl Global for GlobalCopilotAuth {}
308
309#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub(crate) enum CompletionSource {
311 NextEditSuggestion,
312 InlineCompletion,
313}
314
315/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
316#[derive(Clone)]
317pub(crate) struct CopilotEditPrediction {
318 pub(crate) buffer: Entity<Buffer>,
319 pub(crate) range: Range<Anchor>,
320 pub(crate) text: String,
321 pub(crate) command: Option<lsp::Command>,
322 pub(crate) snapshot: BufferSnapshot,
323 pub(crate) source: CompletionSource,
324}
325
326impl Copilot {
327 pub fn new(
328 project: Option<Entity<Project>>,
329 new_server_id: LanguageServerId,
330 fs: Arc<dyn Fs>,
331 node_runtime: NodeRuntime,
332 cx: &mut Context<Self>,
333 ) -> Self {
334 let send_focus_notification = project.map(|project| {
335 cx.subscribe(&project, |this, project, e: &project::Event, cx| {
336 if let project::Event::ActiveEntryChanged(new_entry) = e
337 && let Ok(running) = this.server.as_authenticated()
338 {
339 let uri = new_entry
340 .and_then(|id| project.read(cx).path_for_entry(id, cx))
341 .and_then(|entry| project.read(cx).absolute_path(&entry, cx))
342 .and_then(|abs_path| lsp::Uri::from_file_path(abs_path).ok());
343
344 _ = running.lsp.notify::<DidFocus>(DidFocusParams { uri });
345 }
346 })
347 });
348 let global_authentication_events =
349 cx.try_global::<GlobalCopilotAuth>().cloned().map(|auth| {
350 cx.subscribe(&auth.0, |_, _, _: &Event, cx| {
351 let request_timeout = ProjectSettings::get_global(cx)
352 .global_lsp_settings
353 .get_request_timeout();
354 cx.spawn(async move |this, cx| {
355 let Some(server) = this
356 .update(cx, |this, _| this.language_server().cloned())
357 .ok()
358 .flatten()
359 else {
360 return;
361 };
362 let status = server
363 .request::<request::CheckStatus>(
364 request::CheckStatusParams {
365 local_checks_only: false,
366 },
367 request_timeout,
368 )
369 .await
370 .into_response()
371 .ok();
372 if let Some(status) = status {
373 this.update(cx, |copilot, cx| {
374 copilot.update_sign_in_status(status, cx);
375 })
376 .ok();
377 }
378 })
379 .detach()
380 })
381 });
382 let _subscriptions = std::iter::once(cx.on_app_quit(Self::shutdown_language_server))
383 .chain(send_focus_notification)
384 .chain(global_authentication_events)
385 .collect();
386 let mut this = Self {
387 server_id: new_server_id,
388 fs,
389 node_runtime,
390 server: CopilotServer::Disabled,
391 buffers: Default::default(),
392 _subscriptions,
393 };
394 this.start_copilot(true, false, cx);
395 cx.observe_global::<SettingsStore>(move |this, cx| {
396 let ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
397
398 if ai_disabled {
399 // Stop the server if AI is disabled
400 if !matches!(this.server, CopilotServer::Disabled) {
401 let shutdown = match mem::replace(&mut this.server, CopilotServer::Disabled) {
402 CopilotServer::Running(server) => {
403 let shutdown_future = server.lsp.shutdown();
404 Some(cx.background_spawn(async move {
405 if let Some(fut) = shutdown_future {
406 fut.await;
407 }
408 }))
409 }
410 _ => None,
411 };
412 if let Some(task) = shutdown {
413 task.detach();
414 }
415 cx.notify();
416 }
417 } else {
418 // Only start if AI is enabled
419 this.start_copilot(true, false, cx);
420 if let Ok(server) = this.server.as_running() {
421 notify_did_change_config_to_server(&server.lsp, cx)
422 .context("copilot setting change: did change configuration")
423 .log_err();
424 }
425 }
426 this.update_action_visibilities(cx);
427 })
428 .detach();
429 cx.observe_self(|copilot, cx| {
430 copilot.update_action_visibilities(cx);
431 })
432 .detach();
433 this
434 }
435
436 fn shutdown_language_server(
437 &mut self,
438 _cx: &mut Context<Self>,
439 ) -> impl Future<Output = ()> + use<> {
440 let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) {
441 CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })),
442 _ => None,
443 };
444
445 async move {
446 if let Some(shutdown) = shutdown {
447 shutdown.await;
448 }
449 }
450 }
451
452 pub fn start_copilot(
453 &mut self,
454 check_edit_prediction_provider: bool,
455 awaiting_sign_in_after_start: bool,
456 cx: &mut Context<Self>,
457 ) {
458 if DisableAiSettings::get_global(cx).disable_ai {
459 return;
460 }
461 if !matches!(self.server, CopilotServer::Disabled) {
462 return;
463 }
464 let language_settings = all_language_settings(None, cx);
465 if check_edit_prediction_provider
466 && language_settings.edit_predictions.provider != EditPredictionProvider::Copilot
467 {
468 return;
469 }
470 let server_id = self.server_id;
471 let fs = self.fs.clone();
472 let node_runtime = self.node_runtime.clone();
473 let env = self.build_env(&language_settings.edit_predictions.copilot);
474 let start_task = cx
475 .spawn(async move |this, cx| {
476 Self::start_language_server(
477 server_id,
478 fs,
479 node_runtime,
480 env,
481 this,
482 awaiting_sign_in_after_start,
483 cx,
484 )
485 .await
486 })
487 .shared();
488 self.server = CopilotServer::Starting { task: start_task };
489 cx.notify();
490 }
491
492 fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
493 let proxy_url = copilot_settings.proxy.clone()?;
494 let no_verify = copilot_settings.proxy_no_verify;
495 let http_or_https_proxy = if proxy_url.starts_with("http:") {
496 Some("HTTP_PROXY")
497 } else if proxy_url.starts_with("https:") {
498 Some("HTTPS_PROXY")
499 } else {
500 log::error!(
501 "Unsupported protocol scheme for language server proxy (must be http or https)"
502 );
503 None
504 };
505
506 let mut env = HashMap::default();
507
508 if let Some(proxy_type) = http_or_https_proxy {
509 env.insert(proxy_type.to_string(), proxy_url);
510 if let Some(true) = no_verify {
511 env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
512 };
513 }
514
515 if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {
516 env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
517 }
518
519 if env.is_empty() { None } else { Some(env) }
520 }
521
522 #[cfg(any(test, feature = "test-support"))]
523 pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
524 use fs::FakeFs;
525 use gpui::Subscription;
526 use lsp::FakeLanguageServer;
527 use node_runtime::NodeRuntime;
528
529 let (server, fake_server) = FakeLanguageServer::new(
530 LanguageServerId(0),
531 LanguageServerBinary {
532 path: "path/to/copilot".into(),
533 arguments: vec![],
534 env: None,
535 },
536 "copilot".into(),
537 Default::default(),
538 &mut cx.to_async(),
539 );
540 let node_runtime = NodeRuntime::unavailable();
541 let send_focus_notification = Subscription::new(|| {});
542 let this = cx.new(|cx| Self {
543 server_id: LanguageServerId(0),
544 fs: FakeFs::new(cx.background_executor().clone()),
545 node_runtime,
546 server: CopilotServer::Running(RunningCopilotServer {
547 lsp: Arc::new(server),
548 sign_in_status: SignInStatus::Authorized,
549 registered_buffers: Default::default(),
550 }),
551 _subscriptions: vec![
552 send_focus_notification,
553 cx.on_app_quit(Self::shutdown_language_server),
554 ],
555 buffers: Default::default(),
556 });
557 (this, fake_server)
558 }
559
560 async fn start_language_server(
561 new_server_id: LanguageServerId,
562 fs: Arc<dyn Fs>,
563 node_runtime: NodeRuntime,
564 env: Option<HashMap<String, String>>,
565 this: WeakEntity<Self>,
566 awaiting_sign_in_after_start: bool,
567 cx: &mut AsyncApp,
568 ) {
569 let start_language_server = async {
570 let server_path = get_copilot_lsp(fs, node_runtime.clone()).await?;
571 let node_path = node_runtime.binary_path().await?;
572 ensure_node_version_for_copilot(&node_path).await?;
573
574 let arguments: Vec<OsString> = vec![
575 "--experimental-sqlite".into(),
576 server_path.into(),
577 "--stdio".into(),
578 ];
579 let binary = LanguageServerBinary {
580 path: node_path,
581 arguments,
582 env,
583 };
584
585 let root_path = if cfg!(target_os = "windows") {
586 Path::new("C:/")
587 } else {
588 Path::new("/")
589 };
590
591 let server_name = LanguageServerName("copilot".into());
592 let server = LanguageServer::new(
593 Arc::new(Mutex::new(None)),
594 new_server_id,
595 server_name,
596 binary,
597 root_path,
598 None,
599 Default::default(),
600 cx,
601 )?;
602
603 server
604 .on_notification::<DidChangeStatus, _>({
605 let this = this.clone();
606 move |params, cx| {
607 if params.kind == request::StatusKind::Normal {
608 let this = this.clone();
609 cx.spawn(async move |cx| {
610 let lsp = this
611 .read_with(cx, |copilot, _| {
612 if let CopilotServer::Running(server) = &copilot.server {
613 Some(server.lsp.clone())
614 } else {
615 None
616 }
617 })
618 .ok()
619 .flatten();
620 let Some(lsp) = lsp else { return };
621 let request_timeout = cx.update(|cx| {
622 ProjectSettings::get_global(cx)
623 .global_lsp_settings
624 .get_request_timeout()
625 });
626 let status = lsp
627 .request::<request::CheckStatus>(
628 request::CheckStatusParams {
629 local_checks_only: false,
630 },
631 request_timeout,
632 )
633 .await
634 .into_response()
635 .ok();
636 if let Some(status) = status {
637 this.update(cx, |copilot, cx| {
638 copilot.update_sign_in_status(status, cx);
639 })
640 .ok();
641 }
642 })
643 .detach();
644 }
645 }
646 })
647 .detach();
648
649 server
650 .on_request::<lsp::request::ShowDocument, _, _>(move |params, cx| {
651 if params.external.unwrap_or(false) {
652 let url = params.uri.to_string();
653 cx.update(|cx| cx.open_url(&url));
654 }
655 async move { Ok(lsp::ShowDocumentResult { success: true }) }
656 })
657 .detach();
658
659 let configuration = lsp::DidChangeConfigurationParams {
660 settings: Default::default(),
661 };
662
663 let editor_info = request::SetEditorInfoParams {
664 editor_info: request::EditorInfo {
665 name: "zed".into(),
666 version: env!("CARGO_PKG_VERSION").into(),
667 },
668 editor_plugin_info: request::EditorPluginInfo {
669 name: "zed-copilot".into(),
670 version: "0.0.1".into(),
671 },
672 };
673 let editor_info_json = serde_json::to_value(&editor_info)?;
674
675 let request_timeout = cx.update(|app| {
676 ProjectSettings::get_global(app)
677 .global_lsp_settings
678 .get_request_timeout()
679 });
680
681 let server = cx
682 .update(|cx| {
683 let mut params = server.default_initialize_params(false, false, cx);
684 params.initialization_options = Some(editor_info_json);
685 params
686 .capabilities
687 .window
688 .get_or_insert_with(Default::default)
689 .show_document =
690 Some(lsp::ShowDocumentClientCapabilities { support: true });
691 server.initialize(params, configuration.into(), request_timeout, cx)
692 })
693 .await?;
694
695 this.update(cx, |_, cx| notify_did_change_config_to_server(&server, cx))?
696 .context("copilot: did change configuration")?;
697
698 let status = server
699 .request::<request::CheckStatus>(
700 request::CheckStatusParams {
701 local_checks_only: false,
702 },
703 request_timeout,
704 )
705 .await
706 .into_response()
707 .context("copilot: check status")?;
708
709 anyhow::Ok((server, status))
710 };
711
712 let server = start_language_server.await;
713 this.update(cx, |this, cx| {
714 cx.notify();
715
716 if env::var("ZED_FORCE_COPILOT_ERROR").is_ok() {
717 this.server = CopilotServer::Error(
718 "Forced error for testing (ZED_FORCE_COPILOT_ERROR)".into(),
719 );
720 return;
721 }
722
723 match server {
724 Ok((server, status)) => {
725 this.server = CopilotServer::Running(RunningCopilotServer {
726 lsp: server,
727 sign_in_status: SignInStatus::SignedOut {
728 awaiting_signing_in: awaiting_sign_in_after_start,
729 },
730 registered_buffers: Default::default(),
731 });
732 this.update_sign_in_status(status, cx);
733 }
734 Err(error) => {
735 this.server = CopilotServer::Error(error.to_string().into());
736 cx.notify()
737 }
738 }
739 })
740 .ok();
741 }
742
743 pub fn is_authenticated(&self) -> bool {
744 return matches!(
745 self.server,
746 CopilotServer::Running(RunningCopilotServer {
747 sign_in_status: SignInStatus::Authorized,
748 ..
749 })
750 );
751 }
752
753 pub fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
754 if let CopilotServer::Running(server) = &mut self.server {
755 let task = match &server.sign_in_status {
756 SignInStatus::Authorized => Task::ready(Ok(())).shared(),
757 SignInStatus::SigningIn { task, .. } => {
758 cx.notify();
759 task.clone()
760 }
761 SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => {
762 let lsp = server.lsp.clone();
763
764 let request_timeout = ProjectSettings::get_global(cx)
765 .global_lsp_settings
766 .get_request_timeout();
767
768 let task = cx
769 .spawn(async move |this, cx| {
770 let sign_in = async {
771 let flow = lsp
772 .request::<request::SignIn>(
773 request::SignInParams {},
774 request_timeout,
775 )
776 .await
777 .into_response()
778 .context("copilot sign-in")?;
779
780 this.update(cx, |this, cx| {
781 if let CopilotServer::Running(RunningCopilotServer {
782 sign_in_status: status,
783 ..
784 }) = &mut this.server
785 && let SignInStatus::SigningIn {
786 prompt: prompt_flow,
787 ..
788 } = status
789 {
790 *prompt_flow = Some(flow.clone());
791 cx.notify();
792 }
793 })?;
794
795 anyhow::Ok(())
796 };
797
798 let sign_in = sign_in.await;
799 this.update(cx, |this, cx| match sign_in {
800 Ok(()) => Ok(()),
801 Err(error) => {
802 this.update_sign_in_status(
803 request::SignInStatus::NotSignedIn,
804 cx,
805 );
806 Err(Arc::new(error))
807 }
808 })?
809 })
810 .shared();
811 server.sign_in_status = SignInStatus::SigningIn {
812 prompt: None,
813 task: task.clone(),
814 };
815 cx.notify();
816 task
817 }
818 };
819
820 cx.background_spawn(task.map_err(|err| anyhow!("{err:?}")))
821 } else {
822 // If we're downloading, wait until download is finished
823 // If we're in a stuck state, display to the user
824 Task::ready(Err(anyhow!("copilot hasn't started yet")))
825 }
826 }
827
828 pub fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
829 self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
830 match &self.server {
831 CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => {
832 let request_timeout = ProjectSettings::get_global(cx)
833 .global_lsp_settings
834 .get_request_timeout();
835
836 let server = server.clone();
837 cx.background_spawn(async move {
838 server
839 .request::<request::SignOut>(request::SignOutParams {}, request_timeout)
840 .await
841 .into_response()
842 .context("copilot: sign in confirm")?;
843 anyhow::Ok(())
844 })
845 }
846 CopilotServer::Disabled => cx.background_spawn(async {
847 clear_copilot_config_dir().await;
848 anyhow::Ok(())
849 }),
850 _ => Task::ready(Err(anyhow!("copilot hasn't started yet"))),
851 }
852 }
853
854 pub fn reinstall(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
855 let language_settings = all_language_settings(None, cx);
856 let env = self.build_env(&language_settings.edit_predictions.copilot);
857 let start_task = cx
858 .spawn({
859 let fs = self.fs.clone();
860 let node_runtime = self.node_runtime.clone();
861 let server_id = self.server_id;
862 async move |this, cx| {
863 clear_copilot_dir().await;
864 Self::start_language_server(server_id, fs, node_runtime, env, this, false, cx)
865 .await
866 }
867 })
868 .shared();
869
870 self.server = CopilotServer::Starting {
871 task: start_task.clone(),
872 };
873
874 cx.notify();
875
876 start_task
877 }
878
879 pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {
880 if let CopilotServer::Running(server) = &self.server {
881 Some(&server.lsp)
882 } else {
883 None
884 }
885 }
886
887 pub fn register_buffer(&mut self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) {
888 let weak_buffer = buffer.downgrade();
889 self.buffers.insert(weak_buffer.clone());
890
891 if let CopilotServer::Running(RunningCopilotServer {
892 lsp: server,
893 sign_in_status: status,
894 registered_buffers,
895 ..
896 }) = &mut self.server
897 {
898 if !matches!(status, SignInStatus::Authorized) {
899 return;
900 }
901
902 let entry = registered_buffers.entry(buffer.entity_id());
903 if let Entry::Vacant(e) = entry {
904 let Ok(uri) = uri_for_buffer(buffer, cx) else {
905 return;
906 };
907 let language_id = id_for_language(buffer.read(cx).language());
908 let snapshot = buffer.read(cx).snapshot();
909 server
910 .notify::<lsp::notification::DidOpenTextDocument>(
911 lsp::DidOpenTextDocumentParams {
912 text_document: lsp::TextDocumentItem {
913 uri: uri.clone(),
914 language_id: language_id.clone(),
915 version: 0,
916 text: snapshot.text(),
917 },
918 },
919 )
920 .ok();
921
922 e.insert(RegisteredBuffer {
923 uri,
924 language_id,
925 snapshot,
926 snapshot_version: 0,
927 pending_buffer_change: Task::ready(Some(())),
928 _subscriptions: [
929 cx.subscribe(buffer, |this, buffer, event, cx| {
930 this.handle_buffer_event(buffer, event, cx).log_err();
931 }),
932 cx.observe_release(buffer, move |this, _buffer, _cx| {
933 this.buffers.remove(&weak_buffer);
934 this.unregister_buffer(&weak_buffer);
935 }),
936 ],
937 });
938 }
939 }
940 }
941
942 fn handle_buffer_event(
943 &mut self,
944 buffer: Entity<Buffer>,
945 event: &language::BufferEvent,
946 cx: &mut Context<Self>,
947 ) -> Result<()> {
948 if let Ok(server) = self.server.as_running()
949 && let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
950 {
951 match event {
952 language::BufferEvent::Edited => {
953 drop(registered_buffer.report_changes(&buffer, cx));
954 }
955 language::BufferEvent::Saved => {
956 server
957 .lsp
958 .notify::<lsp::notification::DidSaveTextDocument>(
959 lsp::DidSaveTextDocumentParams {
960 text_document: lsp::TextDocumentIdentifier::new(
961 registered_buffer.uri.clone(),
962 ),
963 text: None,
964 },
965 )
966 .ok();
967 }
968 language::BufferEvent::FileHandleChanged
969 | language::BufferEvent::LanguageChanged(_) => {
970 let new_language_id = id_for_language(buffer.read(cx).language());
971 let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
972 return Ok(());
973 };
974 if new_uri != registered_buffer.uri
975 || new_language_id != registered_buffer.language_id
976 {
977 let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
978 registered_buffer.language_id = new_language_id;
979 server
980 .lsp
981 .notify::<lsp::notification::DidCloseTextDocument>(
982 lsp::DidCloseTextDocumentParams {
983 text_document: lsp::TextDocumentIdentifier::new(old_uri),
984 },
985 )
986 .ok();
987 server
988 .lsp
989 .notify::<lsp::notification::DidOpenTextDocument>(
990 lsp::DidOpenTextDocumentParams {
991 text_document: lsp::TextDocumentItem::new(
992 registered_buffer.uri.clone(),
993 registered_buffer.language_id.clone(),
994 registered_buffer.snapshot_version,
995 registered_buffer.snapshot.text(),
996 ),
997 },
998 )
999 .ok();
1000 }
1001 }
1002 _ => {}
1003 }
1004 }
1005
1006 Ok(())
1007 }
1008
1009 fn unregister_buffer(&mut self, buffer: &WeakEntity<Buffer>) {
1010 if let Ok(server) = self.server.as_running()
1011 && let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id())
1012 {
1013 server
1014 .lsp
1015 .notify::<lsp::notification::DidCloseTextDocument>(
1016 lsp::DidCloseTextDocumentParams {
1017 text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
1018 },
1019 )
1020 .ok();
1021 }
1022 }
1023
1024 pub(crate) fn completions(
1025 &mut self,
1026 buffer: &Entity<Buffer>,
1027 position: Anchor,
1028 cx: &mut Context<Self>,
1029 ) -> Task<Result<Vec<CopilotEditPrediction>>> {
1030 self.register_buffer(buffer, cx);
1031
1032 let server = match self.server.as_authenticated() {
1033 Ok(server) => server,
1034 Err(error) => return Task::ready(Err(error)),
1035 };
1036 let buffer_entity = buffer.clone();
1037 let lsp = server.lsp.clone();
1038 let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) else {
1039 return Task::ready(Err(anyhow::anyhow!("buffer not registered")));
1040 };
1041 let pending_snapshot = registered_buffer.report_changes(buffer, cx);
1042 let buffer = buffer.read(cx);
1043 let uri = registered_buffer.uri.clone();
1044 let position = position.to_point_utf16(buffer);
1045 let snapshot = buffer.snapshot();
1046 let settings = snapshot.settings_at(0, cx);
1047 let tab_size = settings.tab_size.get();
1048 let hard_tabs = settings.hard_tabs;
1049 drop(settings);
1050
1051 let request_timeout = ProjectSettings::get_global(cx)
1052 .global_lsp_settings
1053 .get_request_timeout();
1054
1055 let nes_enabled = AllLanguageSettings::get_global(cx)
1056 .edit_predictions
1057 .copilot
1058 .enable_next_edit_suggestions
1059 .unwrap_or(true);
1060
1061 cx.background_spawn(async move {
1062 let (version, snapshot) = pending_snapshot.await?;
1063 let lsp_position = point_to_lsp(position);
1064
1065 let nes_fut = if nes_enabled {
1066 lsp.request::<NextEditSuggestions>(
1067 request::NextEditSuggestionsParams {
1068 text_document: lsp::VersionedTextDocumentIdentifier {
1069 uri: uri.clone(),
1070 version,
1071 },
1072 position: lsp_position,
1073 },
1074 request_timeout,
1075 )
1076 .map(|resp| {
1077 resp.into_response()
1078 .ok()
1079 .map(|result| {
1080 result
1081 .edits
1082 .into_iter()
1083 .map(|completion| {
1084 let start = snapshot.clip_point_utf16(
1085 point_from_lsp(completion.range.start),
1086 Bias::Left,
1087 );
1088 let end = snapshot.clip_point_utf16(
1089 point_from_lsp(completion.range.end),
1090 Bias::Left,
1091 );
1092 CopilotEditPrediction {
1093 buffer: buffer_entity.clone(),
1094 range: snapshot.anchor_before(start)
1095 ..snapshot.anchor_after(end),
1096 text: completion.text,
1097 command: completion.command,
1098 snapshot: snapshot.clone(),
1099 source: CompletionSource::NextEditSuggestion,
1100 }
1101 })
1102 .collect::<Vec<_>>()
1103 })
1104 .unwrap_or_default()
1105 })
1106 .left_future()
1107 .fuse()
1108 } else {
1109 future::ready(Vec::<CopilotEditPrediction>::new())
1110 .right_future()
1111 .fuse()
1112 };
1113
1114 let inline_fut = lsp
1115 .request::<InlineCompletions>(
1116 request::InlineCompletionsParams {
1117 text_document: lsp::VersionedTextDocumentIdentifier {
1118 uri: uri.clone(),
1119 version,
1120 },
1121 position: lsp_position,
1122 context: InlineCompletionContext {
1123 trigger_kind: InlineCompletionTriggerKind::Automatic,
1124 },
1125 formatting_options: Some(FormattingOptions {
1126 tab_size,
1127 insert_spaces: !hard_tabs,
1128 }),
1129 },
1130 request_timeout,
1131 )
1132 .map(|resp| {
1133 resp.into_response()
1134 .ok()
1135 .map(|result| {
1136 result
1137 .items
1138 .into_iter()
1139 .map(|item| {
1140 let start = snapshot.clip_point_utf16(
1141 point_from_lsp(item.range.start),
1142 Bias::Left,
1143 );
1144 let end = snapshot.clip_point_utf16(
1145 point_from_lsp(item.range.end),
1146 Bias::Left,
1147 );
1148 CopilotEditPrediction {
1149 buffer: buffer_entity.clone(),
1150 range: snapshot.anchor_before(start)
1151 ..snapshot.anchor_after(end),
1152 text: item.insert_text,
1153 command: item.command,
1154 snapshot: snapshot.clone(),
1155 source: CompletionSource::InlineCompletion,
1156 }
1157 })
1158 .collect::<Vec<_>>()
1159 })
1160 .unwrap_or_default()
1161 })
1162 .fuse();
1163
1164 futures::pin_mut!(nes_fut, inline_fut);
1165
1166 let mut nes_result: Option<Vec<CopilotEditPrediction>> = None;
1167 let mut inline_result: Option<Vec<CopilotEditPrediction>> = None;
1168
1169 loop {
1170 select_biased! {
1171 nes = nes_fut => {
1172 if !nes.is_empty() {
1173 return Ok(nes);
1174 }
1175 nes_result = Some(nes);
1176 }
1177 inline = inline_fut => {
1178 if !inline.is_empty() {
1179 return Ok(inline);
1180 }
1181 inline_result = Some(inline);
1182 }
1183 complete => break,
1184 }
1185
1186 if let (Some(nes), Some(inline)) = (&nes_result, &inline_result) {
1187 return if !nes.is_empty() {
1188 Ok(nes.clone())
1189 } else {
1190 Ok(inline.clone())
1191 };
1192 }
1193 }
1194
1195 Ok(nes_result.or(inline_result).unwrap_or_default())
1196 })
1197 }
1198
1199 pub(crate) fn accept_completion(
1200 &mut self,
1201 completion: &CopilotEditPrediction,
1202 cx: &mut Context<Self>,
1203 ) -> Task<Result<()>> {
1204 let server = match self.server.as_authenticated() {
1205 Ok(server) => server,
1206 Err(error) => return Task::ready(Err(error)),
1207 };
1208 if let Some(command) = &completion.command {
1209 let request_timeout = ProjectSettings::get_global(cx)
1210 .global_lsp_settings
1211 .get_request_timeout();
1212
1213 let request = server.lsp.request::<lsp::ExecuteCommand>(
1214 lsp::ExecuteCommandParams {
1215 command: command.command.clone(),
1216 arguments: command.arguments.clone().unwrap_or_default(),
1217 ..Default::default()
1218 },
1219 request_timeout,
1220 );
1221 cx.background_spawn(async move {
1222 request
1223 .await
1224 .into_response()
1225 .context("copilot: notify accepted")?;
1226 Ok(())
1227 })
1228 } else {
1229 Task::ready(Ok(()))
1230 }
1231 }
1232
1233 pub fn status(&self) -> Status {
1234 match &self.server {
1235 CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
1236 CopilotServer::Disabled => Status::Disabled,
1237 CopilotServer::Error(error) => Status::Error(error.clone()),
1238 CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
1239 match sign_in_status {
1240 SignInStatus::Authorized => Status::Authorized,
1241 SignInStatus::Unauthorized => Status::Unauthorized,
1242 SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
1243 prompt: prompt.clone(),
1244 },
1245 SignInStatus::SignedOut {
1246 awaiting_signing_in,
1247 } => Status::SignedOut {
1248 awaiting_signing_in: *awaiting_signing_in,
1249 },
1250 }
1251 }
1252 }
1253 }
1254
1255 pub fn update_sign_in_status(
1256 &mut self,
1257 lsp_status: request::SignInStatus,
1258 cx: &mut Context<Self>,
1259 ) {
1260 self.buffers.retain(|buffer| buffer.is_upgradable());
1261
1262 if let Ok(server) = self.server.as_running() {
1263 match lsp_status {
1264 request::SignInStatus::Ok { user: Some(_) }
1265 | request::SignInStatus::MaybeOk { .. }
1266 | request::SignInStatus::AlreadySignedIn { .. } => {
1267 server.sign_in_status = SignInStatus::Authorized;
1268 cx.emit(Event::CopilotAuthSignedIn);
1269 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1270 if let Some(buffer) = buffer.upgrade() {
1271 self.register_buffer(&buffer, cx);
1272 }
1273 }
1274 }
1275 request::SignInStatus::NotAuthorized { .. } => {
1276 server.sign_in_status = SignInStatus::Unauthorized;
1277 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1278 self.unregister_buffer(&buffer);
1279 }
1280 }
1281 request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => {
1282 if !matches!(server.sign_in_status, SignInStatus::SignedOut { .. }) {
1283 server.sign_in_status = SignInStatus::SignedOut {
1284 awaiting_signing_in: false,
1285 };
1286 }
1287 cx.emit(Event::CopilotAuthSignedOut);
1288 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1289 self.unregister_buffer(&buffer);
1290 }
1291 }
1292 }
1293
1294 cx.notify();
1295 }
1296 }
1297
1298 fn update_action_visibilities(&self, cx: &mut App) {
1299 let signed_in_actions = [
1300 TypeId::of::<Suggest>(),
1301 TypeId::of::<NextSuggestion>(),
1302 TypeId::of::<PreviousSuggestion>(),
1303 TypeId::of::<Reinstall>(),
1304 ];
1305 let auth_actions = [TypeId::of::<SignOut>()];
1306 let no_auth_actions = [TypeId::of::<SignIn>()];
1307 let status = self.status();
1308
1309 let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
1310 let filter = CommandPaletteFilter::global_mut(cx);
1311
1312 if is_ai_disabled {
1313 filter.hide_action_types(&signed_in_actions);
1314 filter.hide_action_types(&auth_actions);
1315 filter.hide_action_types(&no_auth_actions);
1316 } else {
1317 match status {
1318 Status::Disabled => {
1319 filter.hide_action_types(&signed_in_actions);
1320 filter.hide_action_types(&auth_actions);
1321 filter.hide_action_types(&no_auth_actions);
1322 }
1323 Status::Authorized => {
1324 filter.hide_action_types(&no_auth_actions);
1325 filter.show_action_types(signed_in_actions.iter().chain(&auth_actions));
1326 }
1327 _ => {
1328 filter.hide_action_types(&signed_in_actions);
1329 filter.hide_action_types(&auth_actions);
1330 filter.show_action_types(&no_auth_actions);
1331 }
1332 }
1333 }
1334 }
1335}
1336
1337fn id_for_language(language: Option<&Arc<Language>>) -> String {
1338 language
1339 .map(|language| language.lsp_id())
1340 .unwrap_or_else(|| "plaintext".to_string())
1341}
1342
1343fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Uri, ()> {
1344 if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
1345 lsp::Uri::from_file_path(file.abs_path(cx))
1346 } else {
1347 format!("buffer://{}", buffer.entity_id())
1348 .parse()
1349 .map_err(|_| ())
1350 }
1351}
1352
1353fn notify_did_change_config_to_server(
1354 server: &Arc<LanguageServer>,
1355 cx: &mut Context<Copilot>,
1356) -> std::result::Result<(), anyhow::Error> {
1357 let copilot_settings = all_language_settings(None, cx)
1358 .edit_predictions
1359 .copilot
1360 .clone();
1361
1362 if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
1363 copilot_chat.update(cx, |chat, cx| {
1364 chat.set_configuration(
1365 copilot_chat::CopilotChatConfiguration {
1366 enterprise_uri: copilot_settings.enterprise_uri.clone(),
1367 },
1368 cx,
1369 );
1370 });
1371 }
1372
1373 let settings = json!({
1374 "http": {
1375 "proxy": copilot_settings.proxy,
1376 "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
1377 },
1378 "github-enterprise": {
1379 "uri": copilot_settings.enterprise_uri
1380 }
1381 });
1382
1383 server
1384 .notify::<lsp::notification::DidChangeConfiguration>(lsp::DidChangeConfigurationParams {
1385 settings,
1386 })
1387 .ok();
1388 Ok(())
1389}
1390
1391async fn clear_copilot_dir() {
1392 remove_matching(paths::copilot_dir(), |_| true).await
1393}
1394
1395async fn clear_copilot_config_dir() {
1396 remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
1397}
1398
1399async fn ensure_node_version_for_copilot(node_path: &Path) -> anyhow::Result<()> {
1400 const MIN_COPILOT_NODE_VERSION: Version = Version::new(20, 8, 0);
1401
1402 log::info!("Checking Node.js version for Copilot at: {:?}", node_path);
1403
1404 let output = util::command::new_command(node_path)
1405 .arg("--version")
1406 .output()
1407 .await
1408 .with_context(|| format!("checking Node.js version at {:?}", node_path))?;
1409
1410 if !output.status.success() {
1411 anyhow::bail!(
1412 "failed to run node --version for Copilot. stdout: {}, stderr: {}",
1413 String::from_utf8_lossy(&output.stdout),
1414 String::from_utf8_lossy(&output.stderr),
1415 );
1416 }
1417
1418 let version_str = String::from_utf8_lossy(&output.stdout);
1419 let version = Version::parse(version_str.trim().trim_start_matches('v'))
1420 .with_context(|| format!("parsing Node.js version from '{}'", version_str.trim()))?;
1421
1422 if version < MIN_COPILOT_NODE_VERSION {
1423 anyhow::bail!(
1424 "GitHub Copilot language server requires Node.js {MIN_COPILOT_NODE_VERSION} or later, but found {version}. \
1425 Please update your Node.js version or configure a different Node.js path in settings."
1426 );
1427 }
1428
1429 log::info!(
1430 "Node.js version {} meets Copilot requirements (>= {})",
1431 version,
1432 MIN_COPILOT_NODE_VERSION
1433 );
1434 Ok(())
1435}
1436
1437async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::Result<PathBuf> {
1438 const PACKAGE_NAME: &str = "@github/copilot-language-server";
1439 const SERVER_PATH: &str =
1440 "node_modules/@github/copilot-language-server/dist/language-server.js";
1441
1442 let latest_version = node_runtime
1443 .npm_package_latest_version(PACKAGE_NAME)
1444 .await?;
1445 let server_path = paths::copilot_dir().join(SERVER_PATH);
1446
1447 fs.create_dir(paths::copilot_dir()).await?;
1448
1449 let should_install = node_runtime
1450 .should_install_npm_package(
1451 PACKAGE_NAME,
1452 &server_path,
1453 paths::copilot_dir(),
1454 VersionStrategy::Latest(&latest_version),
1455 )
1456 .await;
1457 if should_install {
1458 node_runtime
1459 .npm_install_packages(
1460 paths::copilot_dir(),
1461 &[(PACKAGE_NAME, &latest_version.to_string())],
1462 )
1463 .await?;
1464 }
1465
1466 Ok(server_path)
1467}
1468
1469#[cfg(test)]
1470mod tests {
1471 use super::*;
1472 use fs::FakeFs;
1473 use gpui::TestAppContext;
1474 use language::language_settings::AllLanguageSettings;
1475 use node_runtime::NodeRuntime;
1476 use settings::{Settings, SettingsStore};
1477 use util::{
1478 path,
1479 paths::PathStyle,
1480 rel_path::{RelPath, rel_path},
1481 };
1482
1483 #[gpui::test]
1484 async fn test_copilot_does_not_start_when_ai_disabled(cx: &mut TestAppContext) {
1485 cx.update(|cx| {
1486 let store = SettingsStore::test(cx);
1487 cx.set_global(store);
1488 DisableAiSettings::register(cx);
1489 AllLanguageSettings::register(cx);
1490
1491 // Set disable_ai to true before creating Copilot
1492 DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
1493 });
1494
1495 let copilot = cx.new(|cx| Copilot {
1496 server_id: LanguageServerId(0),
1497 fs: FakeFs::new(cx.background_executor().clone()),
1498 node_runtime: NodeRuntime::unavailable(),
1499 server: CopilotServer::Disabled,
1500 buffers: Default::default(),
1501 _subscriptions: vec![],
1502 });
1503
1504 // Try to start copilot - it should remain disabled
1505 copilot.update(cx, |copilot, cx| {
1506 copilot.start_copilot(false, false, cx);
1507 });
1508
1509 // Verify the server is still disabled
1510 copilot.read_with(cx, |copilot, _| {
1511 assert!(
1512 matches!(copilot.server, CopilotServer::Disabled),
1513 "Copilot should not start when disable_ai is true"
1514 );
1515 });
1516 }
1517
1518 #[gpui::test]
1519 async fn test_copilot_stops_when_ai_becomes_disabled(cx: &mut TestAppContext) {
1520 cx.update(|cx| {
1521 let store = SettingsStore::test(cx);
1522 cx.set_global(store);
1523 DisableAiSettings::register(cx);
1524 AllLanguageSettings::register(cx);
1525
1526 // AI is initially enabled
1527 DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
1528 });
1529
1530 // Create a fake Copilot that's already running, with the settings observer
1531 let (copilot, _lsp) = Copilot::fake(cx);
1532
1533 // Add the settings observer that handles disable_ai changes
1534 copilot.update(cx, |_, cx| {
1535 cx.observe_global::<SettingsStore>(move |this, cx| {
1536 let ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
1537
1538 if ai_disabled {
1539 if !matches!(this.server, CopilotServer::Disabled) {
1540 let shutdown = match mem::replace(&mut this.server, CopilotServer::Disabled)
1541 {
1542 CopilotServer::Running(server) => {
1543 let shutdown_future = server.lsp.shutdown();
1544 Some(cx.background_spawn(async move {
1545 if let Some(fut) = shutdown_future {
1546 fut.await;
1547 }
1548 }))
1549 }
1550 _ => None,
1551 };
1552 if let Some(task) = shutdown {
1553 task.detach();
1554 }
1555 cx.notify();
1556 }
1557 }
1558 })
1559 .detach();
1560 });
1561
1562 // Verify copilot is running
1563 copilot.read_with(cx, |copilot, _| {
1564 assert!(
1565 matches!(copilot.server, CopilotServer::Running(_)),
1566 "Copilot should be running initially"
1567 );
1568 });
1569
1570 // Now disable AI
1571 cx.update(|cx| {
1572 DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
1573 });
1574
1575 // The settings observer should have stopped the server
1576 cx.run_until_parked();
1577
1578 copilot.read_with(cx, |copilot, _| {
1579 assert!(
1580 matches!(copilot.server, CopilotServer::Disabled),
1581 "Copilot should be disabled after disable_ai is set to true"
1582 );
1583 });
1584 }
1585
1586 #[gpui::test(iterations = 10)]
1587 async fn test_buffer_management(cx: &mut TestAppContext) {
1588 init_test(cx);
1589 let (copilot, mut lsp) = Copilot::fake(cx);
1590
1591 let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx));
1592 let buffer_1_uri: lsp::Uri = format!("buffer://{}", buffer_1.entity_id().as_u64())
1593 .parse()
1594 .unwrap();
1595 copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
1596 assert_eq!(
1597 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1598 .await,
1599 lsp::DidOpenTextDocumentParams {
1600 text_document: lsp::TextDocumentItem::new(
1601 buffer_1_uri.clone(),
1602 "plaintext".into(),
1603 0,
1604 "Hello".into()
1605 ),
1606 }
1607 );
1608
1609 let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx));
1610 let buffer_2_uri: lsp::Uri = format!("buffer://{}", buffer_2.entity_id().as_u64())
1611 .parse()
1612 .unwrap();
1613 copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
1614 assert_eq!(
1615 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1616 .await,
1617 lsp::DidOpenTextDocumentParams {
1618 text_document: lsp::TextDocumentItem::new(
1619 buffer_2_uri.clone(),
1620 "plaintext".into(),
1621 0,
1622 "Goodbye".into()
1623 ),
1624 }
1625 );
1626
1627 buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
1628 assert_eq!(
1629 lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
1630 .await,
1631 lsp::DidChangeTextDocumentParams {
1632 text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
1633 content_changes: vec![lsp::TextDocumentContentChangeEvent {
1634 range: Some(lsp::Range::new(
1635 lsp::Position::new(0, 5),
1636 lsp::Position::new(0, 5)
1637 )),
1638 range_length: None,
1639 text: " world".into(),
1640 }],
1641 }
1642 );
1643
1644 // Ensure updates to the file are reflected in the LSP.
1645 buffer_1.update(cx, |buffer, cx| {
1646 buffer.file_updated(
1647 Arc::new(File {
1648 abs_path: path!("/root/child/buffer-1").into(),
1649 path: rel_path("child/buffer-1").into(),
1650 }),
1651 cx,
1652 )
1653 });
1654 assert_eq!(
1655 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1656 .await,
1657 lsp::DidCloseTextDocumentParams {
1658 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
1659 }
1660 );
1661 let buffer_1_uri = lsp::Uri::from_file_path(path!("/root/child/buffer-1")).unwrap();
1662 assert_eq!(
1663 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1664 .await,
1665 lsp::DidOpenTextDocumentParams {
1666 text_document: lsp::TextDocumentItem::new(
1667 buffer_1_uri.clone(),
1668 "plaintext".into(),
1669 1,
1670 "Hello world".into()
1671 ),
1672 }
1673 );
1674
1675 // Ensure all previously-registered buffers are closed when signing out.
1676 lsp.set_request_handler::<request::SignOut, _, _>(|_, _| async {
1677 Ok(request::SignOutResult {})
1678 });
1679 copilot
1680 .update(cx, |copilot, cx| copilot.sign_out(cx))
1681 .await
1682 .unwrap();
1683 let mut received_close_notifications = vec![
1684 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1685 .await,
1686 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1687 .await,
1688 ];
1689 received_close_notifications
1690 .sort_by_key(|notification| notification.text_document.uri.clone());
1691 assert_eq!(
1692 received_close_notifications,
1693 vec![
1694 lsp::DidCloseTextDocumentParams {
1695 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
1696 },
1697 lsp::DidCloseTextDocumentParams {
1698 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
1699 },
1700 ],
1701 );
1702
1703 // Ensure all previously-registered buffers are re-opened when signing in.
1704 lsp.set_request_handler::<request::SignIn, _, _>(|_, _| async {
1705 Ok(request::PromptUserDeviceFlow {
1706 user_code: "test-code".into(),
1707 command: lsp::Command {
1708 title: "Sign in".into(),
1709 command: "github.copilot.finishDeviceFlow".into(),
1710 arguments: None,
1711 },
1712 })
1713 });
1714 copilot
1715 .update(cx, |copilot, cx| copilot.sign_in(cx))
1716 .await
1717 .unwrap();
1718
1719 // Simulate auth completion by directly updating sign-in status
1720 copilot.update(cx, |copilot, cx| {
1721 copilot.update_sign_in_status(
1722 request::SignInStatus::Ok {
1723 user: Some("user-1".into()),
1724 },
1725 cx,
1726 );
1727 });
1728
1729 let mut received_open_notifications = vec![
1730 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1731 .await,
1732 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1733 .await,
1734 ];
1735 received_open_notifications
1736 .sort_by_key(|notification| notification.text_document.uri.clone());
1737 assert_eq!(
1738 received_open_notifications,
1739 vec![
1740 lsp::DidOpenTextDocumentParams {
1741 text_document: lsp::TextDocumentItem::new(
1742 buffer_2_uri.clone(),
1743 "plaintext".into(),
1744 0,
1745 "Goodbye".into()
1746 ),
1747 },
1748 lsp::DidOpenTextDocumentParams {
1749 text_document: lsp::TextDocumentItem::new(
1750 buffer_1_uri.clone(),
1751 "plaintext".into(),
1752 0,
1753 "Hello world".into()
1754 ),
1755 }
1756 ]
1757 );
1758 // Dropping a buffer causes it to be closed on the LSP side as well.
1759 cx.update(|_| drop(buffer_2));
1760 assert_eq!(
1761 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1762 .await,
1763 lsp::DidCloseTextDocumentParams {
1764 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
1765 }
1766 );
1767 }
1768
1769 struct File {
1770 abs_path: PathBuf,
1771 path: Arc<RelPath>,
1772 }
1773
1774 impl language::File for File {
1775 fn as_local(&self) -> Option<&dyn language::LocalFile> {
1776 Some(self)
1777 }
1778
1779 fn disk_state(&self) -> language::DiskState {
1780 language::DiskState::Present {
1781 mtime: ::fs::MTime::from_seconds_and_nanos(100, 42),
1782 }
1783 }
1784
1785 fn path(&self) -> &Arc<RelPath> {
1786 &self.path
1787 }
1788
1789 fn path_style(&self, _: &App) -> PathStyle {
1790 PathStyle::local()
1791 }
1792
1793 fn full_path(&self, _: &App) -> PathBuf {
1794 unimplemented!()
1795 }
1796
1797 fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
1798 unimplemented!()
1799 }
1800
1801 fn to_proto(&self, _: &App) -> rpc::proto::File {
1802 unimplemented!()
1803 }
1804
1805 fn worktree_id(&self, _: &App) -> settings::WorktreeId {
1806 settings::WorktreeId::from_usize(0)
1807 }
1808
1809 fn is_private(&self) -> bool {
1810 false
1811 }
1812 }
1813
1814 impl language::LocalFile for File {
1815 fn abs_path(&self, _: &App) -> PathBuf {
1816 self.abs_path.clone()
1817 }
1818
1819 fn load(&self, _: &App) -> Task<Result<String>> {
1820 unimplemented!()
1821 }
1822
1823 fn load_bytes(&self, _cx: &App) -> Task<Result<Vec<u8>>> {
1824 unimplemented!()
1825 }
1826 }
1827
1828 #[gpui::test]
1829 async fn test_copilot_starts_when_ai_becomes_enabled(cx: &mut TestAppContext) {
1830 cx.update(|cx| {
1831 let store = SettingsStore::test(cx);
1832 cx.set_global(store);
1833 DisableAiSettings::register(cx);
1834 AllLanguageSettings::register(cx);
1835
1836 // AI is initially disabled
1837 DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
1838 });
1839
1840 let copilot = cx.new(|cx| Copilot {
1841 server_id: LanguageServerId(0),
1842 fs: FakeFs::new(cx.background_executor().clone()),
1843 node_runtime: NodeRuntime::unavailable(),
1844 server: CopilotServer::Disabled,
1845 buffers: Default::default(),
1846 _subscriptions: vec![],
1847 });
1848
1849 // Verify copilot is disabled initially
1850 copilot.read_with(cx, |copilot, _| {
1851 assert!(
1852 matches!(copilot.server, CopilotServer::Disabled),
1853 "Copilot should be disabled initially"
1854 );
1855 });
1856
1857 // Try to start - should fail because AI is disabled
1858 // Use check_edit_prediction_provider=false to skip provider check
1859 copilot.update(cx, |copilot, cx| {
1860 copilot.start_copilot(false, false, cx);
1861 });
1862
1863 copilot.read_with(cx, |copilot, _| {
1864 assert!(
1865 matches!(copilot.server, CopilotServer::Disabled),
1866 "Copilot should remain disabled when disable_ai is true"
1867 );
1868 });
1869
1870 // Now enable AI
1871 cx.update(|cx| {
1872 DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
1873 });
1874
1875 // Try to start again - should work now
1876 copilot.update(cx, |copilot, cx| {
1877 copilot.start_copilot(false, false, cx);
1878 });
1879
1880 copilot.read_with(cx, |copilot, _| {
1881 assert!(
1882 matches!(copilot.server, CopilotServer::Starting { .. }),
1883 "Copilot should be starting after disable_ai is set to false"
1884 );
1885 });
1886 }
1887
1888 fn init_test(cx: &mut TestAppContext) {
1889 zlog::init_test();
1890
1891 cx.update(|cx| {
1892 let settings_store = SettingsStore::test(cx);
1893 cx.set_global(settings_store);
1894 });
1895 }
1896}