Merge pull request #2393 from zed-industries/relay-change-events-to-copilot

Antonio Scandurra created

Relay buffer change events to Copilot language server

Change summary

Cargo.lock                        |   4 
crates/copilot/Cargo.toml         |   5 
crates/copilot/src/copilot.rs     | 784 +++++++++++++++++++++++++++-----
crates/copilot/src/request.rs     |  60 ++
crates/editor/src/editor.rs       |  69 +
crates/editor/src/multi_buffer.rs |   2 
crates/language/src/buffer.rs     |   2 
crates/project/Cargo.toml         |   1 
crates/project/src/project.rs     |  24 +
9 files changed, 783 insertions(+), 168 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1341,14 +1341,17 @@ dependencies = [
  "anyhow",
  "async-compression",
  "async-tar",
+ "clock",
  "collections",
  "context_menu",
+ "fs",
  "futures 0.3.25",
  "gpui",
  "language",
  "log",
  "lsp",
  "node_runtime",
+ "rpc",
  "serde",
  "serde_derive",
  "settings",
@@ -4687,6 +4690,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "copilot",
  "ctor",
  "db",
  "env_logger",

crates/copilot/Cargo.toml 🔗

@@ -38,10 +38,13 @@ smol = "1.2.5"
 futures = "0.3"
 
 [dev-dependencies]
+clock = { path = "../clock" }
 collections = { path = "../collections", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }

crates/copilot/src/copilot.rs 🔗

@@ -5,9 +5,14 @@ use anyhow::{anyhow, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use collections::HashMap;
-use futures::{future::Shared, Future, FutureExt, TryFutureExt};
-use gpui::{actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
-use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, Language, ToPointUtf16};
+use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
+use gpui::{
+    actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
+};
+use language::{
+    point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
+    ToPointUtf16,
+};
 use log::{debug, error};
 use lsp::LanguageServer;
 use node_runtime::NodeRuntime;
@@ -16,6 +21,7 @@ use settings::Settings;
 use smol::{fs, io::BufReader, stream::StreamExt};
 use std::{
     ffi::OsString,
+    mem,
     ops::Range,
     path::{Path, PathBuf},
     sync::Arc,
@@ -98,15 +104,38 @@ pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut
 
 enum CopilotServer {
     Disabled,
-    Starting {
-        task: Shared<Task<()>>,
-    },
+    Starting { task: Shared<Task<()>> },
     Error(Arc<str>),
-    Started {
-        server: Arc<LanguageServer>,
-        status: SignInStatus,
-        subscriptions_by_buffer_id: HashMap<usize, gpui::Subscription>,
-    },
+    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 {
+    lsp: Arc<LanguageServer>,
+    sign_in_status: SignInStatus,
+    registered_buffers: HashMap<usize, RegisteredBuffer>,
 }
 
 #[derive(Clone, Debug)]
@@ -141,8 +170,104 @@ impl Status {
     }
 }
 
-#[derive(Debug, PartialEq, Eq)]
+struct RegisteredBuffer {
+    id: usize,
+    uri: lsp::Url,
+    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: &ModelHandle<Buffer>,
+        cx: &mut ModelContext<Copilot>,
+    ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
+        let id = self.id;
+        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 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(&cx)?
+                    .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();
+                                    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.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::<lsp::notification::DidChangeTextDocument>(
+                                lsp::DidChangeTextDocumentParams {
+                                    text_document: lsp::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 {
+    uuid: String,
     pub range: Range<Anchor>,
     pub text: String,
 }
@@ -151,6 +276,7 @@ pub struct Copilot {
     http: Arc<dyn HttpClient>,
     node_runtime: Arc<NodeRuntime>,
     server: CopilotServer,
+    buffers: HashMap<usize, WeakModelHandle<Buffer>>,
 }
 
 impl Entity for Copilot {
@@ -212,12 +338,14 @@ impl Copilot {
                 http,
                 node_runtime,
                 server: CopilotServer::Starting { task: start_task },
+                buffers: Default::default(),
             }
         } else {
             Self {
                 http,
                 node_runtime,
                 server: CopilotServer::Disabled,
+                buffers: Default::default(),
             }
         }
     }
@@ -230,11 +358,12 @@ impl Copilot {
         let this = cx.add_model(|cx| Self {
             http: http.clone(),
             node_runtime: NodeRuntime::new(http, cx.background().clone()),
-            server: CopilotServer::Started {
-                server: Arc::new(server),
-                status: SignInStatus::Authorized,
-                subscriptions_by_buffer_id: Default::default(),
-            },
+            server: CopilotServer::Running(RunningCopilotServer {
+                lsp: Arc::new(server),
+                sign_in_status: SignInStatus::Authorized,
+                registered_buffers: Default::default(),
+            }),
+            buffers: Default::default(),
         });
         (this, fake_server)
     }
@@ -286,6 +415,19 @@ impl Copilot {
                     )
                     .detach();
 
+                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))
             };
 
@@ -294,11 +436,11 @@ impl Copilot {
                 cx.notify();
                 match server {
                     Ok((server, status)) => {
-                        this.server = CopilotServer::Started {
-                            server,
-                            status: SignInStatus::SignedOut,
-                            subscriptions_by_buffer_id: Default::default(),
-                        };
+                        this.server = CopilotServer::Running(RunningCopilotServer {
+                            lsp: server,
+                            sign_in_status: SignInStatus::SignedOut,
+                            registered_buffers: Default::default(),
+                        });
                         this.update_sign_in_status(status, cx);
                     }
                     Err(error) => {
@@ -311,8 +453,8 @@ impl Copilot {
     }
 
     fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if let CopilotServer::Started { server, status, .. } = &mut self.server {
-            let task = match status {
+        if let CopilotServer::Running(server) = &mut self.server {
+            let task = match &server.sign_in_status {
                 SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
                     Task::ready(Ok(())).shared()
                 }
@@ -321,11 +463,11 @@ impl Copilot {
                     task.clone()
                 }
                 SignInStatus::SignedOut => {
-                    let server = server.clone();
+                    let lsp = server.lsp.clone();
                     let task = cx
                         .spawn(|this, mut cx| async move {
                             let sign_in = async {
-                                let sign_in = server
+                                let sign_in = lsp
                                     .request::<request::SignInInitiate>(
                                         request::SignInInitiateParams {},
                                     )
@@ -336,8 +478,10 @@ impl Copilot {
                                     }
                                     request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
                                         this.update(&mut cx, |this, cx| {
-                                            if let CopilotServer::Started { status, .. } =
-                                                &mut this.server
+                                            if let CopilotServer::Running(RunningCopilotServer {
+                                                sign_in_status: status,
+                                                ..
+                                            }) = &mut this.server
                                             {
                                                 if let SignInStatus::SigningIn {
                                                     prompt: prompt_flow,
@@ -349,7 +493,7 @@ impl Copilot {
                                                 }
                                             }
                                         });
-                                        let response = server
+                                        let response = lsp
                                             .request::<request::SignInConfirm>(
                                                 request::SignInConfirmParams {
                                                     user_code: flow.user_code,
@@ -377,7 +521,7 @@ impl Copilot {
                             })
                         })
                         .shared();
-                    *status = SignInStatus::SigningIn {
+                    server.sign_in_status = SignInStatus::SigningIn {
                         prompt: None,
                         task: task.clone(),
                     };
@@ -396,10 +540,8 @@ impl Copilot {
     }
 
     fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if let CopilotServer::Started { server, status, .. } = &mut self.server {
-            *status = SignInStatus::SignedOut;
-            cx.notify();
-
+        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
@@ -433,6 +575,135 @@ impl Copilot {
         cx.foreground().spawn(start_task)
     }
 
+    pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
+        let buffer_id = buffer.id();
+        self.buffers.insert(buffer_id, buffer.downgrade());
+
+        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: lsp::Url = uri_for_buffer(buffer, cx);
+                let language_id = id_for_language(buffer.read(cx).language());
+                let snapshot = buffer.read(cx).snapshot();
+                server
+                    .notify::<lsp::notification::DidOpenTextDocument>(
+                        lsp::DidOpenTextDocumentParams {
+                            text_document: lsp::TextDocumentItem {
+                                uri: uri.clone(),
+                                language_id: language_id.clone(),
+                                version: 0,
+                                text: snapshot.text(),
+                            },
+                        },
+                    )
+                    .log_err();
+
+                RegisteredBuffer {
+                    id: buffer_id,
+                    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(&buffer_id);
+                            this.unregister_buffer(buffer_id);
+                        }),
+                    ],
+                }
+            });
+        }
+    }
+
+    fn handle_buffer_event(
+        &mut self,
+        buffer: ModelHandle<Buffer>,
+        event: &language::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 {
+                    language::Event::Edited => {
+                        let _ = registered_buffer.report_changes(&buffer, cx);
+                    }
+                    language::Event::Saved => {
+                        server
+                            .lsp
+                            .notify::<lsp::notification::DidSaveTextDocument>(
+                                lsp::DidSaveTextDocumentParams {
+                                    text_document: lsp::TextDocumentIdentifier::new(
+                                        registered_buffer.uri.clone(),
+                                    ),
+                                    text: None,
+                                },
+                            )?;
+                    }
+                    language::Event::FileHandleChanged | language::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::<lsp::notification::DidCloseTextDocument>(
+                                    lsp::DidCloseTextDocumentParams {
+                                        text_document: lsp::TextDocumentIdentifier::new(old_uri),
+                                    },
+                                )?;
+                            server
+                                .lsp
+                                .notify::<lsp::notification::DidOpenTextDocument>(
+                                    lsp::DidOpenTextDocumentParams {
+                                        text_document: lsp::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_id: usize) {
+        if let Ok(server) = self.server.as_running() {
+            if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
+                server
+                    .lsp
+                    .notify::<lsp::notification::DidCloseTextDocument>(
+                        lsp::DidCloseTextDocumentParams {
+                            text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
+                        },
+                    )
+                    .log_err();
+            }
+        }
+    }
+
     pub fn completions<T>(
         &mut self,
         buffer: &ModelHandle<Buffer>,
@@ -457,6 +728,51 @@ impl Copilot {
         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: &ModelHandle<Buffer>,
@@ -464,116 +780,48 @@ impl Copilot {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>>
     where
-        R: lsp::request::Request<
-            Params = request::GetCompletionsParams,
-            Result = request::GetCompletionsResult,
-        >,
+        R: 'static
+            + lsp::request::Request<
+                Params = request::GetCompletionsParams,
+                Result = request::GetCompletionsResult,
+            >,
         T: ToPointUtf16,
     {
-        let buffer_id = buffer.id();
-        let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap();
-        let snapshot = buffer.read(cx).snapshot();
-        let server = match &mut self.server {
-            CopilotServer::Starting { .. } => {
-                return Task::ready(Err(anyhow!("copilot is still starting")))
-            }
-            CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))),
-            CopilotServer::Error(error) => {
-                return Task::ready(Err(anyhow!(
-                    "copilot was not started because of an error: {}",
-                    error
-                )))
-            }
-            CopilotServer::Started {
-                server,
-                status,
-                subscriptions_by_buffer_id,
-            } => {
-                if matches!(status, SignInStatus::Authorized { .. }) {
-                    subscriptions_by_buffer_id
-                        .entry(buffer_id)
-                        .or_insert_with(|| {
-                            server
-                                .notify::<lsp::notification::DidOpenTextDocument>(
-                                    lsp::DidOpenTextDocumentParams {
-                                        text_document: lsp::TextDocumentItem {
-                                            uri: uri.clone(),
-                                            language_id: id_for_language(
-                                                buffer.read(cx).language(),
-                                            ),
-                                            version: 0,
-                                            text: snapshot.text(),
-                                        },
-                                    },
-                                )
-                                .log_err();
-
-                            let uri = uri.clone();
-                            cx.observe_release(buffer, move |this, _, _| {
-                                if let CopilotServer::Started {
-                                    server,
-                                    subscriptions_by_buffer_id,
-                                    ..
-                                } = &mut this.server
-                                {
-                                    server
-                                        .notify::<lsp::notification::DidCloseTextDocument>(
-                                            lsp::DidCloseTextDocumentParams {
-                                                text_document: lsp::TextDocumentIdentifier::new(
-                                                    uri.clone(),
-                                                ),
-                                            },
-                                        )
-                                        .log_err();
-                                    subscriptions_by_buffer_id.remove(&buffer_id);
-                                }
-                            })
-                        });
+        self.register_buffer(buffer, cx);
 
-                    server.clone()
-                } else {
-                    return Task::ready(Err(anyhow!("must sign in before using copilot")));
-                }
-            }
+        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 settings = cx.global::<Settings>();
-        let position = position.to_point_utf16(&snapshot);
-        let language = snapshot.language_at(position);
+        let position = position.to_point_utf16(buffer);
+        let language = buffer.language_at(position);
         let language_name = language.map(|language| language.name());
         let language_name = language_name.as_deref();
         let tab_size = settings.tab_size(language_name);
         let hard_tabs = settings.hard_tabs(language_name);
-        let language_id = id_for_language(language);
-
-        let path;
-        let relative_path;
-        if let Some(file) = snapshot.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::new();
-            relative_path = PathBuf::new();
-        }
+        let relative_path = buffer
+            .file()
+            .map(|file| file.path().to_path_buf())
+            .unwrap_or_default();
 
-        cx.background().spawn(async move {
-            let result = server
+        cx.foreground().spawn(async move {
+            let (version, snapshot) = snapshot.await?;
+            let result = lsp
                 .request::<R>(request::GetCompletionsParams {
                     doc: request::GetCompletionsDocument {
-                        source: snapshot.text(),
+                        uri,
                         tab_size: tab_size.into(),
                         indent_size: 1,
                         insert_spaces: !hard_tabs,
-                        uri,
-                        path: path.to_string_lossy().into(),
                         relative_path: relative_path.to_string_lossy().into(),
-                        language_id,
                         position: point_to_lsp(position),
-                        version: 0,
+                        version: version.try_into().unwrap(),
                     },
                 })
                 .await?;
@@ -586,6 +834,7 @@ impl Copilot {
                     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,
                     }
@@ -600,14 +849,16 @@ impl Copilot {
             CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
             CopilotServer::Disabled => Status::Disabled,
             CopilotServer::Error(error) => Status::Error(error.clone()),
-            CopilotServer::Started { status, .. } => match status {
-                SignInStatus::Authorized { .. } => Status::Authorized,
-                SignInStatus::Unauthorized { .. } => Status::Unauthorized,
-                SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
-                    prompt: prompt.clone(),
-                },
-                SignInStatus::SignedOut => Status::SignedOut,
-            },
+            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,
+                }
+            }
         }
     }
 
@@ -616,14 +867,34 @@ impl Copilot {
         lsp_status: request::SignInStatus,
         cx: &mut ModelContext<Self>,
     ) {
-        if let CopilotServer::Started { status, .. } = &mut self.server {
-            *status = match lsp_status {
+        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 { .. } => SignInStatus::Authorized,
-                request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized,
-                request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
-            };
+                | request::SignInStatus::AlreadySignedIn { .. } => {
+                    server.sign_in_status = SignInStatus::Authorized;
+                    for buffer in self.buffers.values().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_id in self.buffers.keys().copied().collect::<Vec<_>>() {
+                        self.unregister_buffer(buffer_id);
+                    }
+                }
+                request::SignInStatus::NotSignedIn => {
+                    server.sign_in_status = SignInStatus::SignedOut;
+                    for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
+                        self.unregister_buffer(buffer_id);
+                    }
+                }
+            }
+
             cx.notify();
         }
     }
@@ -638,6 +909,14 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
     }
 }
 
+fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
+    if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
+        lsp::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
 }
@@ -709,3 +988,226 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
         }
     }
 }
+
+#[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, "Hello", cx));
+        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, "Goodbye", cx));
+        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 language::File for File {
+        fn as_local(&self) -> Option<&dyn language::LocalFile> {
+            Some(self)
+        }
+
+        fn mtime(&self) -> std::time::SystemTime {
+            todo!()
+        }
+
+        fn path(&self) -> &Arc<Path> {
+            &self.path
+        }
+
+        fn full_path(&self, _: &AppContext) -> PathBuf {
+            todo!()
+        }
+
+        fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+            todo!()
+        }
+
+        fn is_deleted(&self) -> bool {
+            todo!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            todo!()
+        }
+
+        fn to_proto(&self) -> rpc::proto::File {
+            todo!()
+        }
+    }
+
+    impl language::LocalFile for File {
+        fn abs_path(&self, _: &AppContext) -> PathBuf {
+            self.abs_path.clone()
+        }
+
+        fn load(&self, _: &AppContext) -> Task<Result<String>> {
+            todo!()
+        }
+
+        fn buffer_reloaded(
+            &self,
+            _: u64,
+            _: &clock::Global,
+            _: language::RopeFingerprint,
+            _: ::fs::LineEnding,
+            _: std::time::SystemTime,
+            _: &mut AppContext,
+        ) {
+            todo!()
+        }
+    }
+}

