1pub mod copilot_chat;
2mod copilot_completion_provider;
3pub mod request;
4mod sign_in;
5
6use crate::sign_in::initiate_sign_in_within_workspace;
7use ::fs::Fs;
8use anyhow::{Context as _, Result, anyhow};
9use collections::{HashMap, HashSet};
10use command_palette_hooks::CommandPaletteFilter;
11use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
12use gpui::{
13 App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Task,
14 WeakEntity, actions,
15};
16use http_client::HttpClient;
17use language::language_settings::CopilotSettings;
18use language::{
19 Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
20 language_settings::{EditPredictionProvider, all_language_settings, language_settings},
21 point_from_lsp, point_to_lsp,
22};
23use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
24use node_runtime::NodeRuntime;
25use parking_lot::Mutex;
26use request::StatusNotification;
27use serde_json::json;
28use settings::SettingsStore;
29use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
30use std::collections::hash_map::Entry;
31use std::{
32 any::TypeId,
33 env,
34 ffi::OsString,
35 mem,
36 ops::Range,
37 path::{Path, PathBuf},
38 sync::Arc,
39};
40use util::{ResultExt, fs::remove_matching};
41use workspace::Workspace;
42
43pub use crate::copilot_completion_provider::CopilotCompletionProvider;
44pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in, reinstall_and_sign_in};
45
46actions!(
47 copilot,
48 [
49 /// Requests a code completion suggestion from Copilot.
50 Suggest,
51 /// Cycles to the next Copilot suggestion.
52 NextSuggestion,
53 /// Cycles to the previous Copilot suggestion.
54 PreviousSuggestion,
55 /// Reinstalls the Copilot language server.
56 Reinstall,
57 /// Signs in to GitHub Copilot.
58 SignIn,
59 /// Signs out of GitHub Copilot.
60 SignOut
61 ]
62);
63
64pub fn init(
65 new_server_id: LanguageServerId,
66 fs: Arc<dyn Fs>,
67 http: Arc<dyn HttpClient>,
68 node_runtime: NodeRuntime,
69 cx: &mut App,
70) {
71 let language_settings = all_language_settings(None, cx);
72 let configuration = copilot_chat::CopilotChatConfiguration {
73 enterprise_uri: language_settings
74 .edit_predictions
75 .copilot
76 .enterprise_uri
77 .clone(),
78 };
79 copilot_chat::init(fs.clone(), http.clone(), configuration, cx);
80
81 let copilot = cx.new({
82 let node_runtime = node_runtime.clone();
83 move |cx| Copilot::start(new_server_id, fs, node_runtime, cx)
84 });
85 Copilot::set_global(copilot.clone(), cx);
86 cx.observe(&copilot, |handle, cx| {
87 let copilot_action_types = [
88 TypeId::of::<Suggest>(),
89 TypeId::of::<NextSuggestion>(),
90 TypeId::of::<PreviousSuggestion>(),
91 TypeId::of::<Reinstall>(),
92 ];
93 let copilot_auth_action_types = [TypeId::of::<SignOut>()];
94 let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
95 let status = handle.read(cx).status();
96 let filter = CommandPaletteFilter::global_mut(cx);
97
98 match status {
99 Status::Disabled => {
100 filter.hide_action_types(&copilot_action_types);
101 filter.hide_action_types(&copilot_auth_action_types);
102 filter.hide_action_types(&copilot_no_auth_action_types);
103 }
104 Status::Authorized => {
105 filter.hide_action_types(&copilot_no_auth_action_types);
106 filter.show_action_types(
107 copilot_action_types
108 .iter()
109 .chain(&copilot_auth_action_types),
110 );
111 }
112 _ => {
113 filter.hide_action_types(&copilot_action_types);
114 filter.hide_action_types(&copilot_auth_action_types);
115 filter.show_action_types(copilot_no_auth_action_types.iter());
116 }
117 }
118 })
119 .detach();
120
121 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
122 workspace.register_action(|workspace, _: &SignIn, window, cx| {
123 if let Some(copilot) = Copilot::global(cx) {
124 let is_reinstall = false;
125 initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx);
126 }
127 });
128 workspace.register_action(|workspace, _: &Reinstall, window, cx| {
129 if let Some(copilot) = Copilot::global(cx) {
130 reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx);
131 }
132 });
133 workspace.register_action(|workspace, _: &SignOut, _window, cx| {
134 if let Some(copilot) = Copilot::global(cx) {
135 sign_out_within_workspace(workspace, copilot, cx);
136 }
137 });
138 })
139 .detach();
140}
141
142enum CopilotServer {
143 Disabled,
144 Starting { task: Shared<Task<()>> },
145 Error(Arc<str>),
146 Running(RunningCopilotServer),
147}
148
149impl CopilotServer {
150 fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
151 let server = self.as_running()?;
152 anyhow::ensure!(
153 matches!(server.sign_in_status, SignInStatus::Authorized { .. }),
154 "must sign in before using copilot"
155 );
156 Ok(server)
157 }
158
159 fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
160 match self {
161 CopilotServer::Starting { .. } => anyhow::bail!("copilot is still starting"),
162 CopilotServer::Disabled => anyhow::bail!("copilot is disabled"),
163 CopilotServer::Error(error) => {
164 anyhow::bail!("copilot was not started because of an error: {error}")
165 }
166 CopilotServer::Running(server) => Ok(server),
167 }
168 }
169}
170
171struct RunningCopilotServer {
172 lsp: Arc<LanguageServer>,
173 sign_in_status: SignInStatus,
174 registered_buffers: HashMap<EntityId, RegisteredBuffer>,
175}
176
177#[derive(Clone, Debug)]
178enum SignInStatus {
179 Authorized,
180 Unauthorized,
181 SigningIn {
182 prompt: Option<request::PromptUserDeviceFlow>,
183 task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
184 },
185 SignedOut {
186 awaiting_signing_in: bool,
187 },
188}
189
190#[derive(Debug, Clone)]
191pub enum Status {
192 Starting {
193 task: Shared<Task<()>>,
194 },
195 Error(Arc<str>),
196 Disabled,
197 SignedOut {
198 awaiting_signing_in: bool,
199 },
200 SigningIn {
201 prompt: Option<request::PromptUserDeviceFlow>,
202 },
203 Unauthorized,
204 Authorized,
205}
206
207impl Status {
208 pub fn is_authorized(&self) -> bool {
209 matches!(self, Status::Authorized)
210 }
211
212 pub fn is_disabled(&self) -> bool {
213 matches!(self, Status::Disabled)
214 }
215}
216
217struct RegisteredBuffer {
218 uri: lsp::Url,
219 language_id: String,
220 snapshot: BufferSnapshot,
221 snapshot_version: i32,
222 _subscriptions: [gpui::Subscription; 2],
223 pending_buffer_change: Task<Option<()>>,
224}
225
226impl RegisteredBuffer {
227 fn report_changes(
228 &mut self,
229 buffer: &Entity<Buffer>,
230 cx: &mut Context<Copilot>,
231 ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
232 let (done_tx, done_rx) = oneshot::channel();
233
234 if buffer.read(cx).version() == self.snapshot.version {
235 let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
236 } else {
237 let buffer = buffer.downgrade();
238 let id = buffer.entity_id();
239 let prev_pending_change =
240 mem::replace(&mut self.pending_buffer_change, Task::ready(None));
241 self.pending_buffer_change = cx.spawn(async move |copilot, cx| {
242 prev_pending_change.await;
243
244 let old_version = copilot
245 .update(cx, |copilot, _| {
246 let server = copilot.server.as_authenticated().log_err()?;
247 let buffer = server.registered_buffers.get_mut(&id)?;
248 Some(buffer.snapshot.version.clone())
249 })
250 .ok()??;
251 let new_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()).ok()?;
252
253 let content_changes = cx
254 .background_spawn({
255 let new_snapshot = new_snapshot.clone();
256 async move {
257 new_snapshot
258 .edits_since::<(PointUtf16, usize)>(&old_version)
259 .map(|edit| {
260 let edit_start = edit.new.start.0;
261 let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
262 let new_text = new_snapshot
263 .text_for_range(edit.new.start.1..edit.new.end.1)
264 .collect();
265 lsp::TextDocumentContentChangeEvent {
266 range: Some(lsp::Range::new(
267 point_to_lsp(edit_start),
268 point_to_lsp(edit_end),
269 )),
270 range_length: None,
271 text: new_text,
272 }
273 })
274 .collect::<Vec<_>>()
275 }
276 })
277 .await;
278
279 copilot
280 .update(cx, |copilot, _| {
281 let server = copilot.server.as_authenticated().log_err()?;
282 let buffer = server.registered_buffers.get_mut(&id)?;
283 if !content_changes.is_empty() {
284 buffer.snapshot_version += 1;
285 buffer.snapshot = new_snapshot;
286 server
287 .lsp
288 .notify::<lsp::notification::DidChangeTextDocument>(
289 &lsp::DidChangeTextDocumentParams {
290 text_document: lsp::VersionedTextDocumentIdentifier::new(
291 buffer.uri.clone(),
292 buffer.snapshot_version,
293 ),
294 content_changes,
295 },
296 )
297 .ok();
298 }
299 let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
300 Some(())
301 })
302 .ok()?;
303
304 Some(())
305 });
306 }
307
308 done_rx
309 }
310}
311
312#[derive(Debug)]
313pub struct Completion {
314 pub uuid: String,
315 pub range: Range<Anchor>,
316 pub text: String,
317}
318
319pub struct Copilot {
320 fs: Arc<dyn Fs>,
321 node_runtime: NodeRuntime,
322 server: CopilotServer,
323 buffers: HashSet<WeakEntity<Buffer>>,
324 server_id: LanguageServerId,
325 _subscription: gpui::Subscription,
326}
327
328pub enum Event {
329 CopilotLanguageServerStarted,
330 CopilotAuthSignedIn,
331 CopilotAuthSignedOut,
332}
333
334impl EventEmitter<Event> for Copilot {}
335
336struct GlobalCopilot(Entity<Copilot>);
337
338impl Global for GlobalCopilot {}
339
340impl Copilot {
341 pub fn global(cx: &App) -> Option<Entity<Self>> {
342 cx.try_global::<GlobalCopilot>()
343 .map(|model| model.0.clone())
344 }
345
346 pub fn set_global(copilot: Entity<Self>, cx: &mut App) {
347 cx.set_global(GlobalCopilot(copilot));
348 }
349
350 fn start(
351 new_server_id: LanguageServerId,
352 fs: Arc<dyn Fs>,
353 node_runtime: NodeRuntime,
354 cx: &mut Context<Self>,
355 ) -> Self {
356 let mut this = Self {
357 server_id: new_server_id,
358 fs,
359 node_runtime,
360 server: CopilotServer::Disabled,
361 buffers: Default::default(),
362 _subscription: cx.on_app_quit(Self::shutdown_language_server),
363 };
364 this.start_copilot(true, false, cx);
365 cx.observe_global::<SettingsStore>(move |this, cx| {
366 this.start_copilot(true, false, cx);
367 this.send_configuration_update(cx);
368 })
369 .detach();
370 this
371 }
372
373 fn shutdown_language_server(
374 &mut self,
375 _cx: &mut Context<Self>,
376 ) -> impl Future<Output = ()> + use<> {
377 let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) {
378 CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })),
379 _ => None,
380 };
381
382 async move {
383 if let Some(shutdown) = shutdown {
384 shutdown.await;
385 }
386 }
387 }
388
389 fn start_copilot(
390 &mut self,
391 check_edit_prediction_provider: bool,
392 awaiting_sign_in_after_start: bool,
393 cx: &mut Context<Self>,
394 ) {
395 if !matches!(self.server, CopilotServer::Disabled) {
396 return;
397 }
398 let language_settings = all_language_settings(None, cx);
399 if check_edit_prediction_provider
400 && language_settings.edit_predictions.provider != EditPredictionProvider::Copilot
401 {
402 return;
403 }
404 let server_id = self.server_id;
405 let fs = self.fs.clone();
406 let node_runtime = self.node_runtime.clone();
407 let env = self.build_env(&language_settings.edit_predictions.copilot);
408 let start_task = cx
409 .spawn(async move |this, cx| {
410 Self::start_language_server(
411 server_id,
412 fs,
413 node_runtime,
414 env,
415 this,
416 awaiting_sign_in_after_start,
417 cx,
418 )
419 .await
420 })
421 .shared();
422 self.server = CopilotServer::Starting { task: start_task };
423 cx.notify();
424 }
425
426 fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
427 let proxy_url = copilot_settings.proxy.clone()?;
428 let no_verify = copilot_settings.proxy_no_verify;
429 let http_or_https_proxy = if proxy_url.starts_with("http:") {
430 Some("HTTP_PROXY")
431 } else if proxy_url.starts_with("https:") {
432 Some("HTTPS_PROXY")
433 } else {
434 log::error!(
435 "Unsupported protocol scheme for language server proxy (must be http or https)"
436 );
437 None
438 };
439
440 let mut env = HashMap::default();
441
442 if let Some(proxy_type) = http_or_https_proxy {
443 env.insert(proxy_type.to_string(), proxy_url);
444 if let Some(true) = no_verify {
445 env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
446 };
447 }
448
449 if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {
450 env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
451 }
452
453 if env.is_empty() { None } else { Some(env) }
454 }
455
456 fn send_configuration_update(&mut self, cx: &mut Context<Self>) {
457 let copilot_settings = all_language_settings(None, cx)
458 .edit_predictions
459 .copilot
460 .clone();
461
462 let settings = json!({
463 "http": {
464 "proxy": copilot_settings.proxy,
465 "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
466 },
467 "github-enterprise": {
468 "uri": copilot_settings.enterprise_uri
469 }
470 });
471
472 if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
473 copilot_chat.update(cx, |chat, cx| {
474 chat.set_configuration(
475 copilot_chat::CopilotChatConfiguration {
476 enterprise_uri: copilot_settings.enterprise_uri.clone(),
477 },
478 cx,
479 );
480 });
481 }
482
483 if let Ok(server) = self.server.as_running() {
484 server
485 .lsp
486 .notify::<lsp::notification::DidChangeConfiguration>(
487 &lsp::DidChangeConfigurationParams { settings },
488 )
489 .log_err();
490 }
491 }
492
493 #[cfg(any(test, feature = "test-support"))]
494 pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
495 use fs::FakeFs;
496 use lsp::FakeLanguageServer;
497 use node_runtime::NodeRuntime;
498
499 let (server, fake_server) = FakeLanguageServer::new(
500 LanguageServerId(0),
501 LanguageServerBinary {
502 path: "path/to/copilot".into(),
503 arguments: vec![],
504 env: None,
505 },
506 "copilot".into(),
507 Default::default(),
508 &mut cx.to_async(),
509 );
510 let node_runtime = NodeRuntime::unavailable();
511 let this = cx.new(|cx| Self {
512 server_id: LanguageServerId(0),
513 fs: FakeFs::new(cx.background_executor().clone()),
514 node_runtime,
515 server: CopilotServer::Running(RunningCopilotServer {
516 lsp: Arc::new(server),
517 sign_in_status: SignInStatus::Authorized,
518 registered_buffers: Default::default(),
519 }),
520 _subscription: cx.on_app_quit(Self::shutdown_language_server),
521 buffers: Default::default(),
522 });
523 (this, fake_server)
524 }
525
526 async fn start_language_server(
527 new_server_id: LanguageServerId,
528 fs: Arc<dyn Fs>,
529 node_runtime: NodeRuntime,
530 env: Option<HashMap<String, String>>,
531 this: WeakEntity<Self>,
532 awaiting_sign_in_after_start: bool,
533 cx: &mut AsyncApp,
534 ) {
535 let start_language_server = async {
536 let server_path = get_copilot_lsp(fs, node_runtime.clone()).await?;
537 let node_path = node_runtime.binary_path().await?;
538 let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
539 let binary = LanguageServerBinary {
540 path: node_path,
541 arguments,
542 env,
543 };
544
545 let root_path = if cfg!(target_os = "windows") {
546 Path::new("C:/")
547 } else {
548 Path::new("/")
549 };
550
551 let server_name = LanguageServerName("copilot".into());
552 let server = LanguageServer::new(
553 Arc::new(Mutex::new(None)),
554 new_server_id,
555 server_name,
556 binary,
557 root_path,
558 None,
559 Default::default(),
560 cx,
561 )?;
562
563 server
564 .on_notification::<StatusNotification, _>(|_, _| { /* Silence the notification */ })
565 .detach();
566
567 let configuration = lsp::DidChangeConfigurationParams {
568 settings: Default::default(),
569 };
570
571 let editor_info = request::SetEditorInfoParams {
572 editor_info: request::EditorInfo {
573 name: "zed".into(),
574 version: env!("CARGO_PKG_VERSION").into(),
575 },
576 editor_plugin_info: request::EditorPluginInfo {
577 name: "zed-copilot".into(),
578 version: "0.0.1".into(),
579 },
580 };
581 let editor_info_json = serde_json::to_value(&editor_info)?;
582
583 let server = cx
584 .update(|cx| {
585 let mut params = server.default_initialize_params(false, cx);
586 params.initialization_options = Some(editor_info_json);
587 server.initialize(params, configuration.into(), cx)
588 })?
589 .await?;
590
591 let status = server
592 .request::<request::CheckStatus>(request::CheckStatusParams {
593 local_checks_only: false,
594 })
595 .await
596 .into_response()
597 .context("copilot: check status")?;
598
599 anyhow::Ok((server, status))
600 };
601
602 let server = start_language_server.await;
603 this.update(cx, |this, cx| {
604 cx.notify();
605 match server {
606 Ok((server, status)) => {
607 this.server = CopilotServer::Running(RunningCopilotServer {
608 lsp: server,
609 sign_in_status: SignInStatus::SignedOut {
610 awaiting_signing_in: awaiting_sign_in_after_start,
611 },
612 registered_buffers: Default::default(),
613 });
614 cx.emit(Event::CopilotLanguageServerStarted);
615 this.update_sign_in_status(status, cx);
616 // Send configuration now that the LSP is fully started
617 this.send_configuration_update(cx);
618 }
619 Err(error) => {
620 this.server = CopilotServer::Error(error.to_string().into());
621 cx.notify()
622 }
623 }
624 })
625 .ok();
626 }
627
628 pub(crate) fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
629 if let CopilotServer::Running(server) = &mut self.server {
630 let task = match &server.sign_in_status {
631 SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(),
632 SignInStatus::SigningIn { task, .. } => {
633 cx.notify();
634 task.clone()
635 }
636 SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => {
637 let lsp = server.lsp.clone();
638 let task = cx
639 .spawn(async move |this, cx| {
640 let sign_in = async {
641 let sign_in = lsp
642 .request::<request::SignInInitiate>(
643 request::SignInInitiateParams {},
644 )
645 .await
646 .into_response()
647 .context("copilot sign-in")?;
648 match sign_in {
649 request::SignInInitiateResult::AlreadySignedIn { user } => {
650 Ok(request::SignInStatus::Ok { user: Some(user) })
651 }
652 request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
653 this.update(cx, |this, cx| {
654 if let CopilotServer::Running(RunningCopilotServer {
655 sign_in_status: status,
656 ..
657 }) = &mut this.server
658 {
659 if let SignInStatus::SigningIn {
660 prompt: prompt_flow,
661 ..
662 } = status
663 {
664 *prompt_flow = Some(flow.clone());
665 cx.notify();
666 }
667 }
668 })?;
669 let response = lsp
670 .request::<request::SignInConfirm>(
671 request::SignInConfirmParams {
672 user_code: flow.user_code,
673 },
674 )
675 .await
676 .into_response()
677 .context("copilot: sign in confirm")?;
678 Ok(response)
679 }
680 }
681 };
682
683 let sign_in = sign_in.await;
684 this.update(cx, |this, cx| match sign_in {
685 Ok(status) => {
686 this.update_sign_in_status(status, cx);
687 Ok(())
688 }
689 Err(error) => {
690 this.update_sign_in_status(
691 request::SignInStatus::NotSignedIn,
692 cx,
693 );
694 Err(Arc::new(error))
695 }
696 })?
697 })
698 .shared();
699 server.sign_in_status = SignInStatus::SigningIn {
700 prompt: None,
701 task: task.clone(),
702 };
703 cx.notify();
704 task
705 }
706 };
707
708 cx.background_spawn(task.map_err(|err| anyhow!("{err:?}")))
709 } else {
710 // If we're downloading, wait until download is finished
711 // If we're in a stuck state, display to the user
712 Task::ready(Err(anyhow!("copilot hasn't started yet")))
713 }
714 }
715
716 pub(crate) fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
717 self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
718 match &self.server {
719 CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => {
720 let server = server.clone();
721 cx.background_spawn(async move {
722 server
723 .request::<request::SignOut>(request::SignOutParams {})
724 .await
725 .into_response()
726 .context("copilot: sign in confirm")?;
727 anyhow::Ok(())
728 })
729 }
730 CopilotServer::Disabled => cx.background_spawn(async {
731 clear_copilot_config_dir().await;
732 anyhow::Ok(())
733 }),
734 _ => Task::ready(Err(anyhow!("copilot hasn't started yet"))),
735 }
736 }
737
738 pub(crate) fn reinstall(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
739 let language_settings = all_language_settings(None, cx);
740 let env = self.build_env(&language_settings.edit_predictions.copilot);
741 let start_task = cx
742 .spawn({
743 let fs = self.fs.clone();
744 let node_runtime = self.node_runtime.clone();
745 let server_id = self.server_id;
746 async move |this, cx| {
747 clear_copilot_dir().await;
748 Self::start_language_server(server_id, fs, node_runtime, env, this, false, cx)
749 .await
750 }
751 })
752 .shared();
753
754 self.server = CopilotServer::Starting {
755 task: start_task.clone(),
756 };
757
758 cx.notify();
759
760 start_task
761 }
762
763 pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {
764 if let CopilotServer::Running(server) = &self.server {
765 Some(&server.lsp)
766 } else {
767 None
768 }
769 }
770
771 pub fn register_buffer(&mut self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) {
772 let weak_buffer = buffer.downgrade();
773 self.buffers.insert(weak_buffer.clone());
774
775 if let CopilotServer::Running(RunningCopilotServer {
776 lsp: server,
777 sign_in_status: status,
778 registered_buffers,
779 ..
780 }) = &mut self.server
781 {
782 if !matches!(status, SignInStatus::Authorized { .. }) {
783 return;
784 }
785
786 let entry = registered_buffers.entry(buffer.entity_id());
787 if let Entry::Vacant(e) = entry {
788 let Ok(uri) = uri_for_buffer(buffer, cx) else {
789 return;
790 };
791 let language_id = id_for_language(buffer.read(cx).language());
792 let snapshot = buffer.read(cx).snapshot();
793 server
794 .notify::<lsp::notification::DidOpenTextDocument>(
795 &lsp::DidOpenTextDocumentParams {
796 text_document: lsp::TextDocumentItem {
797 uri: uri.clone(),
798 language_id: language_id.clone(),
799 version: 0,
800 text: snapshot.text(),
801 },
802 },
803 )
804 .ok();
805
806 e.insert(RegisteredBuffer {
807 uri,
808 language_id,
809 snapshot,
810 snapshot_version: 0,
811 pending_buffer_change: Task::ready(Some(())),
812 _subscriptions: [
813 cx.subscribe(buffer, |this, buffer, event, cx| {
814 this.handle_buffer_event(buffer, event, cx).log_err();
815 }),
816 cx.observe_release(buffer, move |this, _buffer, _cx| {
817 this.buffers.remove(&weak_buffer);
818 this.unregister_buffer(&weak_buffer);
819 }),
820 ],
821 });
822 }
823 }
824 }
825
826 fn handle_buffer_event(
827 &mut self,
828 buffer: Entity<Buffer>,
829 event: &language::BufferEvent,
830 cx: &mut Context<Self>,
831 ) -> Result<()> {
832 if let Ok(server) = self.server.as_running() {
833 if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
834 {
835 match event {
836 language::BufferEvent::Edited => {
837 drop(registered_buffer.report_changes(&buffer, cx));
838 }
839 language::BufferEvent::Saved => {
840 server
841 .lsp
842 .notify::<lsp::notification::DidSaveTextDocument>(
843 &lsp::DidSaveTextDocumentParams {
844 text_document: lsp::TextDocumentIdentifier::new(
845 registered_buffer.uri.clone(),
846 ),
847 text: None,
848 },
849 )?;
850 }
851 language::BufferEvent::FileHandleChanged
852 | language::BufferEvent::LanguageChanged => {
853 let new_language_id = id_for_language(buffer.read(cx).language());
854 let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
855 return Ok(());
856 };
857 if new_uri != registered_buffer.uri
858 || new_language_id != registered_buffer.language_id
859 {
860 let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
861 registered_buffer.language_id = new_language_id;
862 server
863 .lsp
864 .notify::<lsp::notification::DidCloseTextDocument>(
865 &lsp::DidCloseTextDocumentParams {
866 text_document: lsp::TextDocumentIdentifier::new(old_uri),
867 },
868 )?;
869 server
870 .lsp
871 .notify::<lsp::notification::DidOpenTextDocument>(
872 &lsp::DidOpenTextDocumentParams {
873 text_document: lsp::TextDocumentItem::new(
874 registered_buffer.uri.clone(),
875 registered_buffer.language_id.clone(),
876 registered_buffer.snapshot_version,
877 registered_buffer.snapshot.text(),
878 ),
879 },
880 )?;
881 }
882 }
883 _ => {}
884 }
885 }
886 }
887
888 Ok(())
889 }
890
891 fn unregister_buffer(&mut self, buffer: &WeakEntity<Buffer>) {
892 if let Ok(server) = self.server.as_running() {
893 if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) {
894 server
895 .lsp
896 .notify::<lsp::notification::DidCloseTextDocument>(
897 &lsp::DidCloseTextDocumentParams {
898 text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
899 },
900 )
901 .ok();
902 }
903 }
904 }
905
906 pub fn completions<T>(
907 &mut self,
908 buffer: &Entity<Buffer>,
909 position: T,
910 cx: &mut Context<Self>,
911 ) -> Task<Result<Vec<Completion>>>
912 where
913 T: ToPointUtf16,
914 {
915 self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
916 }
917
918 pub fn completions_cycling<T>(
919 &mut self,
920 buffer: &Entity<Buffer>,
921 position: T,
922 cx: &mut Context<Self>,
923 ) -> Task<Result<Vec<Completion>>>
924 where
925 T: ToPointUtf16,
926 {
927 self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
928 }
929
930 pub fn accept_completion(
931 &mut self,
932 completion: &Completion,
933 cx: &mut Context<Self>,
934 ) -> Task<Result<()>> {
935 let server = match self.server.as_authenticated() {
936 Ok(server) => server,
937 Err(error) => return Task::ready(Err(error)),
938 };
939 let request =
940 server
941 .lsp
942 .request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
943 uuid: completion.uuid.clone(),
944 });
945 cx.background_spawn(async move {
946 request
947 .await
948 .into_response()
949 .context("copilot: notify accepted")?;
950 Ok(())
951 })
952 }
953
954 pub fn discard_completions(
955 &mut self,
956 completions: &[Completion],
957 cx: &mut Context<Self>,
958 ) -> Task<Result<()>> {
959 let server = match self.server.as_authenticated() {
960 Ok(server) => server,
961 Err(_) => return Task::ready(Ok(())),
962 };
963 let request =
964 server
965 .lsp
966 .request::<request::NotifyRejected>(request::NotifyRejectedParams {
967 uuids: completions
968 .iter()
969 .map(|completion| completion.uuid.clone())
970 .collect(),
971 });
972 cx.background_spawn(async move {
973 request
974 .await
975 .into_response()
976 .context("copilot: notify rejected")?;
977 Ok(())
978 })
979 }
980
981 fn request_completions<R, T>(
982 &mut self,
983 buffer: &Entity<Buffer>,
984 position: T,
985 cx: &mut Context<Self>,
986 ) -> Task<Result<Vec<Completion>>>
987 where
988 R: 'static
989 + lsp::request::Request<
990 Params = request::GetCompletionsParams,
991 Result = request::GetCompletionsResult,
992 >,
993 T: ToPointUtf16,
994 {
995 self.register_buffer(buffer, cx);
996
997 let server = match self.server.as_authenticated() {
998 Ok(server) => server,
999 Err(error) => return Task::ready(Err(error)),
1000 };
1001 let lsp = server.lsp.clone();
1002 let registered_buffer = server
1003 .registered_buffers
1004 .get_mut(&buffer.entity_id())
1005 .unwrap();
1006 let snapshot = registered_buffer.report_changes(buffer, cx);
1007 let buffer = buffer.read(cx);
1008 let uri = registered_buffer.uri.clone();
1009 let position = position.to_point_utf16(buffer);
1010 let settings = language_settings(
1011 buffer.language_at(position).map(|l| l.name()),
1012 buffer.file(),
1013 cx,
1014 );
1015 let tab_size = settings.tab_size;
1016 let hard_tabs = settings.hard_tabs;
1017 let relative_path = buffer
1018 .file()
1019 .map(|file| file.path().to_path_buf())
1020 .unwrap_or_default();
1021
1022 cx.background_spawn(async move {
1023 let (version, snapshot) = snapshot.await?;
1024 let result = lsp
1025 .request::<R>(request::GetCompletionsParams {
1026 doc: request::GetCompletionsDocument {
1027 uri,
1028 tab_size: tab_size.into(),
1029 indent_size: 1,
1030 insert_spaces: !hard_tabs,
1031 relative_path: relative_path.to_string_lossy().into(),
1032 position: point_to_lsp(position),
1033 version: version.try_into().unwrap(),
1034 },
1035 })
1036 .await
1037 .into_response()
1038 .context("copilot: get completions")?;
1039 let completions = result
1040 .completions
1041 .into_iter()
1042 .map(|completion| {
1043 let start = snapshot
1044 .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
1045 let end =
1046 snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
1047 Completion {
1048 uuid: completion.uuid,
1049 range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
1050 text: completion.text,
1051 }
1052 })
1053 .collect();
1054 anyhow::Ok(completions)
1055 })
1056 }
1057
1058 pub fn status(&self) -> Status {
1059 match &self.server {
1060 CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
1061 CopilotServer::Disabled => Status::Disabled,
1062 CopilotServer::Error(error) => Status::Error(error.clone()),
1063 CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
1064 match sign_in_status {
1065 SignInStatus::Authorized { .. } => Status::Authorized,
1066 SignInStatus::Unauthorized { .. } => Status::Unauthorized,
1067 SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
1068 prompt: prompt.clone(),
1069 },
1070 SignInStatus::SignedOut {
1071 awaiting_signing_in,
1072 } => Status::SignedOut {
1073 awaiting_signing_in: *awaiting_signing_in,
1074 },
1075 }
1076 }
1077 }
1078 }
1079
1080 fn update_sign_in_status(&mut self, lsp_status: request::SignInStatus, cx: &mut Context<Self>) {
1081 self.buffers.retain(|buffer| buffer.is_upgradable());
1082
1083 if let Ok(server) = self.server.as_running() {
1084 match lsp_status {
1085 request::SignInStatus::Ok { user: Some(_) }
1086 | request::SignInStatus::MaybeOk { .. }
1087 | request::SignInStatus::AlreadySignedIn { .. } => {
1088 server.sign_in_status = SignInStatus::Authorized;
1089 cx.emit(Event::CopilotAuthSignedIn);
1090 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1091 if let Some(buffer) = buffer.upgrade() {
1092 self.register_buffer(&buffer, cx);
1093 }
1094 }
1095 }
1096 request::SignInStatus::NotAuthorized { .. } => {
1097 server.sign_in_status = SignInStatus::Unauthorized;
1098 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1099 self.unregister_buffer(&buffer);
1100 }
1101 }
1102 request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => {
1103 if !matches!(server.sign_in_status, SignInStatus::SignedOut { .. }) {
1104 server.sign_in_status = SignInStatus::SignedOut {
1105 awaiting_signing_in: false,
1106 };
1107 }
1108 cx.emit(Event::CopilotAuthSignedOut);
1109 for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
1110 self.unregister_buffer(&buffer);
1111 }
1112 }
1113 }
1114
1115 cx.notify();
1116 }
1117 }
1118}
1119
1120fn id_for_language(language: Option<&Arc<Language>>) -> String {
1121 language
1122 .map(|language| language.lsp_id())
1123 .unwrap_or_else(|| "plaintext".to_string())
1124}
1125
1126fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
1127 if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
1128 lsp::Url::from_file_path(file.abs_path(cx))
1129 } else {
1130 format!("buffer://{}", buffer.entity_id())
1131 .parse()
1132 .map_err(|_| ())
1133 }
1134}
1135
1136async fn clear_copilot_dir() {
1137 remove_matching(paths::copilot_dir(), |_| true).await
1138}
1139
1140async fn clear_copilot_config_dir() {
1141 remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
1142}
1143
1144async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::Result<PathBuf> {
1145 const PACKAGE_NAME: &str = "@github/copilot-language-server";
1146 const SERVER_PATH: &str =
1147 "node_modules/@github/copilot-language-server/dist/language-server.js";
1148
1149 let latest_version = node_runtime
1150 .npm_package_latest_version(PACKAGE_NAME)
1151 .await?;
1152 let server_path = paths::copilot_dir().join(SERVER_PATH);
1153
1154 fs.create_dir(paths::copilot_dir()).await?;
1155
1156 let should_install = node_runtime
1157 .should_install_npm_package(
1158 PACKAGE_NAME,
1159 &server_path,
1160 paths::copilot_dir(),
1161 &latest_version,
1162 )
1163 .await;
1164 if should_install {
1165 node_runtime
1166 .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
1167 .await?;
1168 }
1169
1170 Ok(server_path)
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 use super::*;
1176 use gpui::TestAppContext;
1177 use util::path;
1178
1179 #[gpui::test(iterations = 10)]
1180 async fn test_buffer_management(cx: &mut TestAppContext) {
1181 let (copilot, mut lsp) = Copilot::fake(cx);
1182
1183 let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx));
1184 let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
1185 .parse()
1186 .unwrap();
1187 copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
1188 assert_eq!(
1189 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1190 .await,
1191 lsp::DidOpenTextDocumentParams {
1192 text_document: lsp::TextDocumentItem::new(
1193 buffer_1_uri.clone(),
1194 "plaintext".into(),
1195 0,
1196 "Hello".into()
1197 ),
1198 }
1199 );
1200
1201 let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx));
1202 let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
1203 .parse()
1204 .unwrap();
1205 copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
1206 assert_eq!(
1207 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1208 .await,
1209 lsp::DidOpenTextDocumentParams {
1210 text_document: lsp::TextDocumentItem::new(
1211 buffer_2_uri.clone(),
1212 "plaintext".into(),
1213 0,
1214 "Goodbye".into()
1215 ),
1216 }
1217 );
1218
1219 buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
1220 assert_eq!(
1221 lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
1222 .await,
1223 lsp::DidChangeTextDocumentParams {
1224 text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
1225 content_changes: vec![lsp::TextDocumentContentChangeEvent {
1226 range: Some(lsp::Range::new(
1227 lsp::Position::new(0, 5),
1228 lsp::Position::new(0, 5)
1229 )),
1230 range_length: None,
1231 text: " world".into(),
1232 }],
1233 }
1234 );
1235
1236 // Ensure updates to the file are reflected in the LSP.
1237 buffer_1.update(cx, |buffer, cx| {
1238 buffer.file_updated(
1239 Arc::new(File {
1240 abs_path: path!("/root/child/buffer-1").into(),
1241 path: Path::new("child/buffer-1").into(),
1242 }),
1243 cx,
1244 )
1245 });
1246 assert_eq!(
1247 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1248 .await,
1249 lsp::DidCloseTextDocumentParams {
1250 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
1251 }
1252 );
1253 let buffer_1_uri = lsp::Url::from_file_path(path!("/root/child/buffer-1")).unwrap();
1254 assert_eq!(
1255 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1256 .await,
1257 lsp::DidOpenTextDocumentParams {
1258 text_document: lsp::TextDocumentItem::new(
1259 buffer_1_uri.clone(),
1260 "plaintext".into(),
1261 1,
1262 "Hello world".into()
1263 ),
1264 }
1265 );
1266
1267 // Ensure all previously-registered buffers are closed when signing out.
1268 lsp.set_request_handler::<request::SignOut, _, _>(|_, _| async {
1269 Ok(request::SignOutResult {})
1270 });
1271 copilot
1272 .update(cx, |copilot, cx| copilot.sign_out(cx))
1273 .await
1274 .unwrap();
1275 assert_eq!(
1276 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1277 .await,
1278 lsp::DidCloseTextDocumentParams {
1279 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
1280 }
1281 );
1282 assert_eq!(
1283 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1284 .await,
1285 lsp::DidCloseTextDocumentParams {
1286 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
1287 }
1288 );
1289
1290 // Ensure all previously-registered buffers are re-opened when signing in.
1291 lsp.set_request_handler::<request::SignInInitiate, _, _>(|_, _| async {
1292 Ok(request::SignInInitiateResult::AlreadySignedIn {
1293 user: "user-1".into(),
1294 })
1295 });
1296 copilot
1297 .update(cx, |copilot, cx| copilot.sign_in(cx))
1298 .await
1299 .unwrap();
1300
1301 assert_eq!(
1302 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1303 .await,
1304 lsp::DidOpenTextDocumentParams {
1305 text_document: lsp::TextDocumentItem::new(
1306 buffer_1_uri.clone(),
1307 "plaintext".into(),
1308 0,
1309 "Hello world".into()
1310 ),
1311 }
1312 );
1313 assert_eq!(
1314 lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
1315 .await,
1316 lsp::DidOpenTextDocumentParams {
1317 text_document: lsp::TextDocumentItem::new(
1318 buffer_2_uri.clone(),
1319 "plaintext".into(),
1320 0,
1321 "Goodbye".into()
1322 ),
1323 }
1324 );
1325 // Dropping a buffer causes it to be closed on the LSP side as well.
1326 cx.update(|_| drop(buffer_2));
1327 assert_eq!(
1328 lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
1329 .await,
1330 lsp::DidCloseTextDocumentParams {
1331 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
1332 }
1333 );
1334 }
1335
1336 struct File {
1337 abs_path: PathBuf,
1338 path: Arc<Path>,
1339 }
1340
1341 impl language::File for File {
1342 fn as_local(&self) -> Option<&dyn language::LocalFile> {
1343 Some(self)
1344 }
1345
1346 fn disk_state(&self) -> language::DiskState {
1347 language::DiskState::Present {
1348 mtime: ::fs::MTime::from_seconds_and_nanos(100, 42),
1349 }
1350 }
1351
1352 fn path(&self) -> &Arc<Path> {
1353 &self.path
1354 }
1355
1356 fn full_path(&self, _: &App) -> PathBuf {
1357 unimplemented!()
1358 }
1359
1360 fn file_name<'a>(&'a self, _: &'a App) -> &'a std::ffi::OsStr {
1361 unimplemented!()
1362 }
1363
1364 fn to_proto(&self, _: &App) -> rpc::proto::File {
1365 unimplemented!()
1366 }
1367
1368 fn worktree_id(&self, _: &App) -> settings::WorktreeId {
1369 settings::WorktreeId::from_usize(0)
1370 }
1371
1372 fn is_private(&self) -> bool {
1373 false
1374 }
1375 }
1376
1377 impl language::LocalFile for File {
1378 fn abs_path(&self, _: &App) -> PathBuf {
1379 self.abs_path.clone()
1380 }
1381
1382 fn load(&self, _: &App) -> Task<Result<String>> {
1383 unimplemented!()
1384 }
1385
1386 fn load_bytes(&self, _cx: &App) -> Task<Result<Vec<u8>>> {
1387 unimplemented!()
1388 }
1389 }
1390}
1391
1392#[cfg(test)]
1393#[ctor::ctor]
1394fn init_logger() {
1395 zlog::init_test();
1396}