1432 lines · 50.4 KB · Rust
RawDownload
pub mod copilot_chat;mod copilot_edit_prediction_delegate;pub mod copilot_responses;pub mod request;mod sign_in; use crate::request::NextEditSuggestions;use crate::sign_in::initiate_sign_out;use ::fs::Fs;use anyhow::{Context as _, Result, anyhow};use collections::{HashMap, HashSet};use command_palette_hooks::CommandPaletteFilter;use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};use gpui::{    App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Task,    WeakEntity, actions,};use http_client::HttpClient;use language::language_settings::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::DisableAiSettings;use request::StatusNotification;use semver::Version;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::Workspace; pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;pub use crate::sign_in::{    ConfigurationMode, ConfigurationView, CopilotCodeVerification, initiate_sign_in,    reinstall_and_sign_in,}; 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    ]); pub fn init(    new_server_id: LanguageServerId,    fs: Arc<dyn Fs>,    http: Arc<dyn HttpClient>,    node_runtime: NodeRuntime,    cx: &mut App,) {    let language_settings = all_language_settings(None, cx);    let configuration = copilot_chat::CopilotChatConfiguration {        enterprise_uri: language_settings            .edit_predictions            .copilot            .enterprise_uri            .clone(),    };    copilot_chat::init(fs.clone(), http.clone(), configuration, cx);     let copilot = cx.new(move |cx| Copilot::start(new_server_id, fs, node_runtime, cx));    Copilot::set_global(copilot.clone(), cx);    cx.observe(&copilot, |copilot, cx| {        copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx));    })    .detach();    cx.observe_global::<SettingsStore>(|cx| {        if let Some(copilot) = Copilot::global(cx) {            copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx));        }    })    .detach();     cx.observe_new(|workspace: &mut Workspace, _window, _cx| {        workspace.register_action(|_, _: &SignIn, window, cx| {            initiate_sign_in(window, cx);        });        workspace.register_action(|_, _: &Reinstall, window, cx| {            reinstall_and_sign_in(window, cx);        });        workspace.register_action(|_, _: &SignOut, window, cx| {            initiate_sign_out(window, cx);        });    })    .detach();} 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,    _subscription: gpui::Subscription,} pub enum Event {    CopilotLanguageServerStarted,    CopilotAuthSignedIn,    CopilotAuthSignedOut,} impl EventEmitter<Event> for Copilot {} struct GlobalCopilot(Entity<Copilot>); impl Global for GlobalCopilot {} /// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.struct CopilotEditPrediction {    buffer: Entity<Buffer>,    range: Range<Anchor>,    text: String,    command: Option<lsp::Command>,    snapshot: BufferSnapshot,} impl Copilot {    pub fn global(cx: &App) -> Option<Entity<Self>> {        cx.try_global::<GlobalCopilot>()            .map(|model| model.0.clone())    }     pub fn set_global(copilot: Entity<Self>, cx: &mut App) {        cx.set_global(GlobalCopilot(copilot));    }     fn start(        new_server_id: LanguageServerId,        fs: Arc<dyn Fs>,        node_runtime: NodeRuntime,        cx: &mut Context<Self>,    ) -> Self {        let mut this = Self {            server_id: new_server_id,            fs,            node_runtime,            server: CopilotServer::Disabled,            buffers: Default::default(),            _subscription: cx.on_app_quit(Self::shutdown_language_server),        };        this.start_copilot(true, false, cx);        cx.observe_global::<SettingsStore>(move |this, cx| {            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();            }        })        .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 !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)"            );            None        };         let mut env = HashMap::default();         if let Some(proxy_type) = http_or_https_proxy {            env.insert(proxy_type.to_string(), proxy_url);            if let Some(true) = no_verify {                env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());            };        }         if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {            env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);        }         if env.is_empty() { None } else { Some(env) }    }     #[cfg(any(test, feature = "test-support"))]    pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {        use fs::FakeFs;        use lsp::FakeLanguageServer;        use node_runtime::NodeRuntime;         let (server, fake_server) = FakeLanguageServer::new(            LanguageServerId(0),            LanguageServerBinary {                path: "path/to/copilot".into(),                arguments: vec![],                env: None,            },            "copilot".into(),            Default::default(),            &mut cx.to_async(),        );        let node_runtime = NodeRuntime::unavailable();        let this = cx.new(|cx| Self {            server_id: LanguageServerId(0),            fs: FakeFs::new(cx.background_executor().clone()),            node_runtime,            server: CopilotServer::Running(RunningCopilotServer {                lsp: Arc::new(server),                sign_in_status: SignInStatus::Authorized,                registered_buffers: Default::default(),            }),            _subscription: cx.on_app_quit(Self::shutdown_language_server),            buffers: Default::default(),        });        (this, fake_server)    }     async fn start_language_server(        new_server_id: LanguageServerId,        fs: Arc<dyn Fs>,        node_runtime: NodeRuntime,        env: Option<HashMap<String, String>>,        this: WeakEntity<Self>,        awaiting_sign_in_after_start: bool,        cx: &mut AsyncApp,    ) {        let start_language_server = async {            let server_path = get_copilot_lsp(fs, node_runtime.clone()).await?;            let node_path = node_runtime.binary_path().await?;            ensure_node_version_for_copilot(&node_path).await?;             let arguments: Vec<OsString> = vec![                "--experimental-sqlite".into(),                server_path.into(),                "--stdio".into(),            ];            let binary = LanguageServerBinary {

Lines 1–500 of 1432.