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 this.start_copilot(true, false, cx);
397 if let Ok(server) = this.server.as_running() {
398 notify_did_change_config_to_server(&server.lsp, cx)
399 .context("copilot setting change: did change configuration")
400 .log_err();
401 }
402 this.update_action_visibilities(cx);
403 })
404 .detach();
405 cx.observe_self(|copilot, cx| {
406 copilot.update_action_visibilities(cx);
407 })
408 .detach();
409 this
410 }
411
412 fn shutdown_language_server(
413 &mut self,
414 _cx: &mut Context<Self>,
415 ) -> impl Future<Output = ()> + use<> {
416 let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) {
417 CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })),
418 _ => None,
419 };
420
421 async move {
422 if let Some(shutdown) = shutdown {
423 shutdown.await;
424 }
425 }
426 }
427
428 pub fn start_copilot(
429 &mut self,
430 check_edit_prediction_provider: bool,
431 awaiting_sign_in_after_start: bool,
432 cx: &mut Context<Self>,
433 ) {
434 if !matches!(self.server, CopilotServer::Disabled) {
435 return;
436 }
437 let language_settings = all_language_settings(None, cx);
438 if check_edit_prediction_provider
439 && language_settings.edit_predictions.provider != EditPredictionProvider::Copilot
440 {
441 return;
442 }
443 let server_id = self.server_id;
444 let fs = self.fs.clone();
445 let node_runtime = self.node_runtime.clone();
446 let env = self.build_env(&language_settings.edit_predictions.copilot);
447 let start_task = cx
448 .spawn(async move |this, cx| {
449 Self::start_language_server(
450 server_id,
451 fs,
452 node_runtime,
453 env,
454 this,
455 awaiting_sign_in_after_start,
456 cx,
457 )
458 .await
459 })
460 .shared();
461 self.server = CopilotServer::Starting { task: start_task };
462 cx.notify();
463 }
464
465 fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
466 let proxy_url = copilot_settings.proxy.clone()?;
467 let no_verify = copilot_settings.proxy_no_verify;
468 let http_or_https_proxy = if proxy_url.starts_with("http:") {
469 Some("HTTP_PROXY")
470 } else if proxy_url.starts_with("https:") {
471 Some("HTTPS_PROXY")
472 } else {
473 log::error!(
474 "Unsupported protocol scheme for language server proxy (must be http or https)"
475 );
476 None
477 };
478
479 let mut env = HashMap::default();
480
481 if let Some(proxy_type) = http_or_https_proxy {
482 env.insert(proxy_type.to_string(), proxy_url);
483 if let Some(true) = no_verify {
484 env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
485 };
486 }
487
488 if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {
489 env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
490 }
491
492 if env.is_empty() { None } else { Some(env) }
493 }
494
495 #[cfg(any(test, feature = "test-support"))]
496 pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
497 use fs::FakeFs;
498 use gpui::Subscription;
499 use lsp::FakeLanguageServer;
500 use node_runtime::NodeRuntime;
501
502 let (server, fake_server) = FakeLanguageServer::new(
503 LanguageServerId(0),
504 LanguageServerBinary {
505 path: "path/to/copilot".into(),
506 arguments: vec![],
507 env: None,
508 },
509 "copilot".into(),
510 Default::default(),
511 &mut cx.to_async(),
512 );
513 let node_runtime = NodeRuntime::unavailable();
514 let send_focus_notification = Subscription::new(|| {});
515 let this = cx.new(|cx| Self {
516 server_id: LanguageServerId(0),
517 fs: FakeFs::new(cx.background_executor().clone()),
518 node_runtime,
519 server: CopilotServer::Running(RunningCopilotServer {
520 lsp: Arc::new(server),
521 sign_in_status: SignInStatus::Authorized,
522 registered_buffers: Default::default(),
523 }),
524 _subscriptions: vec![
525 send_focus_notification,
526 cx.on_app_quit(Self::shutdown_language_server),
527 ],
528 buffers: Default::default(),
529 });
530 (this, fake_server)
531 }
532
533 async fn start_language_server(
534 new_server_id: LanguageServerId,
535 fs: Arc<dyn Fs>,
536 node_runtime: NodeRuntime,
537 env: Option<HashMap<String, String>>,
538 this: WeakEntity<Self>,
539 awaiting_sign_in_after_start: bool,
540 cx: &mut AsyncApp,
541 ) {
542 let start_language_server = async {
543 let server_path = get_copilot_lsp(fs, node_runtime.clone()).await?;
544 let node_path = node_runtime.binary_path().await?;
545 ensure_node_version_for_copilot(&node_path).await?;
546
547 let arguments: Vec<OsString> = vec![
548 "--experimental-sqlite".into(),
549 server_path.into(),
550 "--stdio".into(),
551 ];
552 let binary = LanguageServerBinary {
553 path: node_path,
554 arguments,
555 env,
556 };
557
558 let root_path = if cfg!(target_os = "windows") {
559 Path::new("C:/")
560 } else {
561 Path::new("/")
562 };
563
564 let server_name = LanguageServerName("copilot".into());
565 let server = LanguageServer::new(
566 Arc::new(Mutex::new(None)),
567 new_server_id,
568 server_name,
569 binary,
570 root_path,
571 None,
572 Default::default(),
573 cx,
574 )?;
575
576 server
577 .on_notification::<DidChangeStatus, _>({
578 let this = this.clone();
579 move |params, cx| {
580 if params.kind == request::StatusKind::Normal {
581 let this = this.clone();
582 cx.spawn(async move |cx| {
583 let lsp = this
584 .read_with(cx, |copilot, _| {
585 if let CopilotServer::Running(server) = &copilot.server {
586 Some(server.lsp.clone())
587 } else {
588 None
589 }
590 })
591 .ok()
592 .flatten();
593 let Some(lsp) = lsp else { return };
594 let request_timeout = cx.update(|cx| {
595 ProjectSettings::get_global(cx)
596 .global_lsp_settings
597 .get_request_timeout()
598 });
599 let status = lsp
600 .request::<request::CheckStatus>(
601 request::CheckStatusParams {
602 local_checks_only: false,
603 },
604 request_timeout,
605 )
606 .await
607 .into_response()
608 .ok();
609 if let Some(status) = status {
610 this.update(cx, |copilot, cx| {
611 copilot.update_sign_in_status(status, cx);
612 })
613 .ok();
614 }
615 })
616 .detach();
617 }
618 }
619 })
620 .detach();
621
622 server
623 .on_request::<lsp::request::ShowDocument, _, _>(move |params, cx| {
624 if params.external.unwrap_or(false) {
625 let url = params.uri.to_string();
626 cx.update(|cx| cx.open_url(&url));
627 }
628 async move { Ok(lsp::ShowDocumentResult { success: true }) }
629 })
630 .detach();
631
632 let configuration = lsp::DidChangeConfigurationParams {
633 settings: Default::default(),
634 };
635
636 let editor_info = request::SetEditorInfoParams {
637 editor_info: request::EditorInfo {
638 name: "zed".into(),
639 version: env!("CARGO_PKG_VERSION").into(),
640 },
641 editor_plugin_info: request::EditorPluginInfo {
642 name: "zed-copilot".into(),
643 version: "0.0.1".into(),
644 },
645 };
646 let editor_info_json = serde_json::to_value(&editor_info)?;
647
648 let request_timeout = cx.update(|app| {
649 ProjectSettings::get_global(app)
650 .global_lsp_settings
651 .get_request_timeout()
652 });
653
654 let server = cx
655 .update(|cx| {
656 let mut params = server.default_initialize_params(false, false, cx);
657 params.initialization_options = Some(editor_info_json);
658 params
659 .capabilities
660 .window
661 .get_or_insert_with(Default::default)
662 .show_document =
663 Some(lsp::ShowDocumentClientCapabilities { support: true });
664 server.initialize(params, configuration.into(), request_timeout, cx)
665 })
666 .await?;
667
668 this.update(cx, |_, cx| notify_did_change_config_to_server(&server, cx))?
669 .context("copilot: did change configuration")?;
670
671 let status = server
672 .request::<request::CheckStatus>(
673 request::CheckStatusParams {
674 local_checks_only: false,
675 },
676 request_timeout,
677 )
678 .await
679 .into_response()
680 .context("copilot: check status")?;
681
682 anyhow::Ok((server, status))
683 };
684
685 let server = start_language_server.await;
686 this.update(cx, |this, cx| {
687 cx.notify();
688
689 if env::var("ZED_FORCE_COPILOT_ERROR").is_ok() {
690 this.server = CopilotServer::Error(
691 "Forced error for testing (ZED_FORCE_COPILOT_ERROR)".into(),
692 );
693 return;
694 }
695
696 match server {
697 Ok((server, status)) => {
698 this.server = CopilotServer::Running(RunningCopilotServer {
699 lsp: server,
700 sign_in_status: SignInStatus::SignedOut {
701 awaiting_signing_in: awaiting_sign_in_after_start,
702 },
703 registered_buffers: Default::default(),
704 });
705 this.update_sign_in_status(status, cx);
706 }
707 Err(error) => {
708 this.server = CopilotServer::Error(error.to_string().into());
709 cx.notify()
710 }
711 }
712 })
713 .ok();
714 }
715
716 pub fn is_authenticated(&self) -> bool {
717 return matches!(
718 self.server,
719 CopilotServer::Running(RunningCopilotServer {
720 sign_in_status: SignInStatus::Authorized,
721 ..
722 })
723 );
724 }
725
726 pub fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
727 if let CopilotServer::Running(server) = &mut self.server {
728 let task = match &server.sign_in_status {
729 SignInStatus::Authorized => Task::ready(Ok(())).shared(),
730 SignInStatus::SigningIn { task, .. } => {
731 cx.notify();
732 task.clone()
733 }
734 SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => {
735 let lsp = server.lsp.clone();
736
737 let request_timeout = ProjectSettings::get_global(cx)
738 .global_lsp_settings
739 .get_request_timeout();
740
741 let task = cx
742 .spawn(async move |this, cx| {
743 let sign_in = async {
744 let flow = lsp
745 .request::<request::SignIn>(
746 request::SignInParams {},
747 request_timeout,
748 )
749 .await
750 .into_response()
751 .context("copilot sign-in")?;
752
753 this.update(cx, |this, cx| {
754 if let CopilotServer::Running(RunningCopilotServer {
755 sign_in_status: status,
756 ..
757 }) = &mut this.server
758 && let SignInStatus::SigningIn {
759 prompt: prompt_flow,
760 ..
761 } = status
762 {
763 *prompt_flow = Some(flow.clone());
764 cx.notify();
765 }
766 })?;
767
768 anyhow::Ok(())
769 };
770
771 let sign_in = sign_in.await;
772 this.update(cx, |this, cx| match sign_in {
773 Ok(()) => Ok(()),
774 Err(error) => {
775 this.update_sign_in_status(
776 request::SignInStatus::NotSignedIn,
777 cx,
778 );
779 Err(Arc::new(error))
780 }
781 })?
782 })
783 .shared();
784 server.sign_in_status = SignInStatus::SigningIn {
785 prompt: None,
786 task: task.clone(),
787 };
788 cx.notify();
789 task
790 }
791 };
792
793 cx.background_spawn(task.map_err(|err| anyhow!("{err:?}")))
794 } else {
795 // If we're downloading, wait until download is finished
796 // If we're in a stuck state, display to the user
797 Task::ready(Err(anyhow!("copilot hasn't started yet")))
798 }
799 }
800
801 pub fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
802 self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
803 match &self.server {
804 CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => {
805 let request_timeout = ProjectSettings::get_global(cx)
806 .global_lsp_settings
807 .get_request_timeout();
808
809 let server = server.clone();
810 cx.background_spawn(async move {
811 server
812 .request::<request::SignOut>(request::SignOutParams {}, request_timeout)
813 .await
814 .into_response()
815 .context("copilot: sign in confirm")?;
816 anyhow::Ok(())
817 })
818 }
819 CopilotServer::Disabled => cx.background_spawn(async {
820 clear_copilot_config_dir().await;
821 anyhow::Ok(())
822 }),
823 _ => Task::ready(Err(anyhow!("copilot hasn't started yet"))),
824 }
825 }
826
827 pub fn reinstall(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
828 let language_settings = all_language_settings(None, cx);
829 let env = self.build_env(&language_settings.edit_predictions.copilot);
830 let start_task = cx
831 .spawn({
832 let fs = self.fs.clone();
833 let node_runtime = self.node_runtime.clone();
834 let server_id = self.server_id;
835 async move |this, cx| {
836 clear_copilot_dir().await;
837 Self::start_language_server(server_id, fs, node_runtime, env, this, false, cx)
838 .await
839 }
840 })
841 .shared();
842
843 self.server = CopilotServer::Starting {
844 task: start_task.clone(),
845 };
846
847 cx.notify();
848
849 start_task
850 }
851
852 pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {
853 if let CopilotServer::Running(server) = &self.server {
854 Some(&server.lsp)
855 } else {
856 None
857 }
858 }
859
860 pub fn register_buffer(&mut self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) {
861 let weak_buffer = buffer.downgrade();
862 self.buffers.insert(weak_buffer.clone());
863
864 if let CopilotServer::Running(RunningCopilotServer {
865 lsp: server,
866 sign_in_status: status,
867 registered_buffers,
868 ..
869 }) = &mut self.server
870 {
871 if !matches!(status, SignInStatus::Authorized) {
872 return;
873 }
874
875 let entry = registered_buffers.entry(buffer.entity_id());
876 if let Entry::Vacant(e) = entry {
877 let Ok(uri) = uri_for_buffer(buffer, cx) else {
878 return;
879 };
880 let language_id = id_for_language(buffer.read(cx).language());
881 let snapshot = buffer.read(cx).snapshot();
882 server
883 .notify::<lsp::notification::DidOpenTextDocument>(
884 lsp::DidOpenTextDocumentParams {
885 text_document: lsp::TextDocumentItem {
886 uri: uri.clone(),
887 language_id: language_id.clone(),
888 version: 0,
889 text: snapshot.text(),
890 },
891 },
892 )
893 .ok();
894
895 e.insert(RegisteredBuffer {
896 uri,
897 language_id,
898 snapshot,
899 snapshot_version: 0,
900 pending_buffer_change: Task::ready(Some(())),
901 _subscriptions: [
902 cx.subscribe(buffer, |this, buffer, event, cx| {
903 this.handle_buffer_event(buffer, event, cx).log_err();
904 }),
905 cx.observe_release(buffer, move |this, _buffer, _cx| {
906 this.buffers.remove(&weak_buffer);
907 this.unregister_buffer(&weak_buffer);
908 }),
909 ],
910 });
911 }
912 }
913 }
914
915 fn handle_buffer_event(
916 &mut self,
917 buffer: Entity<Buffer>,
918 event: &language::BufferEvent,
919 cx: &mut Context<Self>,
920 ) -> Result<()> {
921 if let Ok(server) = self.server.as_running()
922 && let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
923 {
924 match event {
925 language::BufferEvent::Edited => {
926 drop(registered_buffer.report_changes(&buffer, cx));
927 }
928 language::BufferEvent::Saved => {
929 server
930 .lsp
931 .notify::<lsp::notification::DidSaveTextDocument>(
932 lsp::DidSaveTextDocumentParams {
933 text_document: lsp::TextDocumentIdentifier::new(
934 registered_buffer.uri.clone(),
935 ),
936 text: None,
937 },
938 )
939 .ok();
940 }
941 language::BufferEvent::FileHandleChanged
942 | language::BufferEvent::LanguageChanged(_) => {
943 let new_language_id = id_for_language(buffer.read(cx).language());
944 let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
945 return Ok(());
946 };
947 if new_uri != registered_buffer.uri
948 || new_language_id != registered_buffer.language_id
949 {
950 let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
951 registered_buffer.language_id = new_language_id;
952 server
953 .lsp
954 .notify::<lsp::notification::DidCloseTextDocument>(
955 lsp::DidCloseTextDocumentParams {
956 text_document: lsp::TextDocumentIdentifier::new(old_uri),
957 },
958 )
959 .ok();
960 server
961 .lsp
962 .notify::<lsp::notification::DidOpenTextDocument>(
963 lsp::DidOpenTextDocumentParams {
964 text_document: lsp::TextDocumentItem::new(
965 registered_buffer.uri.clone(),
966 registered_buffer.language_id.clone(),
967 registered_buffer.snapshot_version,
968 registered_buffer.snapshot.text(),
969 ),
970 },
971 )
972 .ok();
973 }
974 }
975 _ => {}
976 }
977 }
978
979 Ok(())
980 }
981
982 fn unregister_buffer(&mut self, buffer: &WeakEntity<Buffer>) {
983 if let Ok(server) = self.server.as_running()
984 && let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id())
985 {
986 server
987 .lsp
988 .notify::<lsp::notification::DidCloseTextDocument>(
989 lsp::DidCloseTextDocumentParams {
990 text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
991 },
992 )
993 .ok();
994 }
995 }
996
997 pub(crate) fn completions(
998 &mut self,
999 buffer: &Entity<Buffer>,
1000 position: Anchor,
1001 cx: &mut Context<Self>,
1002 ) -> Task<Result<Vec<CopilotEditPrediction>>> {
1003 self.register_buffer(buffer, cx);
1004
1005 let server = match self.server.as_authenticated() {
1006 Ok(server) => server,
1007 Err(error) => return Task::ready(Err(error)),
1008 };
1009 let buffer_entity = buffer.clone();
1010 let lsp = server.lsp.clone();
1011 let registered_buffer = server
1012 .registered_buffers
1013 .get_mut(&buffer.entity_id())
1014 .unwrap();
1015 let pending_snapshot = registered_buffer.report_changes(buffer, cx);
1016 let buffer = buffer.read(cx);
1017 let uri = registered_buffer.uri.clone();
1018 let position = position.to_point_utf16(buffer);
1019 let snapshot = buffer.snapshot();
1020 let settings = snapshot.settings_at(0, cx);
1021 let tab_size = settings.tab_size.get();
1022 let hard_tabs = settings.hard_tabs;
1023 drop(settings);
1024
1025 let request_timeout = ProjectSettings::get_global(cx)
1026 .global_lsp_settings
1027 .get_request_timeout();
1028
1029 let nes_enabled = AllLanguageSettings::get_global(cx)
1030 .edit_predictions
1031 .copilot
1032 .enable_next_edit_suggestions
1033 .unwrap_or(true);
1034
1035 cx.background_spawn(async move {
1036 let (version, snapshot) = pending_snapshot.await?;
1037 let lsp_position = point_to_lsp(position);
1038
1039 let nes_fut = if nes_enabled {
1040 lsp.request::<NextEditSuggestions>(
1041 request::NextEditSuggestionsParams {
1042 text_document: lsp::VersionedTextDocumentIdentifier {
1043 uri: uri.clone(),
1044 version,
1045 },
1046 position: lsp_position,
1047 },
1048 request_timeout,
1049 )
1050 .map(|resp| {
1051 resp.into_response()
1052 .ok()
1053 .map(|result| {
1054 result
1055 .edits
1056 .into_iter()
1057 .map(|completion| {
1058 let start = snapshot.clip_point_utf16(
1059 point_from_lsp(completion.range.start),
1060 Bias::Left,
1061 );
1062 let end = snapshot.clip_point_utf16(
1063 point_from_lsp(completion.range.end),
1064 Bias::Left,
1065 );
1066 CopilotEditPrediction {
1067 buffer: buffer_entity.clone(),
1068 range: snapshot.anchor_before(start)
1069 ..snapshot.anchor_after(end),
1070 text: completion.text,
1071 command: completion.command,
1072 snapshot: snapshot.clone(),
1073 source: CompletionSource::NextEditSuggestion,
1074 }
1075 })
1076 .collect::<Vec<_>>()
1077 })
1078 .unwrap_or_default()
1079 })
1080 .left_future()
1081 .fuse()
1082 } else {
1083 future::ready(Vec::<CopilotEditPrediction>::new())
1084 .right_future()
1085 .fuse()
1086 };
1087
1088 let inline_fut = lsp
1089 .request::<InlineCompletions>(
1090 request::InlineCompletionsParams {
1091 text_document: lsp::VersionedTextDocumentIdentifier {
1092 uri: uri.clone(),
1093 version,
1094 },
1095 position: lsp_position,
1096 context: InlineCompletionContext {
1097 trigger_kind: InlineCompletionTriggerKind::Automatic,
1098 },
1099 formatting_options: Some(FormattingOptions {
1100 tab_size,
1101 insert_spaces: !hard_tabs,
1102 }),
1103 },
1104 request_timeout,
1105 )
1106 .map(|resp| {
1107 resp.into_response()
1108 .ok()
1109 .map(|result| {
1110 result
1111 .items
1112 .into_iter()
1113 .map(|item| {
1114 let start = snapshot.clip_point_utf16(
1115 point_from_lsp(item.range.start),
1116 Bias::Left,
1117 );
1118 let end = snapshot.clip_point_utf16(
1119 point_from_lsp(item.range.end),
1120 Bias::Left,
1121 );
1122 CopilotEditPrediction {
1123 buffer: buffer_entity.clone(),
1124 range: snapshot.anchor_before(start)
1125 ..snapshot.anchor_after(end),
1126 text: item.insert_text,
1127 command: item.command,
1128 snapshot: snapshot.clone(),
1129 source: CompletionSource::InlineCompletion,
1130 }
1131 })
1132 .collect::<Vec<_>>()
1133 })
1134 .unwrap_or_default()
1135 })
1136 .fuse();
1137
1138 futures::pin_mut!(nes_fut, inline_fut);
1139
1140 let mut nes_result: Option<Vec<CopilotEditPrediction>> = None;
1141 let mut inline_result: Option<Vec<CopilotEditPrediction>> = None;
1142
1143 loop {
1144 select_biased! {
1145 nes = nes_fut => {
1146 if !nes.is_empty() {
1147 return Ok(nes);
1148 }
1149 nes_result = Some(nes);
1150 }
1151 inline = inline_fut => {
1152 if !inline.is_empty() {
1153 return Ok(inline);
1154 }
1155 inline_result = Some(inline);
1156 }
1157 complete => break,
1158 }
1159
1160 if let (Some(nes), Some(inline)) = (&nes_result, &inline_result) {
1161 return if !nes.is_empty() {
1162 Ok(nes.clone())
1163 } else {
1164 Ok(inline.clone())
1165 };
1166 }
1167 }
1168
1169 Ok(nes_result.or(inline_result).unwrap_or_default())
1170 })
1171 }
1172
1173 pub(crate) fn accept_completion(
1174 &mut self,
1175 completion: &CopilotEditPrediction,
1176 cx: &mut Context<Self>,
1177 ) -> Task<Result<()>> {
1178 let server = match self.server.as_authenticated() {
1179 Ok(server) => server,
1180 Err(error) => return Task::ready(Err(error)),
1181 };
1182 if let Some(command) = &completion.command {
1183 let request_timeout = ProjectSettings::get_global(cx)
1184 .global_lsp_settings
1185 .get_request_timeout();
1186
1187 let request = server.lsp.request::<lsp::ExecuteCommand>(
1188 lsp::ExecuteCommandParams {
1189 command: command.command.clone(),
1190 arguments: command.arguments.clone().unwrap_or_default(),
1191 ..Default::default()
1192 },
1193 request_timeout,
1194 );
1195 cx.background_spawn(async move {
1196 request
1197 .await
1198 .into_response()
1199 .context("copilot: notify accepted")?;
1200 Ok(())
1201 })
1202 } else {
1203 Task::ready(Ok(()))
1204 }
1205 }
1206
1207 pub fn status(&self) -> Status {
1208 match &self.server {
1209 CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
1210 CopilotServer::Disabled => Status::Disabled,
1211 CopilotServer::Error(error) => Status::Error(error.clone()),
1212 CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
1213 match sign_in_status {
1214 SignInStatus::Authorized => Status::Authorized,
1215 SignInStatus::Unauthorized => Status::Unauthorized,
1216 SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
1217 prompt: prompt.clone(),
1218 },
1219 SignInStatus::SignedOut {
1220 awaiting_signing_in,
1221 } => Status::SignedOut {
1222 awaiting_signing_in: *awaiting_signing_in,
1223 },
1224 }
1225 }
1226 }
1227 }
1228
1229 pub fn update_sign_in_status(
1230 &mut self,
1231 lsp_status: request::SignInStatus,
1232 cx: &mut Context<Self>,
1233 ) {
1234 self.buffers.retain(|buffer| buffer.is_upgradable());
1235
1236 if let Ok(server) = self.server.as_running() {
1237 match lsp_status {
1238 request::SignInStatus::Ok { user: Some(_) }
1239 | request::SignInStatus::MaybeOk { .. }
1240 | request::SignInStatus::AlreadySignedIn { .. } => {
1241 server.sign_in_status = SignInStatus::Authorized;
1242 cx.emit(Event::CopilotAuthSignedIn);
1243 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1244 if let Some(buffer) = buffer.upgrade() {
1245 self.register_buffer(&buffer, cx);
1246 }
1247 }
1248 }
1249 request::SignInStatus::NotAuthorized { .. } => {
1250 server.sign_in_status = SignInStatus::Unauthorized;
1251 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1252 self.unregister_buffer(&buffer);
1253 }
1254 }
1255 request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => {
1256 if !matches!(server.sign_in_status, SignInStatus::SignedOut { .. }) {
1257 server.sign_in_status = SignInStatus::SignedOut {
1258 awaiting_signing_in: false,
1259 };
1260 }
1261 cx.emit(Event::CopilotAuthSignedOut);
1262 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1263 self.unregister_buffer(&buffer);
1264 }
1265 }
1266 }
1267
1268 cx.notify();
1269 }
1270 }
1271
1272 fn update_action_visibilities(&self, cx: &mut App) {
1273 let signed_in_actions = [
1274 TypeId::of::<Suggest>(),
1275 TypeId::of::<NextSuggestion>(),
1276 TypeId::of::<PreviousSuggestion>(),
1277 TypeId::of::<Reinstall>(),
1278 ];
1279 let auth_actions = [TypeId::of::<SignOut>()];
1280 let no_auth_actions = [TypeId::of::<SignIn>()];
1281 let status = self.status();
1282
1283 let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
1284 let filter = CommandPaletteFilter::global_mut(cx);
1285
1286 if is_ai_disabled {
1287 filter.hide_action_types(&signed_in_actions);
1288 filter.hide_action_types(&auth_actions);
1289 filter.hide_action_types(&no_auth_actions);
1290 } else {
1291 match status {
1292 Status::Disabled => {
1293 filter.hide_action_types(&signed_in_actions);
1294 filter.hide_action_types(&auth_actions);
1295 filter.hide_action_types(&no_auth_actions);
1296 }
1297 Status::Authorized => {
1298 filter.hide_action_types(&no_auth_actions);
1299 filter.show_action_types(signed_in_actions.iter().chain(&auth_actions));
1300 }
1301 _ => {
1302 filter.hide_action_types(&signed_in_actions);
1303 filter.hide_action_types(&auth_actions);
1304 filter.show_action_types(&no_auth_actions);
1305 }
1306 }
1307 }
1308 }
1309}
1310
1311fn id_for_language(language: Option<&Arc<Language>>) -> String {
1312 language
1313 .map(|language| language.lsp_id())
1314 .unwrap_or_else(|| "plaintext".to_string())
1315}
1316
1317fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Uri, ()> {
1318 if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
1319 lsp::Uri::from_file_path(file.abs_path(cx))
1320 } else {
1321 format!("buffer://{}", buffer.entity_id())
1322 .parse()
1323 .map_err(|_| ())
1324 }
1325}
1326
1327fn notify_did_change_config_to_server(
1328 server: &Arc<LanguageServer>,
1329 cx: &mut Context<Copilot>,
1330) -> std::result::Result<(), anyhow::Error> {
1331 let copilot_settings = all_language_settings(None, cx)
1332 .edit_predictions
1333 .copilot
1334 .clone();
1335
1336 if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
1337 copilot_chat.update(cx, |chat, cx| {
1338 chat.set_configuration(
1339 copilot_chat::CopilotChatConfiguration {
1340 enterprise_uri: copilot_settings.enterprise_uri.clone(),
1341 },
1342 cx,
1343 );
1344 });
1345 }
1346
1347 let settings = json!({
1348 "http": {
1349 "proxy": copilot_settings.proxy,
1350 "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
1351 },
1352 "github-enterprise": {
1353 "uri": copilot_settings.enterprise_uri
1354 }
1355 });
1356
1357 server
1358 .notify::<lsp::notification::DidChangeConfiguration>(lsp::DidChangeConfigurationParams {
1359 settings,
1360 })
1361 .ok();
1362 Ok(())
1363}
1364
1365async fn clear_copilot_dir() {
1366 remove_matching(paths::copilot_dir(), |_| true).await
1367}
1368
1369async fn clear_copilot_config_dir() {
1370 remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
1371}
1372
1373async fn ensure_node_version_for_copilot(node_path: &Path) -> anyhow::Result<()> {
1374 const MIN_COPILOT_NODE_VERSION: Version = Version::new(20, 8, 0);
1375
1376 log::info!("Checking Node.js version for Copilot at: {:?}", node_path);
1377
1378 let output = util::command::new_smol_command(node_path)
1379 .arg("--version")
1380 .output()
1381 .await
1382 .with_context(|| format!("checking Node.js version at {:?}", node_path))?;
1383
1384 if !output.status.success() {
1385 anyhow::bail!(
1386 "failed to run node --version for Copilot. stdout: {}, stderr: {}",
1387 String::from_utf8_lossy(&output.stdout),
1388 String::from_utf8_lossy(&output.stderr),
1389 );
1390 }
1391
1392 let version_str = String::from_utf8_lossy(&output.stdout);
1393 let version = Version::parse(version_str.trim().trim_start_matches('v'))
1394 .with_context(|| format!("parsing Node.js version from '{}'", version_str.trim()))?;
1395
1396 if version < MIN_COPILOT_NODE_VERSION {
1397 anyhow::bail!(
1398 "GitHub Copilot language server requires Node.js {MIN_COPILOT_NODE_VERSION} or later, but found {version}. \
1399 Please update your Node.js version or configure a different Node.js path in settings."
1400 );
1401 }
1402
1403 log::info!(
1404 "Node.js version {} meets Copilot requirements (>= {})",
1405 version,
1406 MIN_COPILOT_NODE_VERSION
1407 );
1408 Ok(())
1409}
1410
1411async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::Result<PathBuf> {
1412 const PACKAGE_NAME: &str = "@github/copilot-language-server";
1413 const SERVER_PATH: &str =
1414 "node_modules/@github/copilot-language-server/dist/language-server.js";
1415
1416 let latest_version = node_runtime
1417 .npm_package_latest_version(PACKAGE_NAME)
1418 .await?;
1419 let server_path = paths::copilot_dir().join(SERVER_PATH);
1420
1421 fs.create_dir(paths::copilot_dir()).await?;
1422
1423 let should_install = node_runtime
1424 .should_install_npm_package(
1425 PACKAGE_NAME,
1426 &server_path,
1427 paths::copilot_dir(),
1428 VersionStrategy::Latest(&latest_version),
1429 )
1430 .await;
1431 if should_install {
1432 node_runtime
1433 .npm_install_packages(
1434 paths::copilot_dir(),
1435 &[(PACKAGE_NAME, &latest_version.to_string())],
1436 )
1437 .await?;
1438 }
1439
1440 Ok(server_path)
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446 use gpui::TestAppContext;
1447 use util::{
1448 path,
1449 paths::PathStyle,
1450 rel_path::{RelPath, rel_path},
1451 };
1452
1453 #[gpui::test(iterations = 10)]
1454 async fn test_buffer_management(cx: &mut TestAppContext) {
1455 init_test(cx);
1456 let (copilot, mut lsp) = Copilot::fake(cx);
1457
1458 let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx));
1459 let buffer_1_uri: lsp::Uri = format!("buffer://{}", buffer_1.entity_id().as_u64())
1460 .parse()
1461 .unwrap();
1462 copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
1463 assert_eq!(
1464 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1465 .await,
1466 lsp::DidOpenTextDocumentParams {
1467 text_document: lsp::TextDocumentItem::new(
1468 buffer_1_uri.clone(),
1469 "plaintext".into(),
1470 0,
1471 "Hello".into()
1472 ),
1473 }
1474 );
1475
1476 let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx));
1477 let buffer_2_uri: lsp::Uri = format!("buffer://{}", buffer_2.entity_id().as_u64())
1478 .parse()
1479 .unwrap();
1480 copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
1481 assert_eq!(
1482 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1483 .await,
1484 lsp::DidOpenTextDocumentParams {
1485 text_document: lsp::TextDocumentItem::new(
1486 buffer_2_uri.clone(),
1487 "plaintext".into(),
1488 0,
1489 "Goodbye".into()
1490 ),
1491 }
1492 );
1493
1494 buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
1495 assert_eq!(
1496 lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
1497 .await,
1498 lsp::DidChangeTextDocumentParams {
1499 text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
1500 content_changes: vec![lsp::TextDocumentContentChangeEvent {
1501 range: Some(lsp::Range::new(
1502 lsp::Position::new(0, 5),
1503 lsp::Position::new(0, 5)
1504 )),
1505 range_length: None,
1506 text: " world".into(),
1507 }],
1508 }
1509 );
1510
1511 // Ensure updates to the file are reflected in the LSP.
1512 buffer_1.update(cx, |buffer, cx| {
1513 buffer.file_updated(
1514 Arc::new(File {
1515 abs_path: path!("/root/child/buffer-1").into(),
1516 path: rel_path("child/buffer-1").into(),
1517 }),
1518 cx,
1519 )
1520 });
1521 assert_eq!(
1522 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1523 .await,
1524 lsp::DidCloseTextDocumentParams {
1525 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
1526 }
1527 );
1528 let buffer_1_uri = lsp::Uri::from_file_path(path!("/root/child/buffer-1")).unwrap();
1529 assert_eq!(
1530 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1531 .await,
1532 lsp::DidOpenTextDocumentParams {
1533 text_document: lsp::TextDocumentItem::new(
1534 buffer_1_uri.clone(),
1535 "plaintext".into(),
1536 1,
1537 "Hello world".into()
1538 ),
1539 }
1540 );
1541
1542 // Ensure all previously-registered buffers are closed when signing out.
1543 lsp.set_request_handler::<request::SignOut, _, _>(|_, _| async {
1544 Ok(request::SignOutResult {})
1545 });
1546 copilot
1547 .update(cx, |copilot, cx| copilot.sign_out(cx))
1548 .await
1549 .unwrap();
1550 let mut received_close_notifications = vec![
1551 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1552 .await,
1553 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1554 .await,
1555 ];
1556 received_close_notifications
1557 .sort_by_key(|notification| notification.text_document.uri.clone());
1558 assert_eq!(
1559 received_close_notifications,
1560 vec![
1561 lsp::DidCloseTextDocumentParams {
1562 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
1563 },
1564 lsp::DidCloseTextDocumentParams {
1565 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
1566 },
1567 ],
1568 );
1569
1570 // Ensure all previously-registered buffers are re-opened when signing in.
1571 lsp.set_request_handler::<request::SignIn, _, _>(|_, _| async {
1572 Ok(request::PromptUserDeviceFlow {
1573 user_code: "test-code".into(),
1574 command: lsp::Command {
1575 title: "Sign in".into(),
1576 command: "github.copilot.finishDeviceFlow".into(),
1577 arguments: None,
1578 },
1579 })
1580 });
1581 copilot
1582 .update(cx, |copilot, cx| copilot.sign_in(cx))
1583 .await
1584 .unwrap();
1585
1586 // Simulate auth completion by directly updating sign-in status
1587 copilot.update(cx, |copilot, cx| {
1588 copilot.update_sign_in_status(
1589 request::SignInStatus::Ok {
1590 user: Some("user-1".into()),
1591 },
1592 cx,
1593 );
1594 });
1595
1596 let mut received_open_notifications = vec![
1597 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1598 .await,
1599 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1600 .await,
1601 ];
1602 received_open_notifications
1603 .sort_by_key(|notification| notification.text_document.uri.clone());
1604 assert_eq!(
1605 received_open_notifications,
1606 vec![
1607 lsp::DidOpenTextDocumentParams {
1608 text_document: lsp::TextDocumentItem::new(
1609 buffer_2_uri.clone(),
1610 "plaintext".into(),
1611 0,
1612 "Goodbye".into()
1613 ),
1614 },
1615 lsp::DidOpenTextDocumentParams {
1616 text_document: lsp::TextDocumentItem::new(
1617 buffer_1_uri.clone(),
1618 "plaintext".into(),
1619 0,
1620 "Hello world".into()
1621 ),
1622 }
1623 ]
1624 );
1625 // Dropping a buffer causes it to be closed on the LSP side as well.
1626 cx.update(|_| drop(buffer_2));
1627 assert_eq!(
1628 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1629 .await,
1630 lsp::DidCloseTextDocumentParams {
1631 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
1632 }
1633 );
1634 }
1635
1636 struct File {
1637 abs_path: PathBuf,
1638 path: Arc<RelPath>,
1639 }
1640
1641 impl language::File for File {
1642 fn as_local(&self) -> Option<&dyn language::LocalFile> {
1643 Some(self)
1644 }
1645
1646 fn disk_state(&self) -> language::DiskState {
1647 language::DiskState::Present {
1648 mtime: ::fs::MTime::from_seconds_and_nanos(100, 42),
1649 }
1650 }
1651
1652 fn path(&self) -> &Arc<RelPath> {
1653 &self.path
1654 }
1655
1656 fn path_style(&self, _: &App) -> PathStyle {
1657 PathStyle::local()
1658 }
1659
1660 fn full_path(&self, _: &App) -> PathBuf {
1661 unimplemented!()
1662 }
1663
1664 fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
1665 unimplemented!()
1666 }
1667
1668 fn to_proto(&self, _: &App) -> rpc::proto::File {
1669 unimplemented!()
1670 }
1671
1672 fn worktree_id(&self, _: &App) -> settings::WorktreeId {
1673 settings::WorktreeId::from_usize(0)
1674 }
1675
1676 fn is_private(&self) -> bool {
1677 false
1678 }
1679 }
1680
1681 impl language::LocalFile for File {
1682 fn abs_path(&self, _: &App) -> PathBuf {
1683 self.abs_path.clone()
1684 }
1685
1686 fn load(&self, _: &App) -> Task<Result<String>> {
1687 unimplemented!()
1688 }
1689
1690 fn load_bytes(&self, _cx: &App) -> Task<Result<Vec<u8>>> {
1691 unimplemented!()
1692 }
1693 }
1694
1695 fn init_test(cx: &mut TestAppContext) {
1696 zlog::init_test();
1697
1698 cx.update(|cx| {
1699 let settings_store = SettingsStore::test(cx);
1700 cx.set_global(settings_store);
1701 });
1702 }
1703}