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