Truncate line when accepting inline suggestions for Supermaven (#13884)

Kevin Wang , Antonio Scandurra , and Conrad Irwin created

Configures inline completions to delete the remaining text on the given
line. This doesn't affect the github copilot inline completion provider
since it seems to only generate suggestions if the cursor is at the end
of the line but fixes the usability issues related to Supermaven.




https://github.com/user-attachments/assets/1b8bc9a3-4666-4665-a436-96e4beee01bb





Release Notes:

- Fixed https://github.com/zed-industries/zed/issues/13039

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

Cargo.lock                                              |  1 
crates/copilot/src/copilot_completion_provider.rs       |  6 
crates/editor/src/editor.rs                             | 44 ++++++++--
crates/editor/src/inline_completion_provider.rs         |  7 +
crates/supermaven/Cargo.toml                            |  1 
crates/supermaven/src/supermaven_completion_provider.rs | 11 ++
6 files changed, 51 insertions(+), 19 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10538,6 +10538,7 @@ dependencies = [
  "settings",
  "smol",
  "supermaven_api",
+ "text",
  "theme",
  "ui",
  "util",

crates/copilot/src/copilot_completion_provider.rs 🔗

@@ -8,7 +8,7 @@ use language::{
     Buffer, OffsetRangeExt, ToOffset,
 };
 use settings::Settings;
-use std::{path::Path, sync::Arc, time::Duration};
+use std::{ops::Range, path::Path, sync::Arc, time::Duration};
 
 pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
 
@@ -239,7 +239,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
         buffer: &Model<Buffer>,
         cursor_position: language::Anchor,
         cx: &'a AppContext,
-    ) -> Option<&'a str> {
+    ) -> Option<(&'a str, Option<Range<language::Anchor>>)> {
         let buffer_id = buffer.entity_id();
         let buffer = buffer.read(cx);
         let completion = self.active_completion()?;
@@ -269,7 +269,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
             if completion_text.trim().is_empty() {
                 None
             } else {
-                Some(completion_text)
+                Some((completion_text, None))
             }
         } else {
             None

crates/editor/src/editor.rs 🔗

@@ -533,7 +533,7 @@ pub struct Editor {
     gutter_hovered: bool,
     hovered_link_state: Option<HoveredLinkState>,
     inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
-    active_inline_completion: Option<Inlay>,
+    active_inline_completion: Option<(Inlay, Option<Range<Anchor>>)>,
     show_inline_completions: bool,
     inlay_hint_cache: InlayHintCache,
     expanded_hunks: ExpandedHunks,
@@ -4953,7 +4953,7 @@ impl Editor {
         _: &AcceptInlineCompletion,
         cx: &mut ViewContext<Self>,
     ) {
-        let Some(completion) = self.take_active_inline_completion(cx) else {
+        let Some((completion, delete_range)) = self.take_active_inline_completion(cx) else {
             return;
         };
         if let Some(provider) = self.inline_completion_provider() {
@@ -4964,6 +4964,10 @@ impl Editor {
             utf16_range_to_replace: None,
             text: completion.text.to_string().into(),
         });
+
+        if let Some(range) = delete_range {
+            self.change_selections(None, cx, |s| s.select_ranges([range]))
+        }
         self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
         self.refresh_inline_completion(true, cx);
         cx.notify();
@@ -4975,7 +4979,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if self.selections.count() == 1 && self.has_active_inline_completion(cx) {
-            if let Some(completion) = self.take_active_inline_completion(cx) {
+            if let Some((completion, delete_range)) = self.take_active_inline_completion(cx) {
                 let mut partial_completion = completion
                     .text
                     .chars()
@@ -4995,7 +4999,12 @@ impl Editor {
                     utf16_range_to_replace: None,
                     text: partial_completion.clone().into(),
                 });
+
+                if let Some(range) = delete_range {
+                    self.change_selections(None, cx, |s| s.select_ranges([range]))
+                }
                 self.insert_with_autoindent_mode(&partial_completion, None, cx);
+
                 self.refresh_inline_completion(true, cx);
                 cx.notify();
             }
@@ -5017,20 +5026,23 @@ impl Editor {
     pub fn has_active_inline_completion(&self, cx: &AppContext) -> bool {
         if let Some(completion) = self.active_inline_completion.as_ref() {
             let buffer = self.buffer.read(cx).read(cx);
-            completion.position.is_valid(&buffer)
+            completion.0.position.is_valid(&buffer)
         } else {
             false
         }
     }
 
-    fn take_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
+    fn take_active_inline_completion(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<(Inlay, Option<Range<Anchor>>)> {
         let completion = self.active_inline_completion.take()?;
         self.display_map.update(cx, |map, cx| {
-            map.splice_inlays(vec![completion.id], Default::default(), cx);
+            map.splice_inlays(vec![completion.0.id], Default::default(), cx);
         });
         let buffer = self.buffer.read(cx).read(cx);
 
-        if completion.position.is_valid(&buffer) {
+        if completion.0.position.is_valid(&buffer) {
             Some(completion)
         } else {
             None
@@ -5041,6 +5053,8 @@ impl Editor {
         let selection = self.selections.newest_anchor();
         let cursor = selection.head();
 
+        let excerpt_id = cursor.excerpt_id;
+
         if self.context_menu.read().is_none()
             && self.completion_tasks.is_empty()
             && selection.start == selection.end
@@ -5049,18 +5063,28 @@ impl Editor {
                 if let Some((buffer, cursor_buffer_position)) =
                     self.buffer.read(cx).text_anchor_for_position(cursor, cx)
                 {
-                    if let Some(text) =
+                    if let Some((text, text_anchor_range)) =
                         provider.active_completion_text(&buffer, cursor_buffer_position, cx)
                     {
                         let text = Rope::from(text);
                         let mut to_remove = Vec::new();
                         if let Some(completion) = self.active_inline_completion.take() {
-                            to_remove.push(completion.id);
+                            to_remove.push(completion.0.id);
                         }
 
                         let completion_inlay =
                             Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
-                        self.active_inline_completion = Some(completion_inlay.clone());
+
+                        let multibuffer_anchor_range = text_anchor_range.and_then(|range| {
+                            let snapshot = self.buffer.read(cx).snapshot(cx);
+                            Some(
+                                snapshot.anchor_in_excerpt(excerpt_id, range.start)?
+                                    ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?,
+                            )
+                        });
+                        self.active_inline_completion =
+                            Some((completion_inlay.clone(), multibuffer_anchor_range));
+
                         self.display_map.update(cx, move |map, cx| {
                             map.splice_inlays(to_remove, vec![completion_inlay], cx)
                         });

crates/editor/src/inline_completion_provider.rs 🔗

@@ -1,6 +1,7 @@
 use crate::Direction;
 use gpui::{AppContext, Model, ModelContext};
 use language::Buffer;
+use std::ops::Range;
 
 pub trait InlineCompletionProvider: 'static + Sized {
     fn name() -> &'static str;
@@ -31,7 +32,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
         buffer: &Model<Buffer>,
         cursor_position: language::Anchor,
         cx: &'a AppContext,
-    ) -> Option<&'a str>;
+    ) -> Option<(&'a str, Option<Range<language::Anchor>>)>;
 }
 
 pub trait InlineCompletionProviderHandle {
@@ -62,7 +63,7 @@ pub trait InlineCompletionProviderHandle {
         buffer: &Model<Buffer>,
         cursor_position: language::Anchor,
         cx: &'a AppContext,
-    ) -> Option<&'a str>;
+    ) -> Option<(&'a str, Option<Range<language::Anchor>>)>;
 }
 
 impl<T> InlineCompletionProviderHandle for Model<T>
@@ -117,7 +118,7 @@ where
         buffer: &Model<Buffer>,
         cursor_position: language::Anchor,
         cx: &'a AppContext,
-    ) -> Option<&'a str> {
+    ) -> Option<(&'a str, Option<Range<language::Anchor>>)> {
         self.read(cx)
             .active_completion_text(buffer, cursor_position, cx)
     }

crates/supermaven/Cargo.toml 🔗

@@ -27,6 +27,7 @@ serde_json.workspace = true
 settings.workspace = true
 supermaven_api.workspace = true
 smol.workspace = true
+text.workspace = true
 ui.workspace = true
 util.workspace = true
 

crates/supermaven/src/supermaven_completion_provider.rs 🔗

@@ -5,7 +5,8 @@ use editor::{Direction, InlineCompletionProvider};
 use futures::StreamExt as _;
 use gpui::{AppContext, EntityId, Model, ModelContext, Task};
 use language::{language_settings::all_language_settings, Anchor, Buffer};
-use std::{path::Path, sync::Arc, time::Duration};
+use std::{ops::Range, path::Path, sync::Arc, time::Duration};
+use text::ToPoint;
 
 pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
 
@@ -139,7 +140,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
         buffer: &Model<Buffer>,
         cursor_position: Anchor,
         cx: &'a AppContext,
-    ) -> Option<&'a str> {
+    ) -> Option<(&'a str, Option<Range<Anchor>>)> {
         let completion_text = self
             .supermaven
             .read(cx)
@@ -150,7 +151,11 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
         let completion_text = completion_text.trim_end();
 
         if !completion_text.trim().is_empty() {
-            Some(completion_text)
+            let snapshot = buffer.read(cx).snapshot();
+            let mut point = cursor_position.to_point(&snapshot);
+            point.column = snapshot.line_len(point.row);
+            let range = cursor_position..snapshot.anchor_after(point);
+            Some((completion_text, Some(range)))
         } else {
             None
         }