Detailed changes
@@ -1,14 +1,14 @@
use crate::{Completion, Copilot};
use anyhow::Result;
use client::telemetry::Telemetry;
-use editor::{Direction, InlineCompletionProvider};
+use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
Buffer, OffsetRangeExt, ToOffset,
};
use settings::Settings;
-use std::{ops::Range, path::Path, sync::Arc, time::Duration};
+use std::{path::Path, sync::Arc, time::Duration};
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
@@ -237,7 +237,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
- ) -> Option<(&'a str, Option<Range<language::Anchor>>)> {
+ ) -> Option<CompletionProposal> {
let buffer_id = buffer.entity_id();
let buffer = buffer.read(cx);
let completion = self.active_completion()?;
@@ -267,7 +267,14 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
if completion_text.trim().is_empty() {
None
} else {
- Some((completion_text, None))
+ Some(CompletionProposal {
+ inlays: vec![InlayProposal::Suggestion(
+ cursor_position.bias_right(buffer),
+ completion_text.into(),
+ )],
+ text: completion_text.into(),
+ delete_range: None,
+ })
}
} else {
None
@@ -414,6 +414,22 @@ impl Default for EditorStyle {
type CompletionId = usize;
+#[derive(Clone, Debug)]
+struct CompletionState {
+ // render_inlay_ids represents the inlay hints that are inserted
+ // for rendering the inline completions. They may be discontinuous
+ // in the event that the completion provider returns some intersection
+ // with the existing content.
+ render_inlay_ids: Vec<InlayId>,
+ // text is the resulting rope that is inserted when the user accepts a completion.
+ text: Rope,
+ // position is the position of the cursor when the completion was triggered.
+ position: multi_buffer::Anchor,
+ // delete_range is the range of text that this completion state covers.
+ // if the completion is accepted, this range should be deleted.
+ delete_range: Option<Range<multi_buffer::Anchor>>,
+}
+
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
struct EditorActionId(usize);
@@ -557,7 +573,7 @@ pub struct Editor {
gutter_hovered: bool,
hovered_link_state: Option<HoveredLinkState>,
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
- active_inline_completion: Option<(Inlay, Option<Range<Anchor>>)>,
+ active_inline_completion: Option<CompletionState>,
// enable_inline_completions is a switch that Vim can use to disable
// inline completions based on its mode.
enable_inline_completions: bool,
@@ -5069,7 +5085,7 @@ impl Editor {
_: &AcceptInlineCompletion,
cx: &mut ViewContext<Self>,
) {
- let Some((completion, delete_range)) = self.take_active_inline_completion(cx) else {
+ let Some(completion) = self.take_active_inline_completion(cx) else {
return;
};
if let Some(provider) = self.inline_completion_provider() {
@@ -5081,7 +5097,7 @@ impl Editor {
text: completion.text.to_string().into(),
});
- if let Some(range) = delete_range {
+ if let Some(range) = completion.delete_range {
self.change_selections(None, cx, |s| s.select_ranges([range]))
}
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
@@ -5095,7 +5111,7 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
if self.selections.count() == 1 && self.has_active_inline_completion(cx) {
- if let Some((completion, delete_range)) = self.take_active_inline_completion(cx) {
+ if let Some(completion) = self.take_active_inline_completion(cx) {
let mut partial_completion = completion
.text
.chars()
@@ -5116,7 +5132,7 @@ impl Editor {
text: partial_completion.clone().into(),
});
- if let Some(range) = delete_range {
+ if let Some(range) = completion.delete_range {
self.change_selections(None, cx, |s| s.select_ranges([range]))
}
self.insert_with_autoindent_mode(&partial_completion, None, cx);
@@ -5142,7 +5158,7 @@ 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.0.position.is_valid(&buffer)
+ completion.position.is_valid(&buffer)
} else {
false
}
@@ -5151,14 +5167,15 @@ impl Editor {
fn take_active_inline_completion(
&mut self,
cx: &mut ViewContext<Self>,
- ) -> Option<(Inlay, Option<Range<Anchor>>)> {
+ ) -> Option<CompletionState> {
let completion = self.active_inline_completion.take()?;
+ let render_inlay_ids = completion.render_inlay_ids.clone();
self.display_map.update(cx, |map, cx| {
- map.splice_inlays(vec![completion.0.id], Default::default(), cx);
+ map.splice_inlays(render_inlay_ids, Default::default(), cx);
});
let buffer = self.buffer.read(cx).read(cx);
- if completion.0.position.is_valid(&buffer) {
+ if completion.position.is_valid(&buffer) {
Some(completion)
} else {
None
@@ -5179,31 +5196,50 @@ impl Editor {
if let Some((buffer, cursor_buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
{
- if let Some((text, text_anchor_range)) =
+ if let Some(proposal) =
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.0.id);
+ to_remove.extend(completion.render_inlay_ids.iter());
}
- let completion_inlay =
- Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
-
- 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)?,
- )
+ let to_add = proposal
+ .inlays
+ .iter()
+ .filter_map(|inlay| {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let id = post_inc(&mut self.next_inlay_id);
+ match inlay {
+ InlayProposal::Hint(position, hint) => {
+ let position =
+ snapshot.anchor_in_excerpt(excerpt_id, *position)?;
+ Some(Inlay::hint(id, position, hint))
+ }
+ InlayProposal::Suggestion(position, text) => {
+ let position =
+ snapshot.anchor_in_excerpt(excerpt_id, *position)?;
+ Some(Inlay::suggestion(id, position, text.clone()))
+ }
+ }
+ })
+ .collect_vec();
+
+ self.active_inline_completion = Some(CompletionState {
+ position: cursor,
+ text: proposal.text,
+ delete_range: proposal.delete_range.and_then(|range| {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let start = snapshot.anchor_in_excerpt(excerpt_id, range.start);
+ let end = snapshot.anchor_in_excerpt(excerpt_id, range.end);
+ Some(start?..end?)
+ }),
+ render_inlay_ids: to_add.iter().map(|i| i.id).collect(),
});
- 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)
- });
+ self.display_map
+ .update(cx, move |map, cx| map.splice_inlays(to_remove, to_add, cx));
+
cx.notify();
return;
}
@@ -2,6 +2,18 @@ use crate::Direction;
use gpui::{AppContext, Model, ModelContext};
use language::Buffer;
use std::ops::Range;
+use text::{Anchor, Rope};
+
+pub enum InlayProposal {
+ Hint(Anchor, project::InlayHint),
+ Suggestion(Anchor, Rope),
+}
+
+pub struct CompletionProposal {
+ pub inlays: Vec<InlayProposal>,
+ pub text: Rope,
+ pub delete_range: Option<Range<Anchor>>,
+}
pub trait InlineCompletionProvider: 'static + Sized {
fn name() -> &'static str;
@@ -32,7 +44,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
- ) -> Option<(&'a str, Option<Range<language::Anchor>>)>;
+ ) -> Option<CompletionProposal>;
}
pub trait InlineCompletionProviderHandle {
@@ -63,7 +75,7 @@ pub trait InlineCompletionProviderHandle {
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
- ) -> Option<(&'a str, Option<Range<language::Anchor>>)>;
+ ) -> Option<CompletionProposal>;
}
impl<T> InlineCompletionProviderHandle for Model<T>
@@ -118,7 +130,7 @@ where
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
- ) -> Option<(&'a str, Option<Range<language::Anchor>>)> {
+ ) -> Option<CompletionProposal> {
self.read(cx)
.active_completion_text(buffer, cursor_position, cx)
}
@@ -1,12 +1,17 @@
use crate::{Supermaven, SupermavenCompletionStateId};
use anyhow::Result;
use client::telemetry::Telemetry;
-use editor::{Direction, InlineCompletionProvider};
+use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
use futures::StreamExt as _;
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
-use language::{language_settings::all_language_settings, Anchor, Buffer};
-use std::{ops::Range, path::Path, sync::Arc, time::Duration};
-use text::ToPoint;
+use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot};
+use std::{
+ ops::{AddAssign, Range},
+ path::Path,
+ sync::Arc,
+ time::Duration,
+};
+use text::{ToOffset, ToPoint};
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
@@ -37,6 +42,69 @@ impl SupermavenCompletionProvider {
}
}
+// Computes the completion state from the difference between the completion text.
+// this is defined by greedily matching the buffer text against the completion text, with any leftover buffer placed at the end.
+// for example, given the completion text "moo cows are cool" and the buffer text "cowsre pool", the completion state would be
+// the inlays "moo ", " a", and "cool" which will render as "[moo ]cows[ a]re [cool]pool" in the editor.
+fn completion_state_from_diff(
+ snapshot: BufferSnapshot,
+ completion_text: &str,
+ position: Anchor,
+ delete_range: Range<Anchor>,
+) -> CompletionProposal {
+ let buffer_text = snapshot
+ .text_for_range(delete_range.clone())
+ .collect::<String>()
+ .chars()
+ .collect::<Vec<char>>();
+
+ let mut inlays: Vec<InlayProposal> = Vec::new();
+
+ let completion = completion_text.chars().collect::<Vec<char>>();
+
+ let mut offset = position.to_offset(&snapshot);
+
+ let mut i = 0;
+ let mut j = 0;
+ while i < completion.len() && j < buffer_text.len() {
+ // find the next instance of the buffer text in the completion text.
+ let k = completion[i..].iter().position(|c| *c == buffer_text[j]);
+ match k {
+ Some(k) => {
+ if k != 0 {
+ // the range from the current position to item is an inlay.
+ inlays.push(InlayProposal::Suggestion(
+ snapshot.anchor_after(offset),
+ completion_text[i..i + k].into(),
+ ));
+ offset.add_assign(j);
+ }
+ i += k + 1;
+ j += 1;
+ }
+ None => {
+ // there are no more matching completions, so drop the remaining
+ // completion text as an inlay.
+ break;
+ }
+ }
+ }
+
+ if j == buffer_text.len() && i < completion.len() {
+ // there is leftover completion text, so drop it as an inlay.
+ inlays.push(InlayProposal::Suggestion(
+ snapshot.anchor_after(offset),
+ completion_text[i..completion_text.len()].into(),
+ ));
+ }
+
+ CompletionProposal {
+ inlays,
+ text: completion_text.into(),
+ delete_range: Some(delete_range),
+ }
+}
+
impl InlineCompletionProvider for SupermavenCompletionProvider {
fn name() -> &'static str {
"supermaven"
@@ -138,7 +206,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
buffer: &Model<Buffer>,
cursor_position: Anchor,
cx: &'a AppContext,
- ) -> Option<(&'a str, Option<Range<Anchor>>)> {
+ ) -> Option<CompletionProposal> {
let completion_text = self
.supermaven
.read(cx)
@@ -153,7 +221,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
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)))
+ Some(completion_state_from_diff(
+ snapshot,
+ completion_text,
+ cursor_position,
+ range,
+ ))
} else {
None
}