Return completion proposals from inline completion providers (#17578)
Updates the inline completion provider to return a completion proposal which is then converted to a completion state. This completion proposal includes more detailed information about which inlays specifically should be rendered. Release Notes: - Added support for fill-in-the-middle style inline completions 
This commit is contained in:
parent
37b2f4b9d3
commit
d315405be1
4 changed files with 166 additions and 38 deletions
|
@ -1,14 +1,14 @@
|
||||||
use crate::{Completion, Copilot};
|
use crate::{Completion, Copilot};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
use editor::{Direction, InlineCompletionProvider};
|
use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{all_language_settings, AllLanguageSettings},
|
language_settings::{all_language_settings, AllLanguageSettings},
|
||||||
Buffer, OffsetRangeExt, ToOffset,
|
Buffer, OffsetRangeExt, ToOffset,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
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);
|
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||||
|
|
||||||
|
@ -237,7 +237,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
cursor_position: language::Anchor,
|
cursor_position: language::Anchor,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> Option<(&'a str, Option<Range<language::Anchor>>)> {
|
) -> Option<CompletionProposal> {
|
||||||
let buffer_id = buffer.entity_id();
|
let buffer_id = buffer.entity_id();
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
let completion = self.active_completion()?;
|
let completion = self.active_completion()?;
|
||||||
|
@ -267,7 +267,14 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||||
if completion_text.trim().is_empty() {
|
if completion_text.trim().is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
@ -414,6 +414,22 @@ impl Default for EditorStyle {
|
||||||
|
|
||||||
type CompletionId = usize;
|
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)]
|
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
|
||||||
struct EditorActionId(usize);
|
struct EditorActionId(usize);
|
||||||
|
|
||||||
|
@ -557,7 +573,7 @@ pub struct Editor {
|
||||||
gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
hovered_link_state: Option<HoveredLinkState>,
|
hovered_link_state: Option<HoveredLinkState>,
|
||||||
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
|
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
|
// enable_inline_completions is a switch that Vim can use to disable
|
||||||
// inline completions based on its mode.
|
// inline completions based on its mode.
|
||||||
enable_inline_completions: bool,
|
enable_inline_completions: bool,
|
||||||
|
@ -5069,7 +5085,7 @@ impl Editor {
|
||||||
_: &AcceptInlineCompletion,
|
_: &AcceptInlineCompletion,
|
||||||
cx: &mut ViewContext<Self>,
|
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;
|
return;
|
||||||
};
|
};
|
||||||
if let Some(provider) = self.inline_completion_provider() {
|
if let Some(provider) = self.inline_completion_provider() {
|
||||||
|
@ -5081,7 +5097,7 @@ impl Editor {
|
||||||
text: completion.text.to_string().into(),
|
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.change_selections(None, cx, |s| s.select_ranges([range]))
|
||||||
}
|
}
|
||||||
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
|
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
|
||||||
|
@ -5095,7 +5111,7 @@ impl Editor {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if self.selections.count() == 1 && self.has_active_inline_completion(cx) {
|
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
|
let mut partial_completion = completion
|
||||||
.text
|
.text
|
||||||
.chars()
|
.chars()
|
||||||
|
@ -5116,7 +5132,7 @@ impl Editor {
|
||||||
text: partial_completion.clone().into(),
|
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.change_selections(None, cx, |s| s.select_ranges([range]))
|
||||||
}
|
}
|
||||||
self.insert_with_autoindent_mode(&partial_completion, None, cx);
|
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 {
|
pub fn has_active_inline_completion(&self, cx: &AppContext) -> bool {
|
||||||
if let Some(completion) = self.active_inline_completion.as_ref() {
|
if let Some(completion) = self.active_inline_completion.as_ref() {
|
||||||
let buffer = self.buffer.read(cx).read(cx);
|
let buffer = self.buffer.read(cx).read(cx);
|
||||||
completion.0.position.is_valid(&buffer)
|
completion.position.is_valid(&buffer)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -5151,14 +5167,15 @@ impl Editor {
|
||||||
fn take_active_inline_completion(
|
fn take_active_inline_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<(Inlay, Option<Range<Anchor>>)> {
|
) -> Option<CompletionState> {
|
||||||
let completion = self.active_inline_completion.take()?;
|
let completion = self.active_inline_completion.take()?;
|
||||||
|
let render_inlay_ids = completion.render_inlay_ids.clone();
|
||||||
self.display_map.update(cx, |map, cx| {
|
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);
|
let buffer = self.buffer.read(cx).read(cx);
|
||||||
|
|
||||||
if completion.0.position.is_valid(&buffer) {
|
if completion.position.is_valid(&buffer) {
|
||||||
Some(completion)
|
Some(completion)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -5179,31 +5196,50 @@ impl Editor {
|
||||||
if let Some((buffer, cursor_buffer_position)) =
|
if let Some((buffer, cursor_buffer_position)) =
|
||||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
|
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)
|
provider.active_completion_text(&buffer, cursor_buffer_position, cx)
|
||||||
{
|
{
|
||||||
let text = Rope::from(text);
|
|
||||||
let mut to_remove = Vec::new();
|
let mut to_remove = Vec::new();
|
||||||
if let Some(completion) = self.active_inline_completion.take() {
|
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 =
|
let to_add = proposal
|
||||||
Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
|
.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();
|
||||||
|
|
||||||
let multibuffer_anchor_range = text_anchor_range.and_then(|range| {
|
self.active_inline_completion = Some(CompletionState {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
position: cursor,
|
||||||
Some(
|
text: proposal.text,
|
||||||
snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
delete_range: proposal.delete_range.and_then(|range| {
|
||||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?,
|
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| {
|
self.display_map
|
||||||
map.splice_inlays(to_remove, vec![completion_inlay], cx)
|
.update(cx, move |map, cx| map.splice_inlays(to_remove, to_add, cx));
|
||||||
});
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,18 @@ use crate::Direction;
|
||||||
use gpui::{AppContext, Model, ModelContext};
|
use gpui::{AppContext, Model, ModelContext};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use std::ops::Range;
|
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 {
|
pub trait InlineCompletionProvider: 'static + Sized {
|
||||||
fn name() -> &'static str;
|
fn name() -> &'static str;
|
||||||
|
@ -32,7 +44,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
cursor_position: language::Anchor,
|
cursor_position: language::Anchor,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> Option<(&'a str, Option<Range<language::Anchor>>)>;
|
) -> Option<CompletionProposal>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait InlineCompletionProviderHandle {
|
pub trait InlineCompletionProviderHandle {
|
||||||
|
@ -63,7 +75,7 @@ pub trait InlineCompletionProviderHandle {
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
cursor_position: language::Anchor,
|
cursor_position: language::Anchor,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> Option<(&'a str, Option<Range<language::Anchor>>)>;
|
) -> Option<CompletionProposal>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> InlineCompletionProviderHandle for Model<T>
|
impl<T> InlineCompletionProviderHandle for Model<T>
|
||||||
|
@ -118,7 +130,7 @@ where
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
cursor_position: language::Anchor,
|
cursor_position: language::Anchor,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> Option<(&'a str, Option<Range<language::Anchor>>)> {
|
) -> Option<CompletionProposal> {
|
||||||
self.read(cx)
|
self.read(cx)
|
||||||
.active_completion_text(buffer, cursor_position, cx)
|
.active_completion_text(buffer, cursor_position, cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
use crate::{Supermaven, SupermavenCompletionStateId};
|
use crate::{Supermaven, SupermavenCompletionStateId};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
use editor::{Direction, InlineCompletionProvider};
|
use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||||
use language::{language_settings::all_language_settings, Anchor, Buffer};
|
use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot};
|
||||||
use std::{ops::Range, path::Path, sync::Arc, time::Duration};
|
use std::{
|
||||||
use text::ToPoint;
|
ops::{AddAssign, Range},
|
||||||
|
path::Path,
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use text::{ToOffset, ToPoint};
|
||||||
|
|
||||||
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
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 {
|
impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||||
fn name() -> &'static str {
|
fn name() -> &'static str {
|
||||||
"supermaven"
|
"supermaven"
|
||||||
|
@ -138,7 +206,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
cursor_position: Anchor,
|
cursor_position: Anchor,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> Option<(&'a str, Option<Range<Anchor>>)> {
|
) -> Option<CompletionProposal> {
|
||||||
let completion_text = self
|
let completion_text = self
|
||||||
.supermaven
|
.supermaven
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -153,7 +221,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||||
let mut point = cursor_position.to_point(&snapshot);
|
let mut point = cursor_position.to_point(&snapshot);
|
||||||
point.column = snapshot.line_len(point.row);
|
point.column = snapshot.line_len(point.row);
|
||||||
let range = cursor_position..snapshot.anchor_after(point);
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue