copilot.rs

  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}