copilot.rs

   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}