crates/copilot/src/request.rs 🔗

@@ -99,14 +99,11 @@ pub struct GetCompletionsParams {
 #[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,
 }
@@ -169,3 +166,60 @@ impl lsp::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 lsp::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 lsp::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 lsp::request::Request for NotifyRejected {
+    type Params = NotifyRejectedParams;
+    type Result = String;
+    const METHOD: &'static str = "notifyRejected";
+}

crates/editor/src/editor.rs 🔗

@@ -52,7 +52,7 @@ pub use language::{char_kind, CharKind};
 use language::{
     AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
     Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
-    Point, Rope, Selection, SelectionGoal, TransactionId,
+    Point, Selection, SelectionGoal, TransactionId,
 };
 use link_go_to_definition::{
     hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
@@ -1037,6 +1037,10 @@ impl Default for CopilotState {
 }
 
 impl CopilotState {
+    fn active_completion(&self) -> Option<&copilot::Completion> {
+        self.completions.get(self.active_completion_index)
+    }
+
     fn text_for_active_completion(
         &self,
         cursor: Anchor,
@@ -1044,7 +1048,7 @@ impl CopilotState {
     ) -> Option<&str> {
         use language::ToOffset as _;
 
-        let completion = self.completions.get(self.active_completion_index)?;
+        let completion = self.active_completion()?;
         let excerpt_id = self.excerpt_id?;
         let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
         if excerpt_id != cursor.excerpt_id
@@ -1097,7 +1101,7 @@ impl CopilotState {
 
     fn push_completion(&mut self, new_completion: copilot::Completion) {
         for completion in &self.completions {
-            if *completion == new_completion {
+            if completion.text == new_completion.text && completion.range == new_completion.range {
                 return;
             }
         }
@@ -1496,7 +1500,7 @@ impl Editor {
             self.refresh_code_actions(cx);
             self.refresh_document_highlights(cx);
             refresh_matching_bracket_highlights(self, cx);
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         }
 
         self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -1870,7 +1874,7 @@ impl Editor {
             return;
         }
 
-        if self.hide_copilot_suggestion(cx).is_some() {
+        if self.discard_copilot_suggestion(cx) {
             return;
         }
 
@@ -2969,7 +2973,7 @@ impl Editor {
         Some(())
     }
 
-    fn cycle_suggestions(
+    fn cycle_copilot_suggestions(
         &mut self,
         direction: Direction,
         cx: &mut ViewContext<Self>,
@@ -3020,7 +3024,7 @@ impl Editor {
 
     fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
         if self.has_active_copilot_suggestion(cx) {
-            self.cycle_suggestions(Direction::Next, cx);
+            self.cycle_copilot_suggestions(Direction::Next, cx);
         } else {
             self.refresh_copilot_suggestions(false, cx);
         }
@@ -3032,37 +3036,55 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if self.has_active_copilot_suggestion(cx) {
-            self.cycle_suggestions(Direction::Prev, cx);
+            self.cycle_copilot_suggestions(Direction::Prev, cx);
         } else {
             self.refresh_copilot_suggestions(false, cx);
         }
     }
 
     fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if let Some(text) = self.hide_copilot_suggestion(cx) {
-            self.insert_with_autoindent_mode(&text.to_string(), None, cx);
+        if let Some(suggestion) = self
+            .display_map
+            .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
+        {
+            if let Some((copilot, completion)) =
+                Copilot::global(cx).zip(self.copilot_state.active_completion())
+            {
+                copilot
+                    .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
+                    .detach_and_log_err(cx);
+            }
+            self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
+            cx.notify();
             true
         } else {
             false
         }
     }
 
-    fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
-        self.display_map.read(cx).has_suggestion()
-    }
-
-    fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
+    fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if self.has_active_copilot_suggestion(cx) {
-            let old_suggestion = self
-                .display_map
+            if let Some(copilot) = Copilot::global(cx) {
+                copilot
+                    .update(cx, |copilot, cx| {
+                        copilot.discard_completions(&self.copilot_state.completions, cx)
+                    })
+                    .detach_and_log_err(cx);
+            }
+
+            self.display_map
                 .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
             cx.notify();
-            old_suggestion.map(|suggestion| suggestion.text)
+            true
         } else {
-            None
+            false
         }
     }
 
+    fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
+        self.display_map.read(cx).has_suggestion()
+    }
+
     fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let selection = self.selections.newest_anchor();
@@ -3072,7 +3094,7 @@ impl Editor {
             || !self.completion_tasks.is_empty()
             || selection.start != selection.end
         {
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         } else if let Some(text) = self
             .copilot_state
             .text_for_active_completion(cursor, &snapshot)
@@ -3088,13 +3110,13 @@ impl Editor {
             });
             cx.notify();
         } else {
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         }
     }
 
     fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
         self.copilot_state = Default::default();
-        self.hide_copilot_suggestion(cx);
+        self.discard_copilot_suggestion(cx);
     }
 
     pub fn render_code_actions_indicator(
@@ -3212,7 +3234,7 @@ impl Editor {
             self.completion_tasks.clear();
         }
         self.context_menu = Some(menu);
-        self.hide_copilot_suggestion(cx);
+        self.discard_copilot_suggestion(cx);
         cx.notify();
     }
 
@@ -6643,6 +6665,7 @@ impl Editor {
             multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
             }
+            multi_buffer::Event::LanguageChanged => {}
         }
     }
 

crates/editor/src/multi_buffer.rs 🔗

@@ -64,6 +64,7 @@ pub enum Event {
     },
     Edited,
     Reloaded,
+    LanguageChanged,
     Reparsed,
     Saved,
     FileHandleChanged,
@@ -1302,6 +1303,7 @@ impl MultiBuffer {
             language::Event::Saved => Event::Saved,
             language::Event::FileHandleChanged => Event::FileHandleChanged,
             language::Event::Reloaded => Event::Reloaded,
+            language::Event::LanguageChanged => Event::LanguageChanged,
             language::Event::Reparsed => Event::Reparsed,
             language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
             language::Event::Closed => Event::Closed,

crates/language/src/buffer.rs 🔗

@@ -187,6 +187,7 @@ pub enum Event {
     Saved,
     FileHandleChanged,
     Reloaded,
+    LanguageChanged,
     Reparsed,
     DiagnosticsUpdated,
     Closed,
@@ -536,6 +537,7 @@ impl Buffer {
         self.syntax_map.lock().clear();
         self.language = language;
         self.reparse(cx);
+        cx.emit(Event::LanguageChanged);
     }
 
     pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {

crates/project/Cargo.toml 🔗

@@ -19,6 +19,7 @@ test-support = [
 
 [dependencies]
 text = { path = "../text" }
+copilot = { path = "../copilot" }
 client = { path = "../client" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }

crates/project/src/project.rs 🔗

@@ -12,6 +12,7 @@ use anyhow::{anyhow, Context, Result};
 use client::{proto, Client, TypedEnvelope, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
+use copilot::Copilot;
 use futures::{
     channel::mpsc::{self, UnboundedReceiver},
     future::{try_join_all, Shared},
@@ -129,6 +130,7 @@ pub struct Project {
     _maintain_buffer_languages: Task<()>,
     _maintain_workspace_config: Task<()>,
     terminals: Terminals,
+    copilot_enabled: bool,
 }
 
 enum BufferMessage {
@@ -472,6 +474,7 @@ impl Project {
                 terminals: Terminals {
                     local_handles: Vec::new(),
                 },
+                copilot_enabled: Copilot::global(cx).is_some(),
             }
         })
     }
@@ -559,6 +562,7 @@ impl Project {
                 terminals: Terminals {
                     local_handles: Vec::new(),
                 },
+                copilot_enabled: Copilot::global(cx).is_some(),
             };
             for worktree in worktrees {
                 let _ = this.add_worktree(&worktree, cx);
@@ -664,6 +668,15 @@ impl Project {
             self.start_language_server(worktree_id, worktree_path, language, cx);
         }
 
+        if !self.copilot_enabled && Copilot::global(cx).is_some() {
+            self.copilot_enabled = true;
+            for buffer in self.opened_buffers.values() {
+                if let Some(buffer) = buffer.upgrade(cx) {
+                    self.register_buffer_with_copilot(&buffer, cx);
+                }
+            }
+        }
+
         cx.notify();
     }
 
@@ -1616,6 +1629,7 @@ impl Project {
 
         self.detect_language_for_buffer(buffer, cx);
         self.register_buffer_with_language_server(buffer, cx);
+        self.register_buffer_with_copilot(buffer, cx);
         cx.observe_release(buffer, |this, buffer, cx| {
             if let Some(file) = File::from_dyn(buffer.file()) {
                 if file.is_local() {
@@ -1731,6 +1745,16 @@ impl Project {
         });
     }
 
+    fn register_buffer_with_copilot(
+        &self,
+        buffer_handle: &ModelHandle<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(copilot) = Copilot::global(cx) {
+            copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx));
+        }
+    }
+
     async fn send_buffer_messages(
         this: WeakModelHandle<Self>,
         rx: UnboundedReceiver<BufferMessage>,