1901 lines · 68.7 KB · Rust
RawDownload
mod copilot_edit_prediction_delegate;pub mod request; use crate::request::{    DidFocus, DidFocusParams, FormattingOptions, InlineCompletionContext,    InlineCompletionTriggerKind, InlineCompletions, NextEditSuggestions,};use ::fs::Fs;use anyhow::{Context as _, Result, anyhow};use collections::{HashMap, HashSet};use command_palette_hooks::CommandPaletteFilter;use futures::future;use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared, select_biased};use gpui::{    App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Subscription,    Task, WeakEntity, actions,};use language::language_settings::{AllLanguageSettings, CopilotSettings};use language::{    Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,    language_settings::{EditPredictionProvider, all_language_settings},    point_from_lsp, point_to_lsp,};use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};use node_runtime::{NodeRuntime, VersionStrategy};use parking_lot::Mutex;use project::project_settings::ProjectSettings;use project::{DisableAiSettings, Project};use request::DidChangeStatus;use serde_json::json;use settings::{Settings, SettingsStore};use std::{    any::TypeId,    collections::hash_map::Entry,    env,    ffi::OsString,    mem,    ops::Range,    path::{Path, PathBuf},    sync::Arc,};use sum_tree::Dimensions;use util::{ResultExt, fs::remove_matching};use workspace::AppState; pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate; actions!(    copilot,    [        /// Requests a code completion suggestion from Copilot.        Suggest,        /// Cycles to the next Copilot suggestion.        NextSuggestion,        /// Cycles to the previous Copilot suggestion.        PreviousSuggestion,        /// Reinstalls the Copilot language server.        Reinstall,        /// Signs in to GitHub Copilot.        SignIn,        /// Signs out of GitHub Copilot.        SignOut    ]); enum CopilotServer {    Disabled,    Starting { task: Shared<Task<()>> },    Error(Arc<str>),    Running(RunningCopilotServer),} impl CopilotServer {    fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {        let server = self.as_running()?;        anyhow::ensure!(            matches!(server.sign_in_status, SignInStatus::Authorized),            "must sign in before using copilot"        );        Ok(server)    }     fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {        match self {            CopilotServer::Starting { .. } => anyhow::bail!("copilot is still starting"),            CopilotServer::Disabled => anyhow::bail!("copilot is disabled"),            CopilotServer::Error(error) => {                anyhow::bail!("copilot was not started because of an error: {error}")            }            CopilotServer::Running(server) => Ok(server),        }    }} struct RunningCopilotServer {    lsp: Arc<LanguageServer>,    sign_in_status: SignInStatus,    registered_buffers: HashMap<EntityId, RegisteredBuffer>,} #[derive(Clone, Debug)]enum SignInStatus {    Authorized,    Unauthorized,    SigningIn {        prompt: Option<request::PromptUserDeviceFlow>,        task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,    },    SignedOut {        awaiting_signing_in: bool,    },} #[derive(Debug, Clone)]pub enum Status {    Starting {        task: Shared<Task<()>>,    },    Error(Arc<str>),    Disabled,    SignedOut {        awaiting_signing_in: bool,    },    SigningIn {        prompt: Option<request::PromptUserDeviceFlow>,    },    Unauthorized,    Authorized,} impl Status {    pub fn is_authorized(&self) -> bool {        matches!(self, Status::Authorized)    }     pub fn is_configured(&self) -> bool {        matches!(            self,            Status::Starting { .. }                | Status::Error(_)                | Status::SigningIn { .. }                | Status::Authorized        )    }} struct RegisteredBuffer {    uri: lsp::Uri,    language_id: String,    snapshot: BufferSnapshot,    snapshot_version: i32,    _subscriptions: [gpui::Subscription; 2],    pending_buffer_change: Task<Option<()>>,} impl RegisteredBuffer {    fn report_changes(        &mut self,        buffer: &Entity<Buffer>,        cx: &mut Context<Copilot>,    ) -> oneshot::Receiver<(i32, BufferSnapshot)> {        let (done_tx, done_rx) = oneshot::channel();         if buffer.read(cx).version() == self.snapshot.version {            let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));        } else {            let buffer = buffer.downgrade();            let id = buffer.entity_id();            let prev_pending_change =                mem::replace(&mut self.pending_buffer_change, Task::ready(None));            self.pending_buffer_change = cx.spawn(async move |copilot, cx| {                prev_pending_change.await;                 let old_version = copilot                    .update(cx, |copilot, _| {                        let server = copilot.server.as_authenticated().log_err()?;                        let buffer = server.registered_buffers.get_mut(&id)?;                        Some(buffer.snapshot.version.clone())                    })                    .ok()??;                let new_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()).ok()?;                 let content_changes = cx                    .background_spawn({                        let new_snapshot = new_snapshot.clone();                        async move {                            new_snapshot                                .edits_since::<Dimensions<PointUtf16, usize>>(&old_version)                                .map(|edit| {                                    let edit_start = edit.new.start.0;                                    let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);                                    let new_text = new_snapshot                                        .text_for_range(edit.new.start.1..edit.new.end.1)                                        .collect();                                    lsp::TextDocumentContentChangeEvent {                                        range: Some(lsp::Range::new(                                            point_to_lsp(edit_start),                                            point_to_lsp(edit_end),                                        )),                                        range_length: None,                                        text: new_text,                                    }                                })                                .collect::<Vec<_>>()                        }                    })                    .await;                 copilot                    .update(cx, |copilot, _| {                        let server = copilot.server.as_authenticated().log_err()?;                        let buffer = server.registered_buffers.get_mut(&id)?;                        if !content_changes.is_empty() {                            buffer.snapshot_version += 1;                            buffer.snapshot = new_snapshot;                            server                                .lsp                                .notify::<lsp::notification::DidChangeTextDocument>(                                    lsp::DidChangeTextDocumentParams {                                        text_document: lsp::VersionedTextDocumentIdentifier::new(                                            buffer.uri.clone(),                                            buffer.snapshot_version,                                        ),                                        content_changes,                                    },                                )                                .ok();                        }                        let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));                        Some(())                    })                    .ok()?;                 Some(())            });        }         done_rx    }} #[derive(Debug)]pub struct Completion {    pub uuid: String,    pub range: Range<Anchor>,    pub text: String,} pub struct Copilot {    fs: Arc<dyn Fs>,    node_runtime: NodeRuntime,    server: CopilotServer,    buffers: HashSet<WeakEntity<Buffer>>,    server_id: LanguageServerId,    _subscriptions: Vec<Subscription>,} pub enum Event {    CopilotAuthSignedIn,    CopilotAuthSignedOut,} impl EventEmitter<Event> for Copilot {} #[derive(Clone)]pub struct GlobalCopilotAuth(pub Entity<Copilot>); impl GlobalCopilotAuth {    pub fn set_global(        server_id: LanguageServerId,        fs: Arc<dyn Fs>,        node_runtime: NodeRuntime,        cx: &mut App,    ) -> GlobalCopilotAuth {        let auth =            GlobalCopilotAuth(cx.new(|cx| Copilot::new(None, server_id, fs, node_runtime, cx)));        cx.set_global(auth.clone());        auth    }    pub fn try_global(cx: &mut App) -> Option<&GlobalCopilotAuth> {        cx.try_global()    }     pub fn try_get_or_init(app_state: Arc<AppState>, cx: &mut App) -> Option<GlobalCopilotAuth> {        let ai_enabled = !DisableAiSettings::get(None, cx).disable_ai;         if let Some(copilot) = cx.try_global::<Self>().cloned() {            if ai_enabled {                Some(copilot)            } else {                cx.remove_global::<Self>();                None            }        } else if ai_enabled {            Some(Self::set_global(                app_state.languages.next_language_server_id(),                app_state.fs.clone(),                app_state.node_runtime.clone(),                cx,            ))        } else {            None        }    }}impl Global for GlobalCopilotAuth {} #[derive(Clone, Copy, Debug, PartialEq, Eq)]pub(crate) enum CompletionSource {    NextEditSuggestion,    InlineCompletion,} /// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.#[derive(Clone)]pub(crate) struct CopilotEditPrediction {    pub(crate) buffer: Entity<Buffer>,    pub(crate) range: Range<Anchor>,    pub(crate) text: String,    pub(crate) command: Option<lsp::Command>,    pub(crate) snapshot: BufferSnapshot,    pub(crate) source: CompletionSource,} impl Copilot {    pub fn new(        project: Option<Entity<Project>>,        new_server_id: LanguageServerId,        fs: Arc<dyn Fs>,        node_runtime: NodeRuntime,        cx: &mut Context<Self>,    ) -> Self {        let send_focus_notification = project.map(|project| {            cx.subscribe(&project, |this, project, e: &project::Event, cx| {                if let project::Event::ActiveEntryChanged(new_entry) = e                    && let Ok(running) = this.server.as_authenticated()                {                    let uri = new_entry                        .and_then(|id| project.read(cx).path_for_entry(id, cx))                        .and_then(|entry| project.read(cx).absolute_path(&entry, cx))                        .and_then(|abs_path| lsp::Uri::from_file_path(abs_path).ok());                     _ = running.lsp.notify::<DidFocus>(DidFocusParams { uri });                }            })        });        let global_authentication_events =            cx.try_global::<GlobalCopilotAuth>().cloned().map(|auth| {                cx.subscribe(&auth.0, |_, _, _: &Event, cx| {                    let request_timeout = ProjectSettings::get_global(cx)                        .global_lsp_settings                        .get_request_timeout();                    cx.spawn(async move |this, cx| {                        let Some(server) = this                            .update(cx, |this, _| this.language_server().cloned())                            .ok()                            .flatten()                        else {                            return;                        };                        let status = server                            .request::<request::CheckStatus>(                                request::CheckStatusParams {                                    local_checks_only: false,                                },                                request_timeout,                            )                            .await                            .into_response()                            .ok();                        if let Some(status) = status {                            this.update(cx, |copilot, cx| {                                copilot.update_sign_in_status(status, cx);                            })                            .ok();                        }                    })                    .detach()                })            });        let _subscriptions = std::iter::once(cx.on_app_quit(Self::shutdown_language_server))            .chain(send_focus_notification)            .chain(global_authentication_events)            .collect();        let mut this = Self {            server_id: new_server_id,            fs,            node_runtime,            server: CopilotServer::Disabled,            buffers: Default::default(),            _subscriptions,        };        this.start_copilot(true, false, cx);        cx.observe_global::<SettingsStore>(move |this, cx| {            let ai_disabled = DisableAiSettings::get_global(cx).disable_ai;             if ai_disabled {                // Stop the server if AI is disabled                if !matches!(this.server, CopilotServer::Disabled) {                    let shutdown = match mem::replace(&mut this.server, CopilotServer::Disabled) {                        CopilotServer::Running(server) => {                            let shutdown_future = server.lsp.shutdown();                            Some(cx.background_spawn(async move {                                if let Some(fut) = shutdown_future {                                    fut.await;                                }                            }))                        }                        _ => None,                    };                    if let Some(task) = shutdown {                        task.detach();                    }                    cx.notify();                }            } else {                // Only start if AI is enabled                this.start_copilot(true, false, cx);                if let Ok(server) = this.server.as_running() {                    notify_did_change_config_to_server(&server.lsp, cx)                        .context("copilot setting change: did change configuration")                        .log_err();                }            }            this.update_action_visibilities(cx);        })        .detach();        cx.observe_self(|copilot, cx| {            copilot.update_action_visibilities(cx);        })        .detach();        this    }     fn shutdown_language_server(        &mut self,        _cx: &mut Context<Self>,    ) -> impl Future<Output = ()> + use<> {        let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) {            CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })),            _ => None,        };         async move {            if let Some(shutdown) = shutdown {                shutdown.await;            }        }    }     pub fn start_copilot(        &mut self,        check_edit_prediction_provider: bool,        awaiting_sign_in_after_start: bool,        cx: &mut Context<Self>,    ) {        if DisableAiSettings::get_global(cx).disable_ai {            return;        }        if !matches!(self.server, CopilotServer::Disabled) {            return;        }        let language_settings = all_language_settings(None, cx);        if check_edit_prediction_provider            && language_settings.edit_predictions.provider != EditPredictionProvider::Copilot        {            return;        }        let server_id = self.server_id;        let fs = self.fs.clone();        let node_runtime = self.node_runtime.clone();        let env = self.build_env(&language_settings.edit_predictions.copilot);        let start_task = cx            .spawn(async move |this, cx| {                Self::start_language_server(                    server_id,                    fs,                    node_runtime,                    env,                    this,                    awaiting_sign_in_after_start,                    cx,                )                .await            })            .shared();        self.server = CopilotServer::Starting { task: start_task };        cx.notify();    }     fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {        let proxy_url = copilot_settings.proxy.clone()?;        let no_verify = copilot_settings.proxy_no_verify;        let http_or_https_proxy = if proxy_url.starts_with("http:") {            Some("HTTP_PROXY")        } else if proxy_url.starts_with("https:") {            Some("HTTPS_PROXY")        } else {            log::error!(                "Unsupported protocol scheme for language server proxy (must be http or https)"

Lines 1–500 of 1901.