1pub mod request;
2mod sign_in;
3
4use anyhow::{anyhow, Context, Result};
5use async_compression::futures::bufread::GzipDecoder;
6use async_tar::Archive;
7use collections::HashMap;
8use futures::{future::Shared, Future, FutureExt, TryFutureExt};
9use gpui::{
10 actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
11};
12use language::{
13 point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
14 ToPointUtf16,
15};
16use log::{debug, error};
17use lsp::LanguageServer;
18use node_runtime::NodeRuntime;
19use request::{LogMessage, StatusNotification};
20use settings::Settings;
21use smol::{fs, io::BufReader, stream::StreamExt};
22use std::{
23 ffi::OsString,
24 mem,
25 ops::Range,
26 path::{Path, PathBuf},
27 sync::Arc,
28};
29use util::{
30 channel::ReleaseChannel, fs::remove_matching, github::latest_github_release, http::HttpClient,
31 paths, ResultExt,
32};
33
34const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
35actions!(copilot_auth, [SignIn, SignOut]);
36
37const COPILOT_NAMESPACE: &'static str = "copilot";
38actions!(
39 copilot,
40 [Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
41);
42
43pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
44 // Disable Copilot for stable releases.
45 if *cx.global::<ReleaseChannel>() == ReleaseChannel::Stable {
46 cx.update_global::<collections::CommandPaletteFilter, _, _>(|filter, _cx| {
47 filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
48 filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
49 });
50 return;
51 }
52
53 let copilot = cx.add_model({
54 let node_runtime = node_runtime.clone();
55 move |cx| Copilot::start(http, node_runtime, cx)
56 });
57 cx.set_global(copilot.clone());
58
59 cx.observe(&copilot, |handle, cx| {
60 let status = handle.read(cx).status();
61 cx.update_global::<collections::CommandPaletteFilter, _, _>(
62 move |filter, _cx| match status {
63 Status::Disabled => {
64 filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
65 filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
66 }
67 Status::Authorized => {
68 filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
69 filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
70 }
71 _ => {
72 filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
73 filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
74 }
75 },
76 );
77 })
78 .detach();
79
80 sign_in::init(cx);
81 cx.add_global_action(|_: &SignIn, cx| {
82 if let Some(copilot) = Copilot::global(cx) {
83 copilot
84 .update(cx, |copilot, cx| copilot.sign_in(cx))
85 .detach_and_log_err(cx);
86 }
87 });
88 cx.add_global_action(|_: &SignOut, cx| {
89 if let Some(copilot) = Copilot::global(cx) {
90 copilot
91 .update(cx, |copilot, cx| copilot.sign_out(cx))
92 .detach_and_log_err(cx);
93 }
94 });
95
96 cx.add_global_action(|_: &Reinstall, cx| {
97 if let Some(copilot) = Copilot::global(cx) {
98 copilot
99 .update(cx, |copilot, cx| copilot.reinstall(cx))
100 .detach();
101 }
102 });
103}
104
105enum CopilotServer {
106 Disabled,
107 Starting {
108 task: Shared<Task<()>>,
109 },
110 Error(Arc<str>),
111 Started {
112 server: Arc<LanguageServer>,
113 status: SignInStatus,
114 registered_buffers: HashMap<usize, RegisteredBuffer>,
115 },
116}
117
118#[derive(Clone, Debug)]
119enum SignInStatus {
120 Authorized,
121 Unauthorized,
122 SigningIn {
123 prompt: Option<request::PromptUserDeviceFlow>,
124 task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
125 },
126 SignedOut,
127}
128
129#[derive(Debug, Clone)]
130pub enum Status {
131 Starting {
132 task: Shared<Task<()>>,
133 },
134 Error(Arc<str>),
135 Disabled,
136 SignedOut,
137 SigningIn {
138 prompt: Option<request::PromptUserDeviceFlow>,
139 },
140 Unauthorized,
141 Authorized,
142}
143
144impl Status {
145 pub fn is_authorized(&self) -> bool {
146 matches!(self, Status::Authorized)
147 }
148}
149
150struct RegisteredBuffer {
151 uri: lsp::Url,
152 language_id: String,
153 snapshot: BufferSnapshot,
154 snapshot_version: i32,
155 _subscriptions: [gpui::Subscription; 2],
156}
157
158impl RegisteredBuffer {
159 fn report_changes(
160 &mut self,
161 buffer: &ModelHandle<Buffer>,
162 server: &LanguageServer,
163 cx: &AppContext,
164 ) -> Result<()> {
165 let buffer = buffer.read(cx);
166 let new_snapshot = buffer.snapshot();
167 let content_changes = buffer
168 .edits_since::<(PointUtf16, usize)>(self.snapshot.version())
169 .map(|edit| {
170 let edit_start = edit.new.start.0;
171 let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
172 let new_text = new_snapshot
173 .text_for_range(edit.new.start.1..edit.new.end.1)
174 .collect();
175 lsp::TextDocumentContentChangeEvent {
176 range: Some(lsp::Range::new(
177 point_to_lsp(edit_start),
178 point_to_lsp(edit_end),
179 )),
180 range_length: None,
181 text: new_text,
182 }
183 })
184 .collect::<Vec<_>>();
185
186 if !content_changes.is_empty() {
187 self.snapshot_version += 1;
188 self.snapshot = new_snapshot;
189 server.notify::<lsp::notification::DidChangeTextDocument>(
190 lsp::DidChangeTextDocumentParams {
191 text_document: lsp::VersionedTextDocumentIdentifier::new(
192 self.uri.clone(),
193 self.snapshot_version,
194 ),
195 content_changes,
196 },
197 )?;
198 }
199
200 Ok(())
201 }
202}
203
204#[derive(Debug, PartialEq, Eq)]
205pub struct Completion {
206 pub range: Range<Anchor>,
207 pub text: String,
208}
209
210pub struct Copilot {
211 http: Arc<dyn HttpClient>,
212 node_runtime: Arc<NodeRuntime>,
213 server: CopilotServer,
214 buffers: HashMap<usize, WeakModelHandle<Buffer>>,
215}
216
217impl Entity for Copilot {
218 type Event = ();
219}
220
221impl Copilot {
222 pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
223 if cx.has_global::<ModelHandle<Self>>() {
224 Some(cx.global::<ModelHandle<Self>>().clone())
225 } else {
226 None
227 }
228 }
229
230 fn start(
231 http: Arc<dyn HttpClient>,
232 node_runtime: Arc<NodeRuntime>,
233 cx: &mut ModelContext<Self>,
234 ) -> Self {
235 cx.observe_global::<Settings, _>({
236 let http = http.clone();
237 let node_runtime = node_runtime.clone();
238 move |this, cx| {
239 if cx.global::<Settings>().features.copilot {
240 if matches!(this.server, CopilotServer::Disabled) {
241 let start_task = cx
242 .spawn({
243 let http = http.clone();
244 let node_runtime = node_runtime.clone();
245 move |this, cx| {
246 Self::start_language_server(http, node_runtime, this, cx)
247 }
248 })
249 .shared();
250 this.server = CopilotServer::Starting { task: start_task };
251 cx.notify();
252 }
253 } else {
254 this.server = CopilotServer::Disabled;
255 cx.notify();
256 }
257 }
258 })
259 .detach();
260
261 if cx.global::<Settings>().features.copilot {
262 let start_task = cx
263 .spawn({
264 let http = http.clone();
265 let node_runtime = node_runtime.clone();
266 move |this, cx| async {
267 Self::start_language_server(http, node_runtime, this, cx).await
268 }
269 })
270 .shared();
271
272 Self {
273 http,
274 node_runtime,
275 server: CopilotServer::Starting { task: start_task },
276 buffers: Default::default(),
277 }
278 } else {
279 Self {
280 http,
281 node_runtime,
282 server: CopilotServer::Disabled,
283 buffers: Default::default(),
284 }
285 }
286 }
287
288 #[cfg(any(test, feature = "test-support"))]
289 pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
290 let (server, fake_server) =
291 LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
292 let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
293 let this = cx.add_model(|cx| Self {
294 http: http.clone(),
295 node_runtime: NodeRuntime::new(http, cx.background().clone()),
296 server: CopilotServer::Started {
297 server: Arc::new(server),
298 status: SignInStatus::Authorized,
299 registered_buffers: Default::default(),
300 },
301 buffers: Default::default(),
302 });
303 (this, fake_server)
304 }
305
306 fn start_language_server(
307 http: Arc<dyn HttpClient>,
308 node_runtime: Arc<NodeRuntime>,
309 this: ModelHandle<Self>,
310 mut cx: AsyncAppContext,
311 ) -> impl Future<Output = ()> {
312 async move {
313 let start_language_server = async {
314 let server_path = get_copilot_lsp(http).await?;
315 let node_path = node_runtime.binary_path().await?;
316 let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
317 let server = LanguageServer::new(
318 0,
319 &node_path,
320 arguments,
321 Path::new("/"),
322 None,
323 cx.clone(),
324 )?;
325
326 let server = server.initialize(Default::default()).await?;
327 let status = server
328 .request::<request::CheckStatus>(request::CheckStatusParams {
329 local_checks_only: false,
330 })
331 .await?;
332
333 server
334 .on_notification::<LogMessage, _>(|params, _cx| {
335 match params.level {
336 // Copilot is pretty agressive about logging
337 0 => debug!("copilot: {}", params.message),
338 1 => debug!("copilot: {}", params.message),
339 _ => error!("copilot: {}", params.message),
340 }
341
342 debug!("copilot metadata: {}", params.metadata_str);
343 debug!("copilot extra: {:?}", params.extra);
344 })
345 .detach();
346
347 server
348 .on_notification::<StatusNotification, _>(
349 |_, _| { /* Silence the notification */ },
350 )
351 .detach();
352
353 anyhow::Ok((server, status))
354 };
355
356 let server = start_language_server.await;
357 this.update(&mut cx, |this, cx| {
358 cx.notify();
359 match server {
360 Ok((server, status)) => {
361 this.server = CopilotServer::Started {
362 server,
363 status: SignInStatus::SignedOut,
364 registered_buffers: Default::default(),
365 };
366 this.update_sign_in_status(status, cx);
367 }
368 Err(error) => {
369 this.server = CopilotServer::Error(error.to_string().into());
370 cx.notify()
371 }
372 }
373 })
374 }
375 }
376
377 fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
378 if let CopilotServer::Started { server, status, .. } = &mut self.server {
379 let task = match status {
380 SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
381 Task::ready(Ok(())).shared()
382 }
383 SignInStatus::SigningIn { task, .. } => {
384 cx.notify();
385 task.clone()
386 }
387 SignInStatus::SignedOut => {
388 let server = server.clone();
389 let task = cx
390 .spawn(|this, mut cx| async move {
391 let sign_in = async {
392 let sign_in = server
393 .request::<request::SignInInitiate>(
394 request::SignInInitiateParams {},
395 )
396 .await?;
397 match sign_in {
398 request::SignInInitiateResult::AlreadySignedIn { user } => {
399 Ok(request::SignInStatus::Ok { user })
400 }
401 request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
402 this.update(&mut cx, |this, cx| {
403 if let CopilotServer::Started { status, .. } =
404 &mut this.server
405 {
406 if let SignInStatus::SigningIn {
407 prompt: prompt_flow,
408 ..
409 } = status
410 {
411 *prompt_flow = Some(flow.clone());
412 cx.notify();
413 }
414 }
415 });
416 let response = server
417 .request::<request::SignInConfirm>(
418 request::SignInConfirmParams {
419 user_code: flow.user_code,
420 },
421 )
422 .await?;
423 Ok(response)
424 }
425 }
426 };
427
428 let sign_in = sign_in.await;
429 this.update(&mut cx, |this, cx| match sign_in {
430 Ok(status) => {
431 this.update_sign_in_status(status, cx);
432 Ok(())
433 }
434 Err(error) => {
435 this.update_sign_in_status(
436 request::SignInStatus::NotSignedIn,
437 cx,
438 );
439 Err(Arc::new(error))
440 }
441 })
442 })
443 .shared();
444 *status = SignInStatus::SigningIn {
445 prompt: None,
446 task: task.clone(),
447 };
448 cx.notify();
449 task
450 }
451 };
452
453 cx.foreground()
454 .spawn(task.map_err(|err| anyhow!("{:?}", err)))
455 } else {
456 // If we're downloading, wait until download is finished
457 // If we're in a stuck state, display to the user
458 Task::ready(Err(anyhow!("copilot hasn't started yet")))
459 }
460 }
461
462 fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
463 self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
464 if let CopilotServer::Started { server, .. } = &self.server {
465 let server = server.clone();
466 cx.background().spawn(async move {
467 server
468 .request::<request::SignOut>(request::SignOutParams {})
469 .await?;
470 anyhow::Ok(())
471 })
472 } else {
473 Task::ready(Err(anyhow!("copilot hasn't started yet")))
474 }
475 }
476
477 fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
478 let start_task = cx
479 .spawn({
480 let http = self.http.clone();
481 let node_runtime = self.node_runtime.clone();
482 move |this, cx| async move {
483 clear_copilot_dir().await;
484 Self::start_language_server(http, node_runtime, this, cx).await
485 }
486 })
487 .shared();
488
489 self.server = CopilotServer::Starting {
490 task: start_task.clone(),
491 };
492
493 cx.notify();
494
495 cx.foreground().spawn(start_task)
496 }
497
498 pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
499 let buffer_id = buffer.id();
500 self.buffers.insert(buffer_id, buffer.downgrade());
501
502 if let CopilotServer::Started {
503 server,
504 status,
505 registered_buffers,
506 ..
507 } = &mut self.server
508 {
509 if !matches!(status, SignInStatus::Authorized { .. }) {
510 return;
511 }
512
513 registered_buffers.entry(buffer.id()).or_insert_with(|| {
514 let uri: lsp::Url = uri_for_buffer(buffer, cx);
515 let language_id = id_for_language(buffer.read(cx).language());
516 let snapshot = buffer.read(cx).snapshot();
517 server
518 .notify::<lsp::notification::DidOpenTextDocument>(
519 lsp::DidOpenTextDocumentParams {
520 text_document: lsp::TextDocumentItem {
521 uri: uri.clone(),
522 language_id: language_id.clone(),
523 version: 0,
524 text: snapshot.text(),
525 },
526 },
527 )
528 .log_err();
529
530 RegisteredBuffer {
531 uri,
532 language_id,
533 snapshot,
534 snapshot_version: 0,
535 _subscriptions: [
536 cx.subscribe(buffer, |this, buffer, event, cx| {
537 this.handle_buffer_event(buffer, event, cx).log_err();
538 }),
539 cx.observe_release(buffer, move |this, _buffer, _cx| {
540 this.buffers.remove(&buffer_id);
541 this.unregister_buffer(buffer_id);
542 }),
543 ],
544 }
545 });
546 }
547 }
548
549 fn handle_buffer_event(
550 &mut self,
551 buffer: ModelHandle<Buffer>,
552 event: &language::Event,
553 cx: &mut ModelContext<Self>,
554 ) -> Result<()> {
555 if let CopilotServer::Started {
556 server,
557 registered_buffers,
558 ..
559 } = &mut self.server
560 {
561 if let Some(registered_buffer) = registered_buffers.get_mut(&buffer.id()) {
562 match event {
563 language::Event::Edited => {
564 registered_buffer.report_changes(&buffer, server, cx)?;
565 }
566 language::Event::Saved => {
567 server.notify::<lsp::notification::DidSaveTextDocument>(
568 lsp::DidSaveTextDocumentParams {
569 text_document: lsp::TextDocumentIdentifier::new(
570 registered_buffer.uri.clone(),
571 ),
572 text: None,
573 },
574 )?;
575 }
576 language::Event::FileHandleChanged | language::Event::LanguageChanged => {
577 let new_language_id = id_for_language(buffer.read(cx).language());
578 let new_uri = uri_for_buffer(&buffer, cx);
579 if new_uri != registered_buffer.uri
580 || new_language_id != registered_buffer.language_id
581 {
582 let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
583 registered_buffer.language_id = new_language_id;
584 server.notify::<lsp::notification::DidCloseTextDocument>(
585 lsp::DidCloseTextDocumentParams {
586 text_document: lsp::TextDocumentIdentifier::new(old_uri),
587 },
588 )?;
589 server.notify::<lsp::notification::DidOpenTextDocument>(
590 lsp::DidOpenTextDocumentParams {
591 text_document: lsp::TextDocumentItem::new(
592 registered_buffer.uri.clone(),
593 registered_buffer.language_id.clone(),
594 registered_buffer.snapshot_version,
595 registered_buffer.snapshot.text(),
596 ),
597 },
598 )?;
599 }
600 }
601 _ => {}
602 }
603 }
604 }
605
606 Ok(())
607 }
608
609 fn unregister_buffer(&mut self, buffer_id: usize) {
610 if let CopilotServer::Started {
611 server,
612 registered_buffers,
613 ..
614 } = &mut self.server
615 {
616 if let Some(buffer) = registered_buffers.remove(&buffer_id) {
617 server
618 .notify::<lsp::notification::DidCloseTextDocument>(
619 lsp::DidCloseTextDocumentParams {
620 text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
621 },
622 )
623 .log_err();
624 }
625 }
626 }
627
628 pub fn completions<T>(
629 &mut self,
630 buffer: &ModelHandle<Buffer>,
631 position: T,
632 cx: &mut ModelContext<Self>,
633 ) -> Task<Result<Vec<Completion>>>
634 where
635 T: ToPointUtf16,
636 {
637 self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
638 }
639
640 pub fn completions_cycling<T>(
641 &mut self,
642 buffer: &ModelHandle<Buffer>,
643 position: T,
644 cx: &mut ModelContext<Self>,
645 ) -> Task<Result<Vec<Completion>>>
646 where
647 T: ToPointUtf16,
648 {
649 self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
650 }
651
652 fn request_completions<R, T>(
653 &mut self,
654 buffer: &ModelHandle<Buffer>,
655 position: T,
656 cx: &mut ModelContext<Self>,
657 ) -> Task<Result<Vec<Completion>>>
658 where
659 R: 'static
660 + lsp::request::Request<
661 Params = request::GetCompletionsParams,
662 Result = request::GetCompletionsResult,
663 >,
664 T: ToPointUtf16,
665 {
666 self.register_buffer(buffer, cx);
667 let (server, registered_buffer) = match &mut self.server {
668 CopilotServer::Starting { .. } => {
669 return Task::ready(Err(anyhow!("copilot is still starting")))
670 }
671 CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))),
672 CopilotServer::Error(error) => {
673 return Task::ready(Err(anyhow!(
674 "copilot was not started because of an error: {}",
675 error
676 )))
677 }
678 CopilotServer::Started {
679 server,
680 status,
681 registered_buffers,
682 ..
683 } => {
684 if matches!(status, SignInStatus::Authorized { .. }) {
685 let registered_buffer = registered_buffers.get_mut(&buffer.id()).unwrap();
686 if let Err(error) = registered_buffer.report_changes(buffer, &server, cx) {
687 return Task::ready(Err(error));
688 }
689 (server.clone(), registered_buffer)
690 } else {
691 return Task::ready(Err(anyhow!("must sign in before using copilot")));
692 }
693 }
694 };
695
696 let uri = registered_buffer.uri.clone();
697 let snapshot = registered_buffer.snapshot.clone();
698 let version = registered_buffer.snapshot_version;
699 let settings = cx.global::<Settings>();
700 let position = position.to_point_utf16(&snapshot);
701 let language = snapshot.language_at(position);
702 let language_name = language.map(|language| language.name());
703 let language_name = language_name.as_deref();
704 let tab_size = settings.tab_size(language_name);
705 let hard_tabs = settings.hard_tabs(language_name);
706 let relative_path = snapshot
707 .file()
708 .map(|file| file.path().to_path_buf())
709 .unwrap_or_default();
710 let request = server.request::<R>(request::GetCompletionsParams {
711 doc: request::GetCompletionsDocument {
712 uri,
713 tab_size: tab_size.into(),
714 indent_size: 1,
715 insert_spaces: !hard_tabs,
716 relative_path: relative_path.to_string_lossy().into(),
717 position: point_to_lsp(position),
718 version: version.try_into().unwrap(),
719 },
720 });
721 cx.background().spawn(async move {
722 let result = request.await?;
723 let completions = result
724 .completions
725 .into_iter()
726 .map(|completion| {
727 let start = snapshot
728 .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
729 let end =
730 snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
731 Completion {
732 range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
733 text: completion.text,
734 }
735 })
736 .collect();
737 anyhow::Ok(completions)
738 })
739 }
740
741 pub fn status(&self) -> Status {
742 match &self.server {
743 CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
744 CopilotServer::Disabled => Status::Disabled,
745 CopilotServer::Error(error) => Status::Error(error.clone()),
746 CopilotServer::Started { status, .. } => match status {
747 SignInStatus::Authorized { .. } => Status::Authorized,
748 SignInStatus::Unauthorized { .. } => Status::Unauthorized,
749 SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
750 prompt: prompt.clone(),
751 },
752 SignInStatus::SignedOut => Status::SignedOut,
753 },
754 }
755 }
756
757 fn update_sign_in_status(
758 &mut self,
759 lsp_status: request::SignInStatus,
760 cx: &mut ModelContext<Self>,
761 ) {
762 self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
763
764 if let CopilotServer::Started { status, .. } = &mut self.server {
765 match lsp_status {
766 request::SignInStatus::Ok { .. }
767 | request::SignInStatus::MaybeOk { .. }
768 | request::SignInStatus::AlreadySignedIn { .. } => {
769 *status = SignInStatus::Authorized;
770
771 for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
772 if let Some(buffer) = buffer.upgrade(cx) {
773 self.register_buffer(&buffer, cx);
774 }
775 }
776 }
777 request::SignInStatus::NotAuthorized { .. } => {
778 *status = SignInStatus::Unauthorized;
779
780 for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
781 self.unregister_buffer(buffer_id);
782 }
783 }
784 request::SignInStatus::NotSignedIn => {
785 *status = SignInStatus::SignedOut;
786
787 for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
788 self.unregister_buffer(buffer_id);
789 }
790 }
791 }
792
793 cx.notify();
794 }
795 }
796}
797
798fn id_for_language(language: Option<&Arc<Language>>) -> String {
799 let language_name = language.map(|language| language.name());
800 match language_name.as_deref() {
801 Some("Plain Text") => "plaintext".to_string(),
802 Some(language_name) => language_name.to_lowercase(),
803 None => "plaintext".to_string(),
804 }
805}
806
807fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
808 if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
809 lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
810 } else {
811 format!("buffer://{}", buffer.id()).parse().unwrap()
812 }
813}
814
815async fn clear_copilot_dir() {
816 remove_matching(&paths::COPILOT_DIR, |_| true).await
817}
818
819async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
820 const SERVER_PATH: &'static str = "dist/agent.js";
821
822 ///Check for the latest copilot language server and download it if we haven't already
823 async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
824 let release = latest_github_release("zed-industries/copilot", http.clone()).await?;
825
826 let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name));
827
828 fs::create_dir_all(version_dir).await?;
829 let server_path = version_dir.join(SERVER_PATH);
830
831 if fs::metadata(&server_path).await.is_err() {
832 // Copilot LSP looks for this dist dir specifcially, so lets add it in.
833 let dist_dir = version_dir.join("dist");
834 fs::create_dir_all(dist_dir.as_path()).await?;
835
836 let url = &release
837 .assets
838 .get(0)
839 .context("Github release for copilot contained no assets")?
840 .browser_download_url;
841
842 let mut response = http
843 .get(&url, Default::default(), true)
844 .await
845 .map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
846 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
847 let archive = Archive::new(decompressed_bytes);
848 archive.unpack(dist_dir).await?;
849
850 remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
851 }
852
853 Ok(server_path)
854 }
855
856 match fetch_latest(http).await {
857 ok @ Result::Ok(..) => ok,
858 e @ Err(..) => {
859 e.log_err();
860 // Fetch a cached binary, if it exists
861 (|| async move {
862 let mut last_version_dir = None;
863 let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?;
864 while let Some(entry) = entries.next().await {
865 let entry = entry?;
866 if entry.file_type().await?.is_dir() {
867 last_version_dir = Some(entry.path());
868 }
869 }
870 let last_version_dir =
871 last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
872 let server_path = last_version_dir.join(SERVER_PATH);
873 if server_path.exists() {
874 Ok(server_path)
875 } else {
876 Err(anyhow!(
877 "missing executable in directory {:?}",
878 last_version_dir
879 ))
880 }
881 })()
882 .await
883 }
884 }
885}