Successfully fetch completions from Copilot

Antonio Scandurra created

We still need to process them and return them into a more Zed-friendly
structure, but we're getting there.

Change summary

crates/copilot/Cargo.toml     |  9 ++++
crates/copilot/src/copilot.rs | 70 +++++++++++++++++++++++++++++++++++-
crates/copilot/src/request.rs | 45 +++++++++++++++++++++++
3 files changed, 122 insertions(+), 2 deletions(-)

Detailed changes

crates/copilot/Cargo.toml 🔗

@@ -23,3 +23,12 @@ serde = { workspace = true }
 serde_derive = { workspace = true }
 smol = "1.2.5"
 futures = "0.3"
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+client = { path = "../client", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/copilot/src/copilot.rs 🔗

@@ -4,8 +4,9 @@ use anyhow::{anyhow, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use client::Client;
 use gpui::{actions, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
-use language::{Buffer, ToPointUtf16};
+use language::{point_to_lsp, Buffer, ToPointUtf16};
 use lsp::LanguageServer;
+use settings::Settings;
 use smol::{fs, io::BufReader, stream::StreamExt};
 use std::{
     env::consts,
@@ -53,6 +54,7 @@ enum SignInStatus {
     SignedOut,
 }
 
+#[derive(Debug)]
 pub enum Event {
     PromptUserDeviceFlow {
         user_code: String,
@@ -198,7 +200,45 @@ impl Copilot {
             Err(error) => return Task::ready(Err(error)),
         };
 
-        cx.spawn(|this, cx| async move { anyhow::Ok(()) })
+        let buffer = buffer.read(cx).snapshot();
+        let position = position.to_point_utf16(&buffer);
+        let language_name = buffer.language_at(position).map(|language| language.name());
+        let language_name = language_name.as_deref();
+
+        let path;
+        let relative_path;
+        if let Some(file) = buffer.file() {
+            if let Some(file) = file.as_local() {
+                path = file.abs_path(cx);
+            } else {
+                path = file.full_path(cx);
+            }
+            relative_path = file.path().to_path_buf();
+        } else {
+            path = PathBuf::from("/untitled");
+            relative_path = PathBuf::from("untitled");
+        }
+
+        let settings = cx.global::<Settings>();
+        let request = server.request::<request::GetCompletions>(request::GetCompletionsParams {
+            doc: request::GetCompletionsDocument {
+                source: buffer.text(),
+                tab_size: settings.tab_size(language_name).into(),
+                indent_size: 1,
+                insert_spaces: !settings.hard_tabs(language_name),
+                uri: lsp::Url::from_file_path(&path).unwrap(),
+                path: path.to_string_lossy().into(),
+                relative_path: relative_path.to_string_lossy().into(),
+                language_id: "csharp".into(),
+                position: point_to_lsp(position),
+                version: 0,
+            },
+        });
+        cx.spawn(|this, cx| async move {
+            dbg!(request.await?);
+
+            anyhow::Ok(())
+        })
     }
 
     pub fn status(&self) -> Status {
@@ -302,3 +342,29 @@ async fn get_lsp_binary(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::TestAppContext;
+    use util::http;
+
+    #[gpui::test]
+    async fn test_smoke(cx: &mut TestAppContext) {
+        Settings::test_async(cx);
+        let http = http::client();
+        let copilot = cx.add_model(|cx| Copilot::start(http, cx));
+        smol::Timer::after(std::time::Duration::from_secs(5)).await;
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_in(cx))
+            .await
+            .unwrap();
+        dbg!(copilot.read_with(cx, |copilot, _| copilot.status()));
+
+        let buffer = cx.add_model(|cx| language::Buffer::new(0, "Lorem ipsum dol", cx));
+        copilot
+            .update(cx, |copilot, cx| copilot.completions(&buffer, 15, cx))
+            .await
+            .unwrap();
+    }
+}

crates/copilot/src/request.rs 🔗

@@ -87,3 +87,48 @@ impl lsp::request::Request for SignOut {
     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 source: String,
+    pub tab_size: u32,
+    pub indent_size: u32,
+    pub insert_spaces: bool,
+    pub uri: lsp::Url,
+    pub path: String,
+    pub relative_path: String,
+    pub language_id: String,
+    pub position: lsp::Position,
+    pub version: usize,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsResult {
+    completions: Vec<Completion>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Completion {
+    text: String,
+    position: lsp::Position,
+    uuid: String,
+    range: lsp::Range,
+    display_text: String,
+}
+
+impl lsp::request::Request for GetCompletions {
+    type Params = GetCompletionsParams;
+    type Result = GetCompletionsResult;
+    const METHOD: &'static str = "getCompletions";
+}