WIP

Antonio Scandurra created

Change summary

Cargo.lock                            |   28 
Cargo.toml                            |    1 
crates/copilot2/Cargo.toml            |   49 +
crates/copilot2/src/copilot2.rs       | 1217 +++++++++++++++++++++++++++++
crates/copilot2/src/request.rs        |  225 +++++
crates/copilot2/src/sign_in.rs        |  376 ++++++++
crates/gpui2/src/app/model_context.rs |    2 
crates/project2/src/project2.rs       |   10 
crates/project2/src/terminals.rs      |    2 
crates/zed2/Cargo.toml                |    4 
crates/zed2/src/main.rs               |   16 
11 files changed, 1,914 insertions(+), 16 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1779,6 +1779,32 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "copilot2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-compression",
+ "async-tar",
+ "clock",
+ "collections",
+ "context_menu",
+ "fs",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "log",
+ "lsp2",
+ "node_runtime",
+ "rpc",
+ "serde",
+ "serde_derive",
+ "settings2",
+ "smol",
+ "theme",
+ "util",
+]
+
 [[package]]
 name = "copilot_button"
 version = "0.1.0"
@@ -10606,6 +10632,7 @@ dependencies = [
  "cli",
  "client2",
  "collections",
+ "copilot2",
  "ctor",
  "db2",
  "env_logger 0.9.3",
@@ -10620,6 +10647,7 @@ dependencies = [
  "indexmap 1.9.3",
  "install_cli",
  "isahc",
+ "language2",
  "language_tools",
  "lazy_static",
  "libc",

Cargo.toml 🔗

@@ -19,6 +19,7 @@ members = [
     "crates/component_test",
     "crates/context_menu",
     "crates/copilot",
+    "crates/copilot2",
     "crates/copilot_button",
     "crates/db",
     "crates/db2",

crates/copilot2/Cargo.toml 🔗

@@ -0,0 +1,49 @@
+[package]
+name = "copilot2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/copilot2.rs"
+doctest = false
+
+[features]
+test-support = [
+    "collections/test-support",
+    "gpui2/test-support",
+    "language2/test-support",
+    "lsp2/test-support",
+    "settings2/test-support",
+    "util/test-support",
+]
+
+[dependencies]
+collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
+gpui2 = { path = "../gpui2" }
+language2 = { path = "../language2" }
+settings2 = { path = "../settings2" }
+theme = { path = "../theme" }
+lsp2 = { path = "../lsp2" }
+node_runtime = { path = "../node_runtime"}
+util = { path = "../util" }
+async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
+async-tar = "0.4.2"
+anyhow.workspace = true
+log.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smol.workspace = true
+futures.workspace = true
+
+[dev-dependencies]
+clock = { path = "../clock" }
+collections = { path = "../collections", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
+gpui2 = { path = "../gpui2", features = ["test-support"] }
+language2 = { path = "../language2", features = ["test-support"] }
+lsp2 = { path = "../lsp2", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+settings2 = { path = "../settings2", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }

crates/copilot2/src/copilot2.rs 🔗

@@ -0,0 +1,1217 @@
+pub mod request;
+mod sign_in;
+
+use anyhow::{anyhow, Context as _, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use collections::{HashMap, HashSet};
+use futures::{channel::oneshot, future::Shared, Future, FutureExt};
+use gpui2::{
+    AppContext, AsyncAppContext, Context, EventEmitter, Handle, ModelContext, Task, WeakHandle,
+};
+use language2::{
+    language_settings::{all_language_settings, language_settings},
+    point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language,
+    LanguageServerName, PointUtf16, ToPointUtf16,
+};
+use lsp2::{LanguageServer, LanguageServerBinary, LanguageServerId};
+use node_runtime::NodeRuntime;
+use request::StatusNotification;
+use settings2::SettingsStore;
+use smol::{fs, io::BufReader, stream::StreamExt};
+use std::{
+    ffi::OsString,
+    mem,
+    ops::Range,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{
+    fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
+};
+
+// todo!()
+// const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
+// actions!(copilot_auth, [SignIn, SignOut]);
+
+// todo!()
+// const COPILOT_NAMESPACE: &'static str = "copilot";
+// actions!(
+//     copilot,
+//     [Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
+// );
+
+pub fn init(
+    new_server_id: LanguageServerId,
+    http: Arc<dyn HttpClient>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    cx: &mut AppContext,
+) {
+    let copilot = cx.entity({
+        let node_runtime = node_runtime.clone();
+        move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
+    });
+    cx.set_global(copilot.clone());
+
+    // TODO
+    // cx.observe(&copilot, |handle, cx| {
+    //     let status = handle.read(cx).status();
+    //     cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
+    //         match status {
+    //             Status::Disabled => {
+    //                 filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
+    //                 filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
+    //             }
+    //             Status::Authorized => {
+    //                 filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
+    //                 filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
+    //             }
+    //             _ => {
+    //                 filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
+    //                 filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
+    //             }
+    //         }
+    //     });
+    // })
+    // .detach();
+
+    // sign_in::init(cx);
+    // cx.add_global_action(|_: &SignIn, cx| {
+    //     if let Some(copilot) = Copilot::global(cx) {
+    //         copilot
+    //             .update(cx, |copilot, cx| copilot.sign_in(cx))
+    //             .detach_and_log_err(cx);
+    //     }
+    // });
+    // cx.add_global_action(|_: &SignOut, cx| {
+    //     if let Some(copilot) = Copilot::global(cx) {
+    //         copilot
+    //             .update(cx, |copilot, cx| copilot.sign_out(cx))
+    //             .detach_and_log_err(cx);
+    //     }
+    // });
+
+    // cx.add_global_action(|_: &Reinstall, cx| {
+    //     if let Some(copilot) = Copilot::global(cx) {
+    //         copilot
+    //             .update(cx, |copilot, cx| copilot.reinstall(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()?;
+        if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) {
+            Ok(server)
+        } else {
+            Err(anyhow!("must sign in before using copilot"))
+        }
+    }
+
+    fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
+        match self {
+            CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")),
+            CopilotServer::Disabled => Err(anyhow!("copilot is disabled")),
+            CopilotServer::Error(error) => Err(anyhow!(
+                "copilot was not started because of an error: {}",
+                error
+            )),
+            CopilotServer::Running(server) => Ok(server),
+        }
+    }
+}
+
+struct RunningCopilotServer {
+    name: LanguageServerName,
+    lsp: Arc<LanguageServer>,
+    sign_in_status: SignInStatus,
+    registered_buffers: HashMap<usize, RegisteredBuffer>,
+}
+
+#[derive(Clone, Debug)]
+enum SignInStatus {
+    Authorized,
+    Unauthorized,
+    SigningIn {
+        prompt: Option<request::PromptUserDeviceFlow>,
+        task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
+    },
+    SignedOut,
+}
+
+#[derive(Debug, Clone)]
+pub enum Status {
+    Starting {
+        task: Shared<Task<()>>,
+    },
+    Error(Arc<str>),
+    Disabled,
+    SignedOut,
+    SigningIn {
+        prompt: Option<request::PromptUserDeviceFlow>,
+    },
+    Unauthorized,
+    Authorized,
+}
+
+impl Status {
+    pub fn is_authorized(&self) -> bool {
+        matches!(self, Status::Authorized)
+    }
+}
+
+struct RegisteredBuffer {
+    uri: lsp2::Url,
+    language_id: String,
+    snapshot: BufferSnapshot,
+    snapshot_version: i32,
+    _subscriptions: [gpui2::Subscription; 2],
+    pending_buffer_change: Task<Option<()>>,
+}
+
+impl RegisteredBuffer {
+    fn report_changes(
+        &mut self,
+        buffer: &Handle<Buffer>,
+        cx: &mut ModelContext<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.id();
+            let prev_pending_change =
+                mem::replace(&mut self.pending_buffer_change, Task::ready(None));
+            self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
+                prev_pending_change.await;
+
+                let old_version = copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
+                    let server = copilot.server.as_authenticated().log_err()?;
+                    let buffer = server.registered_buffers.get_mut(&id)?;
+                    Some(buffer.snapshot.version.clone())
+                })?;
+                let new_snapshot = buffer
+                    .upgrade()?
+                    .read_with(&cx, |buffer, _| buffer.snapshot());
+
+                let content_changes = cx
+                    .background()
+                    .spawn({
+                        let new_snapshot = new_snapshot.clone();
+                        async move {
+                            new_snapshot
+                                .edits_since::<(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();
+                                    lsp2::TextDocumentContentChangeEvent {
+                                        range: Some(lsp2::Range::new(
+                                            point_to_lsp(edit_start),
+                                            point_to_lsp(edit_end),
+                                        )),
+                                        range_length: None,
+                                        text: new_text,
+                                    }
+                                })
+                                .collect::<Vec<_>>()
+                        }
+                    })
+                    .await;
+
+                copilot.upgrade(&cx)?.update(&mut 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::<lsp2::notification::DidChangeTextDocument>(
+                                lsp2::DidChangeTextDocumentParams {
+                                    text_document: lsp2::VersionedTextDocumentIdentifier::new(
+                                        buffer.uri.clone(),
+                                        buffer.snapshot_version,
+                                    ),
+                                    content_changes,
+                                },
+                            )
+                            .log_err();
+                    }
+                    let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
+                    Some(())
+                })?;
+
+                Some(())
+            });
+        }
+
+        done_rx
+    }
+}
+
+#[derive(Debug)]
+pub struct Completion {
+    pub uuid: String,
+    pub range: Range<Anchor>,
+    pub text: String,
+}
+
+pub struct Copilot {
+    http: Arc<dyn HttpClient>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    server: CopilotServer,
+    buffers: HashSet<Handle<Buffer>>,
+    server_id: LanguageServerId,
+    _subscription: gpui2::Subscription,
+}
+
+pub enum Event {
+    CopilotLanguageServerStarted,
+}
+
+impl EventEmitter for Copilot {
+    type Event = Event;
+}
+
+impl Copilot {
+    pub fn global(cx: &AppContext) -> Option<Handle<Self>> {
+        if cx.has_global::<Handle<Self>>() {
+            Some(cx.global::<Handle<Self>>().clone())
+        } else {
+            None
+        }
+    }
+
+    fn start(
+        new_server_id: LanguageServerId,
+        http: Arc<dyn HttpClient>,
+        node_runtime: Arc<dyn NodeRuntime>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        let mut this = Self {
+            server_id: new_server_id,
+            http,
+            node_runtime,
+            server: CopilotServer::Disabled,
+            buffers: Default::default(),
+            _subscription: cx.on_app_quit(Self::shutdown_language_server),
+        };
+        this.enable_or_disable_copilot(cx);
+        cx.observe_global::<SettingsStore, _>(move |this, cx| this.enable_or_disable_copilot(cx))
+            .detach();
+        this
+    }
+
+    fn shutdown_language_server(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> impl Future<Output = ()> {
+        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;
+            }
+        }
+    }
+
+    fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Self>) {
+        let server_id = self.server_id;
+        let http = self.http.clone();
+        let node_runtime = self.node_runtime.clone();
+        if all_language_settings(None, cx).copilot_enabled(None, None) {
+            if matches!(self.server, CopilotServer::Disabled) {
+                let start_task = cx
+                    .spawn({
+                        move |this, cx| {
+                            Self::start_language_server(server_id, http, node_runtime, this, cx)
+                        }
+                    })
+                    .shared();
+                self.server = CopilotServer::Starting { task: start_task };
+                cx.notify();
+            }
+        } else {
+            self.server = CopilotServer::Disabled;
+            cx.notify();
+        }
+    }
+
+    // #[cfg(any(test, feature = "test-support"))]
+    // pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
+    //     use node_runtime::FakeNodeRuntime;
+
+    //     let (server, fake_server) =
+    //         LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
+    //     let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
+    //     let node_runtime = FakeNodeRuntime::new();
+    //     let this = cx.add_model(|_| Self {
+    //         server_id: LanguageServerId(0),
+    //         http: http.clone(),
+    //         node_runtime,
+    //         server: CopilotServer::Running(RunningCopilotServer {
+    //             name: LanguageServerName(Arc::from("copilot")),
+    //             lsp: Arc::new(server),
+    //             sign_in_status: SignInStatus::Authorized,
+    //             registered_buffers: Default::default(),
+    //         }),
+    //         buffers: Default::default(),
+    //     });
+    //     (this, fake_server)
+    // }
+
+    fn start_language_server(
+        new_server_id: LanguageServerId,
+        http: Arc<dyn HttpClient>,
+        node_runtime: Arc<dyn NodeRuntime>,
+        this: Handle<Self>,
+        mut cx: AsyncAppContext,
+    ) -> impl Future<Output = ()> {
+        async move {
+            let start_language_server = async {
+                let server_path = get_copilot_lsp(http).await?;
+                let node_path = node_runtime.binary_path().await?;
+                let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
+                let binary = LanguageServerBinary {
+                    path: node_path,
+                    arguments,
+                };
+                let server =
+                    LanguageServer::new(new_server_id, binary, Path::new("/"), None, cx.clone())?;
+
+                server
+                    .on_notification::<StatusNotification, _>(
+                        |_, _| { /* Silence the notification */ },
+                    )
+                    .detach();
+
+                let server = server.initialize(Default::default()).await?;
+
+                let status = server
+                    .request::<request::CheckStatus>(request::CheckStatusParams {
+                        local_checks_only: false,
+                    })
+                    .await?;
+
+                server
+                    .request::<request::SetEditorInfo>(request::SetEditorInfoParams {
+                        editor_info: request::EditorInfo {
+                            name: "zed".into(),
+                            version: env!("CARGO_PKG_VERSION").into(),
+                        },
+                        editor_plugin_info: request::EditorPluginInfo {
+                            name: "zed-copilot".into(),
+                            version: "0.0.1".into(),
+                        },
+                    })
+                    .await?;
+
+                anyhow::Ok((server, status))
+            };
+
+            let server = start_language_server.await;
+            this.update(&mut cx, |this, cx| {
+                cx.notify();
+                match server {
+                    Ok((server, status)) => {
+                        this.server = CopilotServer::Running(RunningCopilotServer {
+                            name: LanguageServerName(Arc::from("copilot")),
+                            lsp: server,
+                            sign_in_status: SignInStatus::SignedOut,
+                            registered_buffers: Default::default(),
+                        });
+                        cx.emit(Event::CopilotLanguageServerStarted);
+                        this.update_sign_in_status(status, cx);
+                    }
+                    Err(error) => {
+                        this.server = CopilotServer::Error(error.to_string().into());
+                        cx.notify()
+                    }
+                }
+            })
+        }
+    }
+
+    pub fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        if let CopilotServer::Running(server) = &mut self.server {
+            let task = match &server.sign_in_status {
+                SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(),
+                SignInStatus::SigningIn { task, .. } => {
+                    cx.notify();
+                    task.clone()
+                }
+                SignInStatus::SignedOut | SignInStatus::Unauthorized { .. } => {
+                    let lsp = server.lsp.clone();
+                    let task = cx
+                        .spawn(|this, mut cx| async move {
+                            let sign_in = async {
+                                let sign_in = lsp
+                                    .request::<request::SignInInitiate>(
+                                        request::SignInInitiateParams {},
+                                    )
+                                    .await?;
+                                match sign_in {
+                                    request::SignInInitiateResult::AlreadySignedIn { user } => {
+                                        Ok(request::SignInStatus::Ok { user })
+                                    }
+                                    request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
+                                        this.update(&mut cx, |this, cx| {
+                                            if let CopilotServer::Running(RunningCopilotServer {
+                                                sign_in_status: status,
+                                                ..
+                                            }) = &mut this.server
+                                            {
+                                                if let SignInStatus::SigningIn {
+                                                    prompt: prompt_flow,
+                                                    ..
+                                                } = status
+                                                {
+                                                    *prompt_flow = Some(flow.clone());
+                                                    cx.notify();
+                                                }
+                                            }
+                                        });
+                                        let response = lsp
+                                            .request::<request::SignInConfirm>(
+                                                request::SignInConfirmParams {
+                                                    user_code: flow.user_code,
+                                                },
+                                            )
+                                            .await?;
+                                        Ok(response)
+                                    }
+                                }
+                            };
+
+                            let sign_in = sign_in.await;
+                            this.update(&mut cx, |this, cx| match sign_in {
+                                Ok(status) => {
+                                    this.update_sign_in_status(status, cx);
+                                    Ok(())
+                                }
+                                Err(error) => {
+                                    this.update_sign_in_status(
+                                        request::SignInStatus::NotSignedIn,
+                                        cx,
+                                    );
+                                    Err(Arc::new(error))
+                                }
+                            })
+                        })
+                        .shared();
+                    server.sign_in_status = SignInStatus::SigningIn {
+                        prompt: None,
+                        task: task.clone(),
+                    };
+                    cx.notify();
+                    task
+                }
+            };
+
+            cx.foreground()
+                .spawn(task.map_err(|err| anyhow!("{:?}", err)))
+        } else {
+            // If we're downloading, wait until download is finished
+            // If we're in a stuck state, display to the user
+            Task::ready(Err(anyhow!("copilot hasn't started yet")))
+        }
+    }
+
+    fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
+        if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
+            let server = server.clone();
+            cx.background().spawn(async move {
+                server
+                    .request::<request::SignOut>(request::SignOutParams {})
+                    .await?;
+                anyhow::Ok(())
+            })
+        } else {
+            Task::ready(Err(anyhow!("copilot hasn't started yet")))
+        }
+    }
+
+    pub fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
+        let start_task = cx
+            .spawn({
+                let http = self.http.clone();
+                let node_runtime = self.node_runtime.clone();
+                let server_id = self.server_id;
+                move |this, cx| async move {
+                    clear_copilot_dir().await;
+                    Self::start_language_server(server_id, http, node_runtime, this, cx).await
+                }
+            })
+            .shared();
+
+        self.server = CopilotServer::Starting {
+            task: start_task.clone(),
+        };
+
+        cx.notify();
+
+        cx.foreground().spawn(start_task)
+    }
+
+    pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc<LanguageServer>)> {
+        if let CopilotServer::Running(server) = &self.server {
+            Some((&server.name, &server.lsp))
+        } else {
+            None
+        }
+    }
+
+    pub fn register_buffer(&mut self, buffer: &Handle<Buffer>, cx: &mut ModelContext<Self>) {
+        let weak_buffer = buffer.downgrade();
+        self.buffers.insert(weak_buffer.clone());
+
+        if let CopilotServer::Running(RunningCopilotServer {
+            lsp: server,
+            sign_in_status: status,
+            registered_buffers,
+            ..
+        }) = &mut self.server
+        {
+            if !matches!(status, SignInStatus::Authorized { .. }) {
+                return;
+            }
+
+            registered_buffers.entry(buffer.id()).or_insert_with(|| {
+                let uri: lsp2::Url = uri_for_buffer(buffer, cx);
+                let language_id = id_for_language(buffer.read(cx).language());
+                let snapshot = buffer.read(cx).snapshot();
+                server
+                    .notify::<lsp2::notification::DidOpenTextDocument>(
+                        lsp2::DidOpenTextDocumentParams {
+                            text_document: lsp2::TextDocumentItem {
+                                uri: uri.clone(),
+                                language_id: language_id.clone(),
+                                version: 0,
+                                text: snapshot.text(),
+                            },
+                        },
+                    )
+                    .log_err();
+
+                RegisteredBuffer {
+                    uri,
+                    language_id,
+                    snapshot,
+                    snapshot_version: 0,
+                    pending_buffer_change: Task::ready(Some(())),
+                    _subscriptions: [
+                        cx.subscribe(buffer, |this, buffer, event, cx| {
+                            this.handle_buffer_event(buffer, event, cx).log_err();
+                        }),
+                        cx.observe_release(buffer, move |this, _buffer, _cx| {
+                            this.buffers.remove(&weak_buffer);
+                            this.unregister_buffer(&weak_buffer);
+                        }),
+                    ],
+                }
+            });
+        }
+    }
+
+    fn handle_buffer_event(
+        &mut self,
+        buffer: Handle<Buffer>,
+        event: &language2::Event,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        if let Ok(server) = self.server.as_running() {
+            if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
+                match event {
+                    language2::Event::Edited => {
+                        let _ = registered_buffer.report_changes(&buffer, cx);
+                    }
+                    language2::Event::Saved => {
+                        server
+                            .lsp
+                            .notify::<lsp2::notification::DidSaveTextDocument>(
+                                lsp2::DidSaveTextDocumentParams {
+                                    text_document: lsp2::TextDocumentIdentifier::new(
+                                        registered_buffer.uri.clone(),
+                                    ),
+                                    text: None,
+                                },
+                            )?;
+                    }
+                    language2::Event::FileHandleChanged | language2::Event::LanguageChanged => {
+                        let new_language_id = id_for_language(buffer.read(cx).language());
+                        let new_uri = uri_for_buffer(&buffer, cx);
+                        if new_uri != registered_buffer.uri
+                            || new_language_id != registered_buffer.language_id
+                        {
+                            let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
+                            registered_buffer.language_id = new_language_id;
+                            server
+                                .lsp
+                                .notify::<lsp2::notification::DidCloseTextDocument>(
+                                    lsp2::DidCloseTextDocumentParams {
+                                        text_document: lsp2::TextDocumentIdentifier::new(old_uri),
+                                    },
+                                )?;
+                            server
+                                .lsp
+                                .notify::<lsp2::notification::DidOpenTextDocument>(
+                                    lsp2::DidOpenTextDocumentParams {
+                                        text_document: lsp2::TextDocumentItem::new(
+                                            registered_buffer.uri.clone(),
+                                            registered_buffer.language_id.clone(),
+                                            registered_buffer.snapshot_version,
+                                            registered_buffer.snapshot.text(),
+                                        ),
+                                    },
+                                )?;
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn unregister_buffer(&mut self, buffer: &WeakHandle<Buffer>) {
+        if let Ok(server) = self.server.as_running() {
+            if let Some(buffer) = server.registered_buffers.remove(&buffer.id()) {
+                server
+                    .lsp
+                    .notify::<lsp2::notification::DidCloseTextDocument>(
+                        lsp2::DidCloseTextDocumentParams {
+                            text_document: lsp2::TextDocumentIdentifier::new(buffer.uri),
+                        },
+                    )
+                    .log_err();
+            }
+        }
+    }
+
+    pub fn completions<T>(
+        &mut self,
+        buffer: &Handle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    where
+        T: ToPointUtf16,
+    {
+        self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
+    }
+
+    pub fn completions_cycling<T>(
+        &mut self,
+        buffer: &Handle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    where
+        T: ToPointUtf16,
+    {
+        self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
+    }
+
+    pub fn accept_completion(
+        &mut self,
+        completion: &Completion,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let request =
+            server
+                .lsp
+                .request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
+                    uuid: completion.uuid.clone(),
+                });
+        cx.background().spawn(async move {
+            request.await?;
+            Ok(())
+        })
+    }
+
+    pub fn discard_completions(
+        &mut self,
+        completions: &[Completion],
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let request =
+            server
+                .lsp
+                .request::<request::NotifyRejected>(request::NotifyRejectedParams {
+                    uuids: completions
+                        .iter()
+                        .map(|completion| completion.uuid.clone())
+                        .collect(),
+                });
+        cx.background().spawn(async move {
+            request.await?;
+            Ok(())
+        })
+    }
+
+    fn request_completions<R, T>(
+        &mut self,
+        buffer: &Handle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    where
+        R: 'static
+            + lsp2::request::Request<
+                Params = request::GetCompletionsParams,
+                Result = request::GetCompletionsResult,
+            >,
+        T: ToPointUtf16,
+    {
+        self.register_buffer(buffer, cx);
+
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let lsp = server.lsp.clone();
+        let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
+        let snapshot = registered_buffer.report_changes(buffer, cx);
+        let buffer = buffer.read(cx);
+        let uri = registered_buffer.uri.clone();
+        let position = position.to_point_utf16(buffer);
+        let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
+        let tab_size = settings.tab_size;
+        let hard_tabs = settings.hard_tabs;
+        let relative_path = buffer
+            .file()
+            .map(|file| file.path().to_path_buf())
+            .unwrap_or_default();
+
+        cx.foreground().spawn(async move {
+            let (version, snapshot) = snapshot.await?;
+            let result = lsp
+                .request::<R>(request::GetCompletionsParams {
+                    doc: request::GetCompletionsDocument {
+                        uri,
+                        tab_size: tab_size.into(),
+                        indent_size: 1,
+                        insert_spaces: !hard_tabs,
+                        relative_path: relative_path.to_string_lossy().into(),
+                        position: point_to_lsp(position),
+                        version: version.try_into().unwrap(),
+                    },
+                })
+                .await?;
+            let completions = result
+                .completions
+                .into_iter()
+                .map(|completion| {
+                    let start = snapshot
+                        .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
+                    let end =
+                        snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
+                    Completion {
+                        uuid: completion.uuid,
+                        range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                        text: completion.text,
+                    }
+                })
+                .collect();
+            anyhow::Ok(completions)
+        })
+    }
+
+    pub fn status(&self) -> Status {
+        match &self.server {
+            CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
+            CopilotServer::Disabled => Status::Disabled,
+            CopilotServer::Error(error) => Status::Error(error.clone()),
+            CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
+                match sign_in_status {
+                    SignInStatus::Authorized { .. } => Status::Authorized,
+                    SignInStatus::Unauthorized { .. } => Status::Unauthorized,
+                    SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
+                        prompt: prompt.clone(),
+                    },
+                    SignInStatus::SignedOut => Status::SignedOut,
+                }
+            }
+        }
+    }
+
+    fn update_sign_in_status(
+        &mut self,
+        lsp_status: request::SignInStatus,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.buffers.retain(|buffer| buffer.is_upgradable(cx));
+
+        if let Ok(server) = self.server.as_running() {
+            match lsp_status {
+                request::SignInStatus::Ok { .. }
+                | request::SignInStatus::MaybeOk { .. }
+                | request::SignInStatus::AlreadySignedIn { .. } => {
+                    server.sign_in_status = SignInStatus::Authorized;
+                    for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
+                        if let Some(buffer) = buffer.upgrade(cx) {
+                            self.register_buffer(&buffer, cx);
+                        }
+                    }
+                }
+                request::SignInStatus::NotAuthorized { .. } => {
+                    server.sign_in_status = SignInStatus::Unauthorized;
+                    for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
+                        self.unregister_buffer(&buffer);
+                    }
+                }
+                request::SignInStatus::NotSignedIn => {
+                    server.sign_in_status = SignInStatus::SignedOut;
+                    for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
+                        self.unregister_buffer(&buffer);
+                    }
+                }
+            }
+
+            cx.notify();
+        }
+    }
+}
+
+fn id_for_language(language: Option<&Arc<Language>>) -> String {
+    let language_name = language.map(|language| language.name());
+    match language_name.as_deref() {
+        Some("Plain Text") => "plaintext".to_string(),
+        Some(language_name) => language_name.to_lowercase(),
+        None => "plaintext".to_string(),
+    }
+}
+
+fn uri_for_buffer(buffer: &Handle<Buffer>, cx: &AppContext) -> lsp2::Url {
+    if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
+        lsp2::Url::from_file_path(file.abs_path(cx)).unwrap()
+    } else {
+        format!("buffer://{}", buffer.id()).parse().unwrap()
+    }
+}
+
+async fn clear_copilot_dir() {
+    remove_matching(&paths::COPILOT_DIR, |_| true).await
+}
+
+async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
+    const SERVER_PATH: &'static str = "dist/agent.js";
+
+    ///Check for the latest copilot language server and download it if we haven't already
+    async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
+        let release = latest_github_release("zed-industries/copilot", false, http.clone()).await?;
+
+        let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name));
+
+        fs::create_dir_all(version_dir).await?;
+        let server_path = version_dir.join(SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            // Copilot LSP looks for this dist dir specifcially, so lets add it in.
+            let dist_dir = version_dir.join("dist");
+            fs::create_dir_all(dist_dir.as_path()).await?;
+
+            let url = &release
+                .assets
+                .get(0)
+                .context("Github release for copilot contained no assets")?
+                .browser_download_url;
+
+            let mut response = http
+                .get(&url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(dist_dir).await?;
+
+            remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
+        }
+
+        Ok(server_path)
+    }
+
+    match fetch_latest(http).await {
+        ok @ Result::Ok(..) => ok,
+        e @ Err(..) => {
+            e.log_err();
+            // Fetch a cached binary, if it exists
+            (|| async move {
+                let mut last_version_dir = None;
+                let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?;
+                while let Some(entry) = entries.next().await {
+                    let entry = entry?;
+                    if entry.file_type().await?.is_dir() {
+                        last_version_dir = Some(entry.path());
+                    }
+                }
+                let last_version_dir =
+                    last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+                let server_path = last_version_dir.join(SERVER_PATH);
+                if server_path.exists() {
+                    Ok(server_path)
+                } else {
+                    Err(anyhow!(
+                        "missing executable in directory {:?}",
+                        last_version_dir
+                    ))
+                }
+            })()
+            .await
+        }
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use gpui::{executor::Deterministic, TestAppContext};
+
+//     #[gpui::test(iterations = 10)]
+//     async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+//         deterministic.forbid_parking();
+//         let (copilot, mut lsp) = Copilot::fake(cx);
+
+//         let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
+//         let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
+//         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_1_uri.clone(),
+//                     "plaintext".into(),
+//                     0,
+//                     "Hello".into()
+//                 ),
+//             }
+//         );
+
+//         let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
+//         let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
+//         copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_2_uri.clone(),
+//                     "plaintext".into(),
+//                     0,
+//                     "Goodbye".into()
+//                 ),
+//             }
+//         );
+
+//         buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
+//                 .await,
+//             lsp::DidChangeTextDocumentParams {
+//                 text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
+//                 content_changes: vec![lsp::TextDocumentContentChangeEvent {
+//                     range: Some(lsp::Range::new(
+//                         lsp::Position::new(0, 5),
+//                         lsp::Position::new(0, 5)
+//                     )),
+//                     range_length: None,
+//                     text: " world".into(),
+//                 }],
+//             }
+//         );
+
+//         // Ensure updates to the file are reflected in the LSP.
+//         buffer_1
+//             .update(cx, |buffer, cx| {
+//                 buffer.file_updated(
+//                     Arc::new(File {
+//                         abs_path: "/root/child/buffer-1".into(),
+//                         path: Path::new("child/buffer-1").into(),
+//                     }),
+//                     cx,
+//                 )
+//             })
+//             .await;
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+//                 .await,
+//             lsp::DidCloseTextDocumentParams {
+//                 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
+//             }
+//         );
+//         let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_1_uri.clone(),
+//                     "plaintext".into(),
+//                     1,
+//                     "Hello world".into()
+//                 ),
+//             }
+//         );
+
+//         // Ensure all previously-registered buffers are closed when signing out.
+//         lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
+//             Ok(request::SignOutResult {})
+//         });
+//         copilot
+//             .update(cx, |copilot, cx| copilot.sign_out(cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+//                 .await,
+//             lsp::DidCloseTextDocumentParams {
+//                 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
+//             }
+//         );
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+//                 .await,
+//             lsp::DidCloseTextDocumentParams {
+//                 text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
+//             }
+//         );
+
+//         // Ensure all previously-registered buffers are re-opened when signing in.
+//         lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
+//             Ok(request::SignInInitiateResult::AlreadySignedIn {
+//                 user: "user-1".into(),
+//             })
+//         });
+//         copilot
+//             .update(cx, |copilot, cx| copilot.sign_in(cx))
+//             .await
+//             .unwrap();
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_2_uri.clone(),
+//                     "plaintext".into(),
+//                     0,
+//                     "Goodbye".into()
+//                 ),
+//             }
+//         );
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+//                 .await,
+//             lsp::DidOpenTextDocumentParams {
+//                 text_document: lsp::TextDocumentItem::new(
+//                     buffer_1_uri.clone(),
+//                     "plaintext".into(),
+//                     0,
+//                     "Hello world".into()
+//                 ),
+//             }
+//         );
+
+//         // Dropping a buffer causes it to be closed on the LSP side as well.
+//         cx.update(|_| drop(buffer_2));
+//         assert_eq!(
+//             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+//                 .await,
+//             lsp::DidCloseTextDocumentParams {
+//                 text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
+//             }
+//         );
+//     }
+
+//     struct File {
+//         abs_path: PathBuf,
+//         path: Arc<Path>,
+//     }
+
+//     impl language2::File for File {
+//         fn as_local(&self) -> Option<&dyn language2::LocalFile> {
+//             Some(self)
+//         }
+
+//         fn mtime(&self) -> std::time::SystemTime {
+//             unimplemented!()
+//         }
+
+//         fn path(&self) -> &Arc<Path> {
+//             &self.path
+//         }
+
+//         fn full_path(&self, _: &AppContext) -> PathBuf {
+//             unimplemented!()
+//         }
+
+//         fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+//             unimplemented!()
+//         }
+
+//         fn is_deleted(&self) -> bool {
+//             unimplemented!()
+//         }
+
+//         fn as_any(&self) -> &dyn std::any::Any {
+//             unimplemented!()
+//         }
+
+//         fn to_proto(&self) -> rpc::proto::File {
+//             unimplemented!()
+//         }
+
+//         fn worktree_id(&self) -> usize {
+//             0
+//         }
+//     }
+
+//     impl language::LocalFile for File {
+//         fn abs_path(&self, _: &AppContext) -> PathBuf {
+//             self.abs_path.clone()
+//         }
+
+//         fn load(&self, _: &AppContext) -> Task<Result<String>> {
+//             unimplemented!()
+//         }
+
+//         fn buffer_reloaded(
+//             &self,
+//             _: u64,
+//             _: &clock::Global,
+//             _: language::RopeFingerprint,
+//             _: language::LineEnding,
+//             _: std::time::SystemTime,
+//             _: &mut AppContext,
+//         ) {
+//             unimplemented!()
+//         }
+//     }
+// }

crates/copilot2/src/request.rs 🔗

@@ -0,0 +1,225 @@
+use serde::{Deserialize, Serialize};
+
+pub enum CheckStatus {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CheckStatusParams {
+    pub local_checks_only: bool,
+}
+
+impl lsp2::request::Request for CheckStatus {
+    type Params = CheckStatusParams;
+    type Result = SignInStatus;
+    const METHOD: &'static str = "checkStatus";
+}
+
+pub enum SignInInitiate {}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct SignInInitiateParams {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(tag = "status")]
+pub enum SignInInitiateResult {
+    AlreadySignedIn { user: String },
+    PromptUserDeviceFlow(PromptUserDeviceFlow),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PromptUserDeviceFlow {
+    pub user_code: String,
+    pub verification_uri: String,
+}
+
+impl lsp2::request::Request for SignInInitiate {
+    type Params = SignInInitiateParams;
+    type Result = SignInInitiateResult;
+    const METHOD: &'static str = "signInInitiate";
+}
+
+pub enum SignInConfirm {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SignInConfirmParams {
+    pub user_code: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(tag = "status")]
+pub enum SignInStatus {
+    #[serde(rename = "OK")]
+    Ok {
+        user: String,
+    },
+    MaybeOk {
+        user: String,
+    },
+    AlreadySignedIn {
+        user: String,
+    },
+    NotAuthorized {
+        user: String,
+    },
+    NotSignedIn,
+}
+
+impl lsp2::request::Request for SignInConfirm {
+    type Params = SignInConfirmParams;
+    type Result = SignInStatus;
+    const METHOD: &'static str = "signInConfirm";
+}
+
+pub enum SignOut {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SignOutParams {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SignOutResult {}
+
+impl lsp2::request::Request for SignOut {
+    type Params = SignOutParams;
+    type Result = SignOutResult;
+    const METHOD: &'static str = "signOut";
+}
+
+pub enum GetCompletions {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsParams {
+    pub doc: GetCompletionsDocument,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsDocument {
+    pub tab_size: u32,
+    pub indent_size: u32,
+    pub insert_spaces: bool,
+    pub uri: lsp2::Url,
+    pub relative_path: String,
+    pub position: lsp2::Position,
+    pub version: usize,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsResult {
+    pub completions: Vec<Completion>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Completion {
+    pub text: String,
+    pub position: lsp2::Position,
+    pub uuid: String,
+    pub range: lsp2::Range,
+    pub display_text: String,
+}
+
+impl lsp2::request::Request for GetCompletions {
+    type Params = GetCompletionsParams;
+    type Result = GetCompletionsResult;
+    const METHOD: &'static str = "getCompletions";
+}
+
+pub enum GetCompletionsCycling {}
+
+impl lsp2::request::Request for GetCompletionsCycling {
+    type Params = GetCompletionsParams;
+    type Result = GetCompletionsResult;
+    const METHOD: &'static str = "getCompletionsCycling";
+}
+
+pub enum LogMessage {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LogMessageParams {
+    pub level: u8,
+    pub message: String,
+    pub metadata_str: String,
+    pub extra: Vec<String>,
+}
+
+impl lsp2::notification::Notification for LogMessage {
+    type Params = LogMessageParams;
+    const METHOD: &'static str = "LogMessage";
+}
+
+pub enum StatusNotification {}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct StatusNotificationParams {
+    pub message: String,
+    pub status: String, // One of Normal/InProgress
+}
+
+impl lsp2::notification::Notification for StatusNotification {
+    type Params = StatusNotificationParams;
+    const METHOD: &'static str = "statusNotification";
+}
+
+pub enum SetEditorInfo {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SetEditorInfoParams {
+    pub editor_info: EditorInfo,
+    pub editor_plugin_info: EditorPluginInfo,
+}
+
+impl lsp2::request::Request for SetEditorInfo {
+    type Params = SetEditorInfoParams;
+    type Result = String;
+    const METHOD: &'static str = "setEditorInfo";
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorInfo {
+    pub name: String,
+    pub version: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorPluginInfo {
+    pub name: String,
+    pub version: String,
+}
+
+pub enum NotifyAccepted {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotifyAcceptedParams {
+    pub uuid: String,
+}
+
+impl lsp2::request::Request for NotifyAccepted {
+    type Params = NotifyAcceptedParams;
+    type Result = String;
+    const METHOD: &'static str = "notifyAccepted";
+}
+
+pub enum NotifyRejected {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotifyRejectedParams {
+    pub uuids: Vec<String>,
+}
+
+impl lsp2::request::Request for NotifyRejected {
+    type Params = NotifyRejectedParams;
+    type Result = String;
+    const METHOD: &'static str = "notifyRejected";
+}

crates/copilot2/src/sign_in.rs 🔗

@@ -0,0 +1,376 @@
+// TODO add logging in
+// use crate::{request::PromptUserDeviceFlow, Copilot, Status};
+// use gpui::{
+//     elements::*,
+//     geometry::rect::RectF,
+//     platform::{WindowBounds, WindowKind, WindowOptions},
+//     AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
+//     WindowHandle,
+// };
+// use theme::ui::modal;
+
+// #[derive(PartialEq, Eq, Debug, Clone)]
+// struct CopyUserCode;
+
+// #[derive(PartialEq, Eq, Debug, Clone)]
+// struct OpenGithub;
+
+// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
+
+// pub fn init(cx: &mut AppContext) {
+//     if let Some(copilot) = Copilot::global(cx) {
+//         let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
+//         cx.observe(&copilot, move |copilot, cx| {
+//             let status = copilot.read(cx).status();
+
+//             match &status {
+//                 crate::Status::SigningIn { prompt } => {
+//                     if let Some(window) = verification_window.as_mut() {
+//                         let updated = window
+//                             .root(cx)
+//                             .map(|root| {
+//                                 root.update(cx, |verification, cx| {
+//                                     verification.set_status(status.clone(), cx);
+//                                     cx.activate_window();
+//                                 })
+//                             })
+//                             .is_some();
+//                         if !updated {
+//                             verification_window = Some(create_copilot_auth_window(cx, &status));
+//                         }
+//                     } else if let Some(_prompt) = prompt {
+//                         verification_window = Some(create_copilot_auth_window(cx, &status));
+//                     }
+//                 }
+//                 Status::Authorized | Status::Unauthorized => {
+//                     if let Some(window) = verification_window.as_ref() {
+//                         if let Some(verification) = window.root(cx) {
+//                             verification.update(cx, |verification, cx| {
+//                                 verification.set_status(status, cx);
+//                                 cx.platform().activate(true);
+//                                 cx.activate_window();
+//                             });
+//                         }
+//                     }
+//                 }
+//                 _ => {
+//                     if let Some(code_verification) = verification_window.take() {
+//                         code_verification.update(cx, |cx| cx.remove_window());
+//                     }
+//                 }
+//             }
+//         })
+//         .detach();
+//     }
+// }
+
+// fn create_copilot_auth_window(
+//     cx: &mut AppContext,
+//     status: &Status,
+// ) -> WindowHandle<CopilotCodeVerification> {
+//     let window_size = theme::current(cx).copilot.modal.dimensions();
+//     let window_options = WindowOptions {
+//         bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
+//         titlebar: None,
+//         center: true,
+//         focus: true,
+//         show: true,
+//         kind: WindowKind::Normal,
+//         is_movable: true,
+//         screen: None,
+//     };
+//     cx.add_window(window_options, |_cx| {
+//         CopilotCodeVerification::new(status.clone())
+//     })
+// }
+
+// pub struct CopilotCodeVerification {
+//     status: Status,
+//     connect_clicked: bool,
+// }
+
+// impl CopilotCodeVerification {
+//     pub fn new(status: Status) -> Self {
+//         Self {
+//             status,
+//             connect_clicked: false,
+//         }
+//     }
+
+//     pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
+//         self.status = status;
+//         cx.notify();
+//     }
+
+//     fn render_device_code(
+//         data: &PromptUserDeviceFlow,
+//         style: &theme::Copilot,
+//         cx: &mut ViewContext<Self>,
+//     ) -> impl Element<Self> {
+//         let copied = cx
+//             .read_from_clipboard()
+//             .map(|item| item.text() == &data.user_code)
+//             .unwrap_or(false);
+
+//         let device_code_style = &style.auth.prompting.device_code;
+
+//         MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
+//             Flex::row()
+//                 .with_child(
+//                     Label::new(data.user_code.clone(), device_code_style.text.clone())
+//                         .aligned()
+//                         .contained()
+//                         .with_style(device_code_style.left_container)
+//                         .constrained()
+//                         .with_width(device_code_style.left),
+//                 )
+//                 .with_child(
+//                     Label::new(
+//                         if copied { "Copied!" } else { "Copy" },
+//                         device_code_style.cta.style_for(state).text.clone(),
+//                     )
+//                     .aligned()
+//                     .contained()
+//                     .with_style(*device_code_style.right_container.style_for(state))
+//                     .constrained()
+//                     .with_width(device_code_style.right),
+//                 )
+//                 .contained()
+//                 .with_style(device_code_style.cta.style_for(state).container)
+//         })
+//         .on_click(gpui::platform::MouseButton::Left, {
+//             let user_code = data.user_code.clone();
+//             move |_, _, cx| {
+//                 cx.platform()
+//                     .write_to_clipboard(ClipboardItem::new(user_code.clone()));
+//                 cx.notify();
+//             }
+//         })
+//         .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
+//     }
+
+//     fn render_prompting_modal(
+//         connect_clicked: bool,
+//         data: &PromptUserDeviceFlow,
+//         style: &theme::Copilot,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum ConnectButton {}
+
+//         Flex::column()
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "Enable Copilot by connecting",
+//                             style.auth.prompting.subheading.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "your existing license.",
+//                             style.auth.prompting.subheading.text.clone(),
+//                         )
+//                         .aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(style.auth.prompting.subheading.container),
+//             )
+//             .with_child(Self::render_device_code(data, &style, cx))
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "Paste this code into GitHub after",
+//                             style.auth.prompting.hint.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "clicking the button below.",
+//                             style.auth.prompting.hint.text.clone(),
+//                         )
+//                         .aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(style.auth.prompting.hint.container.clone()),
+//             )
+//             .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
+//                 if connect_clicked {
+//                     "Waiting for connection..."
+//                 } else {
+//                     "Connect to GitHub"
+//                 },
+//                 style.auth.content_width,
+//                 &style.auth.cta_button,
+//                 cx,
+//                 {
+//                     let verification_uri = data.verification_uri.clone();
+//                     move |_, verification, cx| {
+//                         cx.platform().open_url(&verification_uri);
+//                         verification.connect_clicked = true;
+//                     }
+//                 },
+//             ))
+//             .align_children_center()
+//             .into_any()
+//     }
+
+//     fn render_enabled_modal(
+//         style: &theme::Copilot,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         enum DoneButton {}
+
+//         let enabled_style = &style.auth.authorized;
+//         Flex::column()
+//             .with_child(
+//                 Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
+//                     .contained()
+//                     .with_style(enabled_style.subheading.container)
+//                     .aligned(),
+//             )
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "You can update your settings or",
+//                             enabled_style.hint.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "sign out from the Copilot menu in",
+//                             enabled_style.hint.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(enabled_style.hint.container),
+//             )
+//             .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
+//                 "Done",
+//                 style.auth.content_width,
+//                 &style.auth.cta_button,
+//                 cx,
+//                 |_, _, cx| cx.remove_window(),
+//             ))
+//             .align_children_center()
+//             .into_any()
+//     }
+
+//     fn render_unauthorized_modal(
+//         style: &theme::Copilot,
+//         cx: &mut ViewContext<Self>,
+//     ) -> AnyElement<Self> {
+//         let unauthorized_style = &style.auth.not_authorized;
+
+//         Flex::column()
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "Enable Copilot by connecting",
+//                             unauthorized_style.subheading.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "your existing license.",
+//                             unauthorized_style.subheading.text.clone(),
+//                         )
+//                         .aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(unauthorized_style.subheading.container),
+//             )
+//             .with_child(
+//                 Flex::column()
+//                     .with_children([
+//                         Label::new(
+//                             "You must have an active copilot",
+//                             unauthorized_style.warning.text.clone(),
+//                         )
+//                         .aligned(),
+//                         Label::new(
+//                             "license to use it in Zed.",
+//                             unauthorized_style.warning.text.clone(),
+//                         )
+//                         .aligned(),
+//                     ])
+//                     .align_children_center()
+//                     .contained()
+//                     .with_style(unauthorized_style.warning.container),
+//             )
+//             .with_child(theme::ui::cta_button::<Self, _, _, _>(
+//                 "Subscribe on GitHub",
+//                 style.auth.content_width,
+//                 &style.auth.cta_button,
+//                 cx,
+//                 |_, _, cx| {
+//                     cx.remove_window();
+//                     cx.platform().open_url(COPILOT_SIGN_UP_URL)
+//                 },
+//             ))
+//             .align_children_center()
+//             .into_any()
+//     }
+// }
+
+// impl Entity for CopilotCodeVerification {
+//     type Event = ();
+// }
+
+// impl View for CopilotCodeVerification {
+//     fn ui_name() -> &'static str {
+//         "CopilotCodeVerification"
+//     }
+
+//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         cx.notify()
+//     }
+
+//     fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+//         cx.notify()
+//     }
+
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         enum ConnectModal {}
+
+//         let style = theme::current(cx).clone();
+
+//         modal::<ConnectModal, _, _, _, _>(
+//             "Connect Copilot to Zed",
+//             &style.copilot.modal,
+//             cx,
+//             |cx| {
+//                 Flex::column()
+//                     .with_children([
+//                         theme::ui::icon(&style.copilot.auth.header).into_any(),
+//                         match &self.status {
+//                             Status::SigningIn {
+//                                 prompt: Some(prompt),
+//                             } => Self::render_prompting_modal(
+//                                 self.connect_clicked,
+//                                 &prompt,
+//                                 &style.copilot,
+//                                 cx,
+//                             ),
+//                             Status::Unauthorized => {
+//                                 self.connect_clicked = false;
+//                                 Self::render_unauthorized_modal(&style.copilot, cx)
+//                             }
+//                             Status::Authorized => {
+//                                 self.connect_clicked = false;
+//                                 Self::render_enabled_modal(&style.copilot, cx)
+//                             }
+//                             _ => Empty::new().into_any(),
+//                         },
+//                     ])
+//                     .align_children_center()
+//             },
+//         )
+//         .into_any()
+//     }
+// }

crates/gpui2/src/app/model_context.rs 🔗

@@ -87,7 +87,7 @@ impl<'a, T: Send + Sync + 'static> ModelContext<'a, T> {
 
     pub fn on_app_quit<Fut>(
         &mut self,
-        on_quit: impl Fn(&mut T, &mut AppContext) -> Fut + Send + Sync + 'static,
+        on_quit: impl Fn(&mut T, &mut ModelContext<T>) -> Fut + Send + Sync + 'static,
     ) -> Subscription
     where
         Fut: 'static + Future<Output = ()> + Send,

crates/project2/src/project2.rs 🔗

@@ -26,8 +26,7 @@ use futures::{
 };
 use globset::{Glob, GlobSet, GlobSetBuilder};
 use gpui2::{
-    AnyHandle, AppContext, AsyncAppContext, EventEmitter, Executor, Handle, ModelContext, Task,
-    WeakHandle,
+    AnyHandle, AppContext, AsyncAppContext, EventEmitter, Handle, ModelContext, Task, WeakHandle,
 };
 use itertools::Itertools;
 use language2::{
@@ -821,7 +820,10 @@ impl Project {
         }
     }
 
-    fn shutdown_language_servers(&mut self) -> impl Future<Output = ()> {
+    fn shutdown_language_servers(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> impl Future<Output = ()> {
         let shutdown_futures = self
             .language_servers
             .drain()
@@ -5683,7 +5685,7 @@ impl Project {
     async fn background_search(
         unnamed_buffers: Vec<Handle<Buffer>>,
         opened_buffers: HashMap<Arc<Path>, (Handle<Buffer>, BufferSnapshot)>,
-        executor: Executor,
+        executor: Arc<Background>,
         fs: Arc<dyn Fs>,
         workers: usize,
         query: SearchQuery,

crates/project2/src/terminals.rs 🔗

@@ -116,7 +116,7 @@ impl Project {
         }
     }
 
-    pub fn local_terminal_handles(&self) -> &Vec<WeakModelHandle<terminal::Terminal>> {
+    pub fn local_terminal_handles(&self) -> &Vec<WeakHandle<terminal::Terminal>> {
         &self.terminals.local_handles
     }
 }

crates/zed2/Cargo.toml 🔗

@@ -29,7 +29,7 @@ collections = { path = "../collections" }
 # context_menu = { path = "../context_menu" }
 client2 = { path = "../client2" }
 # clock = { path = "../clock" }
-# copilot = { path = "../copilot" }
+copilot2 = { path = "../copilot2" }
 # copilot_button = { path = "../copilot_button" }
 # diagnostics = { path = "../diagnostics" }
 db2 = { path = "../db2" }
@@ -44,7 +44,7 @@ fuzzy = { path = "../fuzzy" }
 gpui2 = { path = "../gpui2" }
 install_cli = { path = "../install_cli" }
 # journal = { path = "../journal" }
-# language = { path = "../language" }
+language2 = { path = "../language2" }
 # language_selector = { path = "../language_selector" }
 lsp = { path = "../lsp" }
 language_tools = { path = "../language_tools" }

crates/zed2/src/main.rs 🔗

@@ -109,8 +109,8 @@ fn main() {
         // handle_keymap_file_changes(user_keymap_file_rx, cx);
 
         // let client = client2::Client::new(http.clone(), cx);
-        // let mut languages = LanguageRegistry::new(login_shell_env_loaded);
-        // let copilot_language_server_id = languages.next_language_server_id();
+        let mut languages = LanguageRegistry::new(login_shell_env_loaded);
+        let copilot_language_server_id = languages.next_language_server_id();
         // languages.set_executor(cx.background().clone());
         // languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
         // let languages = Arc::new(languages);
@@ -140,12 +140,12 @@ fn main() {
         // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
         // vim::init(cx);
         // terminal_view::init(cx);
-        // copilot::init(
-        //     copilot_language_server_id,
-        //     http.clone(),
-        //     node_runtime.clone(),
-        //     cx,
-        // );
+        copilot2::init(
+            copilot_language_server_id,
+            http.clone(),
+            node_runtime.clone(),
+            cx,
+        );
         // assistant::init(cx);
         // component_test::init(cx);