Notify LSP when Copilot suggestions are accepted/rejected

Antonio Scandurra created

Change summary

crates/copilot/src/copilot.rs | 48 +++++++++++++++++++++++++
crates/copilot/src/request.rs | 28 +++++++++++++++
crates/editor/src/editor.rs   | 68 ++++++++++++++++++++++++------------
3 files changed, 120 insertions(+), 24 deletions(-)

Detailed changes

crates/copilot/src/copilot.rs 🔗

@@ -224,8 +224,9 @@ impl RegisteredBuffer {
     }
 }
 
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug)]
 pub struct Completion {
+    uuid: String,
     pub range: Range<Anchor>,
     pub text: String,
 }
@@ -684,6 +685,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>,

crates/copilot/src/request.rs 🔗

@@ -195,3 +195,31 @@ 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();
     }