diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 41ba59a0d5..c54fefad6f 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -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, cursor_position: language::Anchor, cx: &'a AppContext, - ) -> Option<(&'a str, Option>)> { + ) -> Option { 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 diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d17e9cfbd..e46b87b0e2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, + // 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>, +} + #[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, inline_completion_provider: Option, - active_inline_completion: Option<(Inlay, Option>)>, + active_inline_completion: Option, // 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, ) { - 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, ) { 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, - ) -> Option<(Inlay, Option>)> { + ) -> Option { 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 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(); - 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(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; } diff --git a/crates/editor/src/inline_completion_provider.rs b/crates/editor/src/inline_completion_provider.rs index b7516419b9..1085a6294e 100644 --- a/crates/editor/src/inline_completion_provider.rs +++ b/crates/editor/src/inline_completion_provider.rs @@ -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, + pub text: Rope, + pub delete_range: Option>, +} pub trait InlineCompletionProvider: 'static + Sized { fn name() -> &'static str; @@ -32,7 +44,7 @@ pub trait InlineCompletionProvider: 'static + Sized { buffer: &Model, cursor_position: language::Anchor, cx: &'a AppContext, - ) -> Option<(&'a str, Option>)>; + ) -> Option; } pub trait InlineCompletionProviderHandle { @@ -63,7 +75,7 @@ pub trait InlineCompletionProviderHandle { buffer: &Model, cursor_position: language::Anchor, cx: &'a AppContext, - ) -> Option<(&'a str, Option>)>; + ) -> Option; } impl InlineCompletionProviderHandle for Model @@ -118,7 +130,7 @@ where buffer: &Model, cursor_position: language::Anchor, cx: &'a AppContext, - ) -> Option<(&'a str, Option>)> { + ) -> Option { self.read(cx) .active_completion_text(buffer, cursor_position, cx) } diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 8612187c61..4119771714 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -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, +) -> CompletionProposal { + let buffer_text = snapshot + .text_for_range(delete_range.clone()) + .collect::() + .chars() + .collect::>(); + + let mut inlays: Vec = Vec::new(); + + let completion = completion_text.chars().collect::>(); + + 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, cursor_position: Anchor, cx: &'a AppContext, - ) -> Option<(&'a str, Option>)> { + ) -> Option { 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 }