Compare commits

...

3 Commits

Author SHA1 Message Date
Michael Sloan
0529a4f125 Use TextEdit instead of tuple + renames in edit+highlights struct 2025-01-27 23:03:00 -07:00
Michael Sloan
92d16eaa5d More complete / efficient fix of highlights in edit preview 2025-01-27 18:05:10 -07:00
Michael Sloan
949562fed9 Fix highlights in edit predictions preview
The approach here is to take the highlights for unchanged and deleted regions from the current snapshot, and take the highlights for the inserted regions from the `EditPreview` which was pre-computed when receiving the completion.

This also changes the edit range to use `anchor_before(start)..anchor_after(end)` instead of `anchor_after(start)..anchor_before(end)`. The motivation for this is that the edit range for insertions was getting inverted when resolved within the edited snapshot.
2025-01-27 00:51:42 -07:00
11 changed files with 499 additions and 303 deletions

View File

@@ -4,7 +4,7 @@ use gpui::{App, Context, Entity, EntityId, Task};
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
Buffer, OffsetRangeExt, ToOffset,
Buffer, OffsetRangeExt, PlainTextEdit, ToOffset,
};
use settings::Settings;
use std::{path::Path, time::Duration};
@@ -255,8 +255,11 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
} else {
let position = cursor_position.bias_right(buffer);
Some(InlineCompletion {
edits: vec![(position..position, completion_text.into())],
edit_preview: None,
edits: vec![PlainTextEdit {
old_range: position..position,
new_text: completion_text.into(),
}
.into()],
})
}
} else {

View File

@@ -96,9 +96,9 @@ use itertools::Itertools;
use language::{
language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize,
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId,
TreeSitterOptions,
CursorShape, Diagnostic, Documentation, HighlightedEdits, IndentKind, IndentSize, Language,
OffsetRangeExt, Point, Selection, SelectionGoal, TextEdit, TextEditWithNewHighlights,
TextObject, TransactionId, TreeSitterOptions,
};
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
@@ -499,8 +499,7 @@ pub(crate) enum EditDisplayMode {
enum InlineCompletion {
Edit {
edits: Vec<(Range<Anchor>, String)>,
edit_preview: Option<EditPreview>,
edits: Vec<TextEditWithNewHighlights<Anchor>>,
display_mode: EditDisplayMode,
snapshot: BufferSnapshot,
},
@@ -4854,10 +4853,16 @@ impl Editor {
}
let snapshot = self.buffer.read(cx).snapshot(cx);
let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot);
let last_edit_end = edits.last().unwrap().old_range().end.bias_right(&snapshot);
self.buffer.update(cx, |buffer, cx| {
buffer.edit(edits.iter().cloned(), None, cx)
buffer.edit(
edits
.iter()
.map(|edit| (edit.old_range().clone(), edit.new_text().clone())),
None,
cx,
)
});
self.change_selections(None, window, cx, |s| {
@@ -4900,10 +4905,10 @@ impl Editor {
// Find an insertion that starts at the cursor position.
let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor_offset = self.selections.newest::<usize>(cx).head();
let insertion = edits.iter().find_map(|(range, text)| {
let range = range.to_offset(&snapshot);
let insertion = edits.iter().find_map(|edit| {
let range = edit.old_range().to_offset(&snapshot);
if range.is_empty() && range.start == cursor_offset {
Some(text)
Some(edit.new_text())
} else {
None
}
@@ -5039,21 +5044,19 @@ impl Editor {
let edits = inline_completion
.edits
.into_iter()
.flat_map(|(range, new_text)| {
let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?;
let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?;
Some((start..end, new_text))
.flat_map(|edit| {
edit.maybe_map_position(|anchor| multibuffer.anchor_in_excerpt(excerpt_id, anchor))
})
.collect::<Vec<_>>();
if edits.is_empty() {
return None;
}
let first_edit_start = edits.first().unwrap().0.start;
let first_edit_start = edits.first().unwrap().old_range().start;
let first_edit_start_point = first_edit_start.to_point(&multibuffer);
let edit_start_row = first_edit_start_point.row.saturating_sub(2);
let last_edit_end = edits.last().unwrap().0.end;
let last_edit_end = edits.last().unwrap().old_range().end;
let last_edit_end_point = last_edit_end.to_point(&multibuffer);
let edit_end_row = cmp::min(multibuffer.max_point().row, last_edit_end_point.row + 2);
@@ -5070,14 +5073,14 @@ impl Editor {
} else {
if edits
.iter()
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
.all(|edit| edit.old_range().to_offset(&multibuffer).is_empty())
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
for edit in &edits {
let inlay = Inlay::inline_completion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
edit.old_range().start,
edit.new_text().as_str(),
);
inlay_ids.push(inlay.id);
inlays.push(inlay);
@@ -5087,7 +5090,7 @@ impl Editor {
} else {
let background_color = cx.theme().status().deleted_background;
self.highlight_text::<InlineCompletionHighlight>(
edits.iter().map(|(range, _)| range.clone()).collect(),
edits.iter().map(|edit| edit.old_range().clone()).collect(),
HighlightStyle {
background_color: Some(background_color),
..Default::default()
@@ -5101,7 +5104,7 @@ impl Editor {
let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) {
if provider.show_tab_accept_marker()
&& first_edit_start_point.row == last_edit_end_point.row
&& !edits.iter().any(|(_, edit)| edit.contains('\n'))
&& !edits.iter().any(|edit| edit.new_text().contains('\n'))
{
EditDisplayMode::TabAccept
} else {
@@ -5115,7 +5118,6 @@ impl Editor {
InlineCompletion::Edit {
edits,
edit_preview: inline_completion.edit_preview,
display_mode,
snapshot,
}
@@ -5162,14 +5164,9 @@ impl Editor {
let text = match &self.active_inline_completion.as_ref()?.completion {
InlineCompletion::Edit {
edits,
edit_preview,
display_mode: _,
snapshot,
} => edit_preview
.as_ref()
.and_then(|edit_preview| {
inline_completion_edit_text(&snapshot, &edits, edit_preview, true, cx)
})
} => inline_completion_edit_text(&snapshot, &edits, true, cx)
.map(InlineCompletionText::Edit),
InlineCompletion::Move(target) => {
let target_point =
@@ -15822,22 +15819,21 @@ pub fn diagnostic_block_renderer(
fn inline_completion_edit_text(
current_snapshot: &BufferSnapshot,
edits: &[(Range<Anchor>, String)],
edit_preview: &EditPreview,
edits: &[TextEditWithNewHighlights<Anchor>],
include_deletions: bool,
cx: &App,
) -> Option<HighlightedEdits> {
let edits = edits
.iter()
.map(|(anchor, text)| {
(
anchor.start.text_anchor..anchor.end.text_anchor,
text.clone(),
)
})
.map(|edit| edit.clone().map_position(|anchor| anchor.text_anchor))
.collect::<Vec<_>>();
Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx))
Some(HighlightedEdits::highlight(
current_snapshot,
&edits,
include_deletions,
cx,
))
}
pub fn highlight_diagnostic_message(
@@ -16096,15 +16092,15 @@ impl Global for KillRing {}
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
fn all_edits_insertions_or_deletions(
edits: &Vec<(Range<Anchor>, String)>,
edits: &Vec<TextEditWithNewHighlights<Anchor>>,
snapshot: &MultiBufferSnapshot,
) -> bool {
let mut all_insertions = true;
let mut all_deletions = true;
for (range, new_text) in edits.iter() {
let range_is_empty = range.to_offset(&snapshot).is_empty();
let text_is_empty = new_text.is_empty();
for edit in edits.iter() {
let range_is_empty = edit.old_range().to_offset(&snapshot).is_empty();
let text_is_empty = edit.new_text().is_empty();
if range_is_empty != text_is_empty {
if range_is_empty {

View File

@@ -20,7 +20,7 @@ use language::{
BracketPairConfig,
Capability::ReadWrite,
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
Override, ParsedMarkdown, Point,
Override, ParsedMarkdown, PlainTextEdit, Point,
};
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::IndentGuide;
@@ -15421,39 +15421,45 @@ async fn assert_highlighted_edits(
})
.unwrap();
let edits = edits
let text_anchor_edits = edits
.into_iter()
.map(|(range, edit)| {
(
snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
edit,
)
.map(|(range, insertion)| PlainTextEdit {
old_range: snapshot.anchor_after(range.start).text_anchor
..snapshot.anchor_before(range.end).text_anchor,
new_text: insertion,
})
.collect::<Vec<_>>();
let text_anchor_edits = edits
.clone()
.into_iter()
.map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
.collect::<Vec<_>>();
let edit_preview = window
.update(cx, |_, _window, cx| {
let edits = cx
.update(|_window, cx| {
buffer
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.preview_edits(text_anchor_edits.into(), cx)
.highlight_edit_insertions(text_anchor_edits, cx.background_executor())
})
.unwrap()
.await;
let edits = cx.update(|_window, cx| {
let multibuffer_snapshot = buffer.read(cx).snapshot(cx);
let excerpt_id = multibuffer_snapshot.excerpts().next().unwrap().0;
edits
.into_iter()
.map(|edit| {
edit.map_position(|anchor| {
multibuffer_snapshot
.anchor_in_excerpt(excerpt_id, anchor)
.unwrap()
})
})
.collect::<Vec<_>>()
});
cx.update(|_window, cx| {
let highlighted_edits = inline_completion_edit_text(
&snapshot.as_singleton().unwrap().2,
&edits,
&edit_preview,
include_deletions,
cx,
)

View File

@@ -44,7 +44,7 @@ use language::{
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
ShowWhitespaceSetting,
},
ChunkRendererContext,
ChunkRendererContext, TextEdit,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
@@ -3432,7 +3432,6 @@ impl EditorElement {
}
InlineCompletion::Edit {
edits,
edit_preview,
display_mode,
snapshot,
} => {
@@ -3443,13 +3442,13 @@ impl EditorElement {
let edit_start = edits
.first()
.unwrap()
.0
.old_range()
.start
.to_display_point(editor_snapshot);
let edit_end = edits
.last()
.unwrap()
.0
.old_range()
.end
.to_display_point(editor_snapshot);
@@ -3461,7 +3460,7 @@ impl EditorElement {
match display_mode {
EditDisplayMode::TabAccept => {
let range = &edits.first()?.0;
let range = &edits.first()?.old_range();
let target_display_point = range.end.to_display_point(editor_snapshot);
let target_line_end = DisplayPoint::new(
@@ -3487,9 +3486,8 @@ impl EditorElement {
EditDisplayMode::DiffPopover => {}
}
let highlighted_edits = edit_preview.as_ref().and_then(|edit_preview| {
crate::inline_completion_edit_text(&snapshot, edits, edit_preview, false, cx)
})?;
let highlighted_edits =
crate::inline_completion_edit_text(&snapshot, edits, false, cx)?;
let line_count = highlighted_edits.text.lines().count() + 1;

View File

@@ -1,7 +1,7 @@
use gpui::{prelude::*, Entity};
use indoc::indoc;
use inline_completion::InlineCompletionProvider;
use language::{Language, LanguageConfig};
use language::{Language, LanguageConfig, PlainTextEdit, TextEdit, TextEditWithNewHighlights};
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::{num::NonZeroU32, ops::Range, sync::Arc};
use text::{Point, ToOffset};
@@ -24,7 +24,7 @@ async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) {
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), "-273.15");
assert_eq!(edits[0].new_text().as_str(), "-273.15");
});
accept_completion(&mut cx);
@@ -46,7 +46,7 @@ async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) {
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), "3.14159");
assert_eq!(edits[0].new_text().as_str(), "3.14159");
});
accept_completion(&mut cx);
@@ -158,7 +158,7 @@ async fn test_indentation(cx: &mut gpui::TestAppContext) {
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), " const function()");
assert_eq!(edits[0].new_text().as_str(), " const function()");
});
// When the cursor is before the suggested indentation level, accepting a
@@ -278,7 +278,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext
fn assert_editor_active_edit_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
assert: impl FnOnce(MultiBufferSnapshot, &Vec<TextEditWithNewHighlights<Anchor>>),
) {
cx.editor(|editor, _, cx| {
let completion_state = editor
@@ -324,16 +324,18 @@ fn propose_edits<T: ToOffset>(
cx: &mut EditorTestContext,
) {
let snapshot = cx.buffer_snapshot();
let edits = edits.into_iter().map(|(range, text)| {
let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
(range, text.into())
let edits = edits.into_iter().map(|edit| {
PlainTextEdit {
old_range: snapshot.anchor_after(edit.0.start)..snapshot.anchor_before(edit.0.end),
new_text: edit.1.to_string(),
}
.into()
});
cx.update(|_, cx| {
provider.update(cx, |provider, _| {
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
edits: edits.collect(),
edit_preview: None,
}))
})
});

View File

@@ -1,6 +1,5 @@
use gpui::{App, Context, Entity};
use language::Buffer;
use std::ops::Range;
use language::{Buffer, TextEditWithNewHighlights};
// TODO: Find a better home for `Direction`.
//
@@ -14,8 +13,7 @@ pub enum Direction {
#[derive(Clone)]
pub struct InlineCompletion {
pub edits: Vec<(Range<language::Anchor>, String)>,
pub edit_preview: Option<language::EditPreview>,
pub edits: Vec<TextEditWithNewHighlights<language::Anchor>>,
}
pub trait InlineCompletionProvider: 'static + Sized {

View File

@@ -25,8 +25,8 @@ use collections::HashMap;
use fs::MTime;
use futures::channel::oneshot;
use gpui::{
AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels,
SharedString, Task, TaskLabel, Window,
AnyElement, App, AppContext as _, BackgroundExecutor, Context, Entity, EventEmitter,
HighlightStyle, Pixels, SharedString, Task, TaskLabel, Window,
};
use lsp::LanguageServerId;
use parking_lot::Mutex;
@@ -588,10 +588,128 @@ pub struct Runnable {
pub buffer: BufferId,
}
#[derive(Clone)]
pub struct EditPreview {
applied_edits_snapshot: text::BufferSnapshot,
syntax_snapshot: SyntaxSnapshot,
pub trait TextEdit<T: Clone> {
fn old_range(&self) -> &Range<T>;
fn mut_old_range(&mut self) -> &mut Range<T>;
fn new_text(&self) -> &String;
fn with_prefix_dropped(&self, prefix_length: usize) -> Self;
// TODO: replace uses of tuples with PlainTextEdit.
fn to_tuple(&self) -> (Range<T>, String) {
(self.old_range().clone(), self.new_text().clone())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlainTextEdit<T> {
pub old_range: Range<T>,
pub new_text: String,
}
impl<T: Clone> TextEdit<T> for PlainTextEdit<T> {
fn old_range(&self) -> &Range<T> {
&self.old_range
}
fn mut_old_range(&mut self) -> &mut Range<T> {
&mut self.old_range
}
fn new_text(&self) -> &String {
&self.new_text
}
fn with_prefix_dropped(&self, prefix_length: usize) -> Self {
PlainTextEdit {
old_range: self.old_range.clone(),
new_text: self.new_text[prefix_length..].to_string(),
}
}
}
impl<T> PlainTextEdit<T> {
pub fn map_position<O>(self, f: impl Fn(T) -> O) -> PlainTextEdit<O> {
PlainTextEdit {
old_range: f(self.old_range.start)..f(self.old_range.end),
new_text: self.new_text.clone(),
}
}
pub fn maybe_map_position<O>(self, f: impl Fn(T) -> Option<O>) -> Option<PlainTextEdit<O>> {
Some(PlainTextEdit {
old_range: f(self.old_range.start)?..f(self.old_range.end)?,
new_text: self.new_text.clone(),
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TextEditWithNewHighlights<T> {
pub text_edit: PlainTextEdit<T>,
pub new_highlights: Vec<(Range<usize>, HighlightId)>,
}
impl<T: Clone> TextEdit<T> for TextEditWithNewHighlights<T> {
fn old_range(&self) -> &Range<T> {
&self.text_edit.old_range
}
fn mut_old_range(&mut self) -> &mut Range<T> {
&mut self.text_edit.old_range
}
fn new_text(&self) -> &String {
&self.text_edit.new_text
}
fn with_prefix_dropped(&self, prefix_length: usize) -> Self {
TextEditWithNewHighlights {
text_edit: self.text_edit.with_prefix_dropped(prefix_length),
new_highlights: self.new_highlights.clone(),
}
}
}
impl<T> TextEditWithNewHighlights<T> {
pub fn map_edit<O>(
self,
f: impl FnOnce(PlainTextEdit<T>) -> PlainTextEdit<O>,
) -> TextEditWithNewHighlights<O> {
TextEditWithNewHighlights {
text_edit: f(self.text_edit),
new_highlights: self.new_highlights,
}
}
pub fn maybe_map_edit<O>(
self,
f: impl FnOnce(PlainTextEdit<T>) -> Option<PlainTextEdit<O>>,
) -> Option<TextEditWithNewHighlights<O>> {
Some(TextEditWithNewHighlights {
text_edit: f(self.text_edit)?,
new_highlights: self.new_highlights,
})
}
pub fn map_position<O>(self, f: impl Fn(T) -> O) -> TextEditWithNewHighlights<O> {
self.map_edit(|edit| edit.map_position(f))
}
pub fn maybe_map_position<O>(
self,
f: impl Fn(T) -> Option<O>,
) -> Option<TextEditWithNewHighlights<O>> {
self.maybe_map_edit(|edit| edit.maybe_map_position(f))
}
}
impl<T> From<PlainTextEdit<T>> for TextEditWithNewHighlights<T> {
fn from(text_edit: PlainTextEdit<T>) -> Self {
TextEditWithNewHighlights {
text_edit,
new_highlights: vec![],
}
}
}
#[derive(Default, Clone, Debug)]
@@ -600,76 +718,102 @@ pub struct HighlightedEdits {
pub highlights: Vec<(Range<usize>, HighlightStyle)>,
}
impl EditPreview {
pub fn highlight_edits(
&self,
impl HighlightedEdits {
pub fn highlight(
current_snapshot: &BufferSnapshot,
edits: &[(Range<Anchor>, String)],
edits: &[TextEditWithNewHighlights<Anchor>],
include_deletions: bool,
cx: &App,
) -> HighlightedEdits {
let mut text = String::new();
let mut highlights = Vec::new();
let Some(range) = self.compute_visible_range(edits, current_snapshot) else {
let Some(visible_range) = Self::compute_visible_range(edits, current_snapshot) else {
return HighlightedEdits::default();
};
let mut offset = range.start;
let mut delta = 0isize;
let mut start_of_unchanged = visible_range.start;
let syntax_theme = cx.theme().syntax();
let status_colors = cx.theme().status();
let deleted_highlight = HighlightStyle {
background_color: Some(status_colors.deleted_background),
..Default::default()
};
let created_highlight = HighlightStyle {
background_color: Some(status_colors.created_background),
..Default::default()
};
for (range, edit_text) in edits {
let edit_range = range.to_offset(current_snapshot);
let new_edit_start = (edit_range.start as isize + delta) as usize;
let new_edit_range = new_edit_start..new_edit_start + edit_text.len();
for edit in edits {
let edit_range = edit.text_edit.old_range.to_offset(current_snapshot);
let unchanged_range = start_of_unchanged..edit_range.start;
start_of_unchanged = edit_range.end;
let prev_range = offset..new_edit_start;
if !prev_range.is_empty() {
let start = text.len();
self.highlight_text(prev_range, &mut text, &mut highlights, None, cx);
offset += text.len() - start;
if !unchanged_range.is_empty() {
Self::add_highlighted_text_from(
unchanged_range,
&current_snapshot,
None,
syntax_theme,
&mut text,
&mut highlights,
);
}
if include_deletions && !edit_range.is_empty() {
let start = text.len();
text.extend(current_snapshot.text_for_range(edit_range.clone()));
let end = text.len();
highlights.push((
start..end,
HighlightStyle {
background_color: Some(status_colors.deleted_background),
..Default::default()
},
));
}
if !edit_text.is_empty() {
self.highlight_text(
new_edit_range,
Self::add_highlighted_text_from(
edit_range,
&current_snapshot,
Some(deleted_highlight),
syntax_theme,
&mut text,
&mut highlights,
Some(HighlightStyle {
background_color: Some(status_colors.created_background),
..Default::default()
}),
cx,
);
offset += edit_text.len();
}
delta += edit_text.len() as isize - edit_range.len() as isize;
let new_text = &edit.text_edit.new_text;
if !new_text.is_empty() {
let delta = text.len();
text.push_str(&new_text);
let mut offset = delta;
for (highlight_range, highlight_id) in edit.new_highlights.iter() {
let start = highlight_range.start + delta;
let end = highlight_range.end + delta;
let unhighlighted_range = offset..start;
if !unhighlighted_range.is_empty() {
highlights.push((unhighlighted_range, created_highlight));
}
highlights.push((
start..end,
highlight_id.style(syntax_theme).map_or(
created_highlight,
|mut highlight_style| {
highlight_style.highlight(created_highlight);
highlight_style
},
),
));
offset = end;
}
let unhighlighted_range = offset..text.len();
if !unhighlighted_range.is_empty() {
highlights.push((unhighlighted_range, created_highlight));
}
}
}
self.highlight_text(
offset..(range.end as isize + delta) as usize,
&mut text,
&mut highlights,
None,
cx,
);
let unchanged_range = start_of_unchanged..visible_range.end;
if !unchanged_range.is_empty() {
Self::add_highlighted_text_from(
unchanged_range,
&current_snapshot,
None,
syntax_theme,
&mut text,
&mut highlights,
);
}
HighlightedEdits {
text: text.into(),
@@ -677,22 +821,22 @@ impl EditPreview {
}
}
fn highlight_text(
&self,
fn add_highlighted_text_from(
range: Range<usize>,
snapshot: &BufferSnapshot,
override_style: Option<HighlightStyle>,
syntax_theme: &SyntaxTheme,
text: &mut String,
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
override_style: Option<HighlightStyle>,
cx: &App,
) {
for chunk in self.highlighted_chunks(range) {
for chunk in snapshot.chunks(range, true) {
let start = text.len();
text.push_str(chunk.text);
let end = text.len();
if let Some(mut highlight_style) = chunk
.syntax_highlight_id
.and_then(|id| id.style(cx.theme().syntax()))
.and_then(|id| id.style(syntax_theme))
{
if let Some(override_style) = override_style {
highlight_style.highlight(override_style);
@@ -704,38 +848,17 @@ impl EditPreview {
}
}
fn highlighted_chunks(&self, range: Range<usize>) -> BufferChunks {
let captures =
self.syntax_snapshot
.captures(range.clone(), &self.applied_edits_snapshot, |grammar| {
grammar.highlights_query.as_ref()
});
let highlight_maps = captures
.grammars()
.iter()
.map(|grammar| grammar.highlight_map())
.collect();
BufferChunks::new(
self.applied_edits_snapshot.as_rope(),
range,
Some((captures, highlight_maps)),
false,
None,
)
}
/// Offset range in `current_snapshot` that contains all edits, expanded to start at the
/// beginning of the first line and end at the end of the last line.
fn compute_visible_range(
&self,
edits: &[(Range<Anchor>, String)],
edits: &[TextEditWithNewHighlights<Anchor>],
snapshot: &BufferSnapshot,
) -> Option<Range<usize>> {
let (first, _) = edits.first()?;
let (last, _) = edits.last()?;
let first_edit = edits.first()?;
let last_edit = edits.last()?;
let start = first.start.to_point(snapshot);
let end = last.end.to_point(snapshot);
let start = first_edit.old_range().start.to_point(snapshot);
let end = last_edit.old_range().end.to_point(snapshot);
// Ensure that the first line of the first edit and the last line of the last edit are always fully visible
let range = Point::new(start.row, 0)..Point::new(end.row, snapshot.line_len(end.row));
@@ -996,30 +1119,66 @@ impl Buffer {
})
}
pub fn preview_edits(
pub fn highlight_edit_insertions(
&self,
edits: Arc<[(Range<Anchor>, String)]>,
cx: &App,
) -> Task<EditPreview> {
edits: Vec<PlainTextEdit<Anchor>>,
background_executor: &BackgroundExecutor,
) -> Task<Vec<TextEditWithNewHighlights<Anchor>>> {
let registry = self.language_registry();
let language = self.language().cloned();
let mut branch_buffer = self.text.branch();
let mut syntax_snapshot = self.syntax_map.lock().snapshot();
cx.background_executor().spawn(async move {
if !edits.is_empty() {
branch_buffer.edit(edits.iter().cloned());
let snapshot = branch_buffer.snapshot();
syntax_snapshot.interpolate(&snapshot);
background_executor.spawn(async move {
if edits.is_empty() {
return vec![];
}
branch_buffer.edit(edits.iter().map(|edit| edit.to_tuple()));
let snapshot = branch_buffer.snapshot();
syntax_snapshot.interpolate(&snapshot);
if let Some(language) = language {
syntax_snapshot.reparse(&snapshot, registry, language);
}
if let Some(language) = language {
syntax_snapshot.reparse(&snapshot, registry, language);
}
}
EditPreview {
applied_edits_snapshot: branch_buffer.snapshot(),
syntax_snapshot,
}
edits
.iter()
.map(|text_edit| {
let range = text_edit.old_range().to_offset(&snapshot);
let captures = syntax_snapshot.captures(range.clone(), &snapshot, |grammar| {
grammar.highlights_query.as_ref()
});
let highlight_maps = captures
.grammars()
.iter()
.map(|grammar| grammar.highlight_map())
.collect();
let chunks = BufferChunks::new(
snapshot.as_rope(),
range,
Some((captures, highlight_maps)),
false,
None,
);
let mut new_highlights = Vec::new();
let mut offset = 0;
for chunk in chunks {
let start = offset;
let end = start + chunk.text.len();
offset = end;
if let Some(highlight_id) = chunk.syntax_highlight_id {
new_highlights.push((start..end, highlight_id));
}
}
TextEditWithNewHighlights {
text_edit: text_edit.clone(),
new_highlights,
}
})
.collect::<Vec<_>>()
})
}

View File

@@ -2674,20 +2674,18 @@ async fn test_preview_edits(cx: &mut TestAppContext) {
let edits = buffer.read_with(cx, |buffer, _| {
edits
.into_iter()
.map(|(range, text)| {
(
buffer.anchor_before(range.start)..buffer.anchor_after(range.end),
text.to_string(),
)
.map(|(range, text)| PlainTextEdit {
old_range: buffer.anchor_before(range.start)..buffer.anchor_after(range.end),
new_text: text.to_string(),
})
.collect::<Vec<_>>()
});
let edit_preview = buffer
let edits = buffer
.read_with(cx, |buffer, cx| {
buffer.preview_edits(edits.clone().into(), cx)
buffer.highlight_edit_insertions(edits, cx.background_executor())
})
.await;
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, true, cx))
cx.read(|cx| HighlightedEdits::highlight(&buffer.read(cx).snapshot(), &edits, true, cx))
}
}
@@ -2712,12 +2710,20 @@ async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
let edits = construct_edits(&buffer, [(Point::new(1, 4)..Point::new(1, 4), "first")], cx);
let edit_preview = buffer
.read_with(cx, |buffer, cx| buffer.preview_edits(edits.clone(), cx))
let edits_with_new_highlights = buffer
.read_with(cx, |buffer, cx| {
buffer.highlight_edit_insertions(edits, cx.background_executor())
})
.await;
let highlighted_edits =
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, false, cx));
let highlighted_edits = cx.read(|cx| {
HighlightedEdits::highlight(
&buffer.read(cx).snapshot(),
&edits_with_new_highlights,
false,
cx,
)
});
let created_background = cx.read(|cx| cx.theme().status().created_background);
@@ -2732,13 +2738,24 @@ async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
let edits = construct_edits(&buffer, [(Point::new(1, 4)..Point::new(1, 4), "f")], cx);
cx.update(|cx| {
buffer.update(cx, |buffer, cx| {
buffer.edit(edits.iter().cloned(), None, cx);
buffer.edit(edits.iter().map(|edit| edit.to_tuple()), None, cx);
})
});
let edits = construct_edits(&buffer, [(Point::new(1, 5)..Point::new(1, 5), "irst")], cx);
let highlighted_edits =
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, false, cx));
let edits_with_new_highlights = buffer
.read_with(cx, |buffer, cx| {
buffer.highlight_edit_insertions(edits.clone(), cx.background_executor())
})
.await;
let highlighted_edits = cx.read(|cx| {
HighlightedEdits::highlight(
&buffer.read(cx).snapshot(),
&edits_with_new_highlights,
false,
cx,
)
});
assert_eq!(highlighted_edits.text, " first_name: String");
assert_eq!(highlighted_edits.highlights.len(), 1);
@@ -2752,20 +2769,16 @@ async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
buffer: &Entity<Buffer>,
edits: impl IntoIterator<Item = (Range<Point>, &'static str)>,
cx: &mut TestAppContext,
) -> Arc<[(Range<Anchor>, String)]> {
buffer
.read_with(cx, |buffer, _| {
edits
.into_iter()
.map(|(range, text)| {
(
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
text.to_string(),
)
})
.collect::<Vec<_>>()
})
.into()
) -> Vec<PlainTextEdit<Anchor>> {
buffer.read_with(cx, |buffer, _| {
edits
.into_iter()
.map(|(range, text)| PlainTextEdit {
old_range: buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
new_text: text.to_string(),
})
.collect::<Vec<_>>()
})
}
}

View File

@@ -3,7 +3,10 @@ use anyhow::Result;
use futures::StreamExt as _;
use gpui::{App, Context, Entity, EntityId, Task};
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot};
use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, PlainTextEdit,
TextEditWithNewHighlights,
};
use std::{
ops::{AddAssign, Range},
path::Path,
@@ -48,7 +51,7 @@ fn completion_from_diff(
.text_for_range(delete_range.clone())
.collect::<String>();
let mut edits: Vec<(Range<language::Anchor>, String)> = Vec::new();
let mut edits: Vec<TextEditWithNewHighlights<Anchor>> = Vec::new();
let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect();
let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect();
@@ -67,8 +70,11 @@ fn completion_from_diff(
if k != 0 {
let offset = snapshot.anchor_after(offset);
// the range from the current position to item is an inlay.
let edit = (offset..offset, completion_graphemes[i..i + k].join(""));
edits.push(edit);
let edit = PlainTextEdit {
old_range: offset..offset,
new_text: completion_graphemes[i..i + k].join(""),
};
edits.push(edit.into());
}
i += k + 1;
j += 1;
@@ -85,15 +91,16 @@ fn completion_from_diff(
if j == buffer_graphemes.len() && i < completion_graphemes.len() {
let offset = snapshot.anchor_after(offset);
// there is leftover completion text, so drop it as an inlay.
let edit_range = offset..offset;
let edit_text = completion_graphemes[i..].join("");
edits.push((edit_range, edit_text));
edits.push(
PlainTextEdit {
old_range: offset..offset,
new_text: completion_graphemes[i..].join(""),
}
.into(),
);
}
InlineCompletion {
edits,
edit_preview: None,
}
InlineCompletion { edits }
}
impl InlineCompletionProvider for SupermavenCompletionProvider {

View File

@@ -5,7 +5,7 @@ use gpui::{
point, prelude::*, quad, size, AnyElement, App, Bounds, Corners, Edges, HighlightStyle, Hsla,
StyledText, TextLayout, TextStyle,
};
use language::OffsetRangeExt;
use language::{OffsetRangeExt, TextEdit};
use settings::Settings;
use theme::ThemeSettings;
use ui::prelude::*;
@@ -26,8 +26,9 @@ impl CompletionDiffElement {
let mut cursor_offset_in_diff = None;
let mut delta = 0;
let mut diff_highlights = Vec::new();
for (old_range, new_text) in completion.edits.iter() {
let old_range = old_range.to_offset(&completion.snapshot);
for edit in completion.edits.iter() {
let old_range = edit.old_range().to_offset(&completion.snapshot);
let new_text = &edit.new_text();
if cursor_offset_in_diff.is_none() && completion.cursor_offset <= old_range.end {
cursor_offset_in_diff =
@@ -51,7 +52,7 @@ impl CompletionDiffElement {
}
if !new_text.is_empty() {
diff.insert_str(old_end_in_diff, new_text);
diff.insert_str(old_end_in_diff, new_text.as_str());
diff_highlights.push((
old_end_in_diff..old_end_in_diff + new_text.len(),
HighlightStyle {

View File

@@ -14,8 +14,8 @@ use gpui::{
};
use http_client::{HttpClient, Method};
use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview,
OffsetRangeExt, Point, ToOffset, ToPoint,
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt,
PlainTextEdit, Point, TextEdit, TextEditWithNewHighlights, ToOffset, ToPoint,
};
use language_models::LlmApiToken;
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
@@ -74,9 +74,8 @@ pub struct InlineCompletion {
path: Arc<Path>,
excerpt_range: Range<usize>,
cursor_offset: usize,
edits: Arc<[(Range<Anchor>, String)]>,
edits: Arc<[TextEditWithNewHighlights<Anchor>]>,
snapshot: BufferSnapshot,
edit_preview: EditPreview,
input_outline: Arc<str>,
input_events: Arc<str>,
input_excerpt: Arc<str>,
@@ -91,22 +90,30 @@ impl InlineCompletion {
.duration_since(self.request_sent_at)
}
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
interpolate(&self.snapshot, new_snapshot, self.edits.clone())
fn interpolate(
&self,
new_snapshot: &BufferSnapshot,
) -> Option<Vec<TextEditWithNewHighlights<Anchor>>> {
interpolate(&self.snapshot, new_snapshot, &self.edits)
}
}
fn interpolate(
fn interpolate<E>(
old_snapshot: &BufferSnapshot,
new_snapshot: &BufferSnapshot,
current_edits: Arc<[(Range<Anchor>, String)]>,
) -> Option<Vec<(Range<Anchor>, String)>> {
current_edits: &[E],
) -> Option<Vec<E>>
where
E: Clone + TextEdit<Anchor>,
{
let mut edits = Vec::new();
let mut user_edits = new_snapshot
.edits_since::<usize>(&old_snapshot.version)
.peekable();
for (model_old_range, model_new_text) in current_edits.iter() {
for model_edit in current_edits.iter() {
let model_old_range = model_edit.old_range();
let model_new_text = model_edit.new_text();
let model_offset_range = model_old_range.to_offset(old_snapshot);
while let Some(next_user_edit) = user_edits.peek() {
if next_user_edit.old.end < model_offset_range.start {
@@ -118,19 +125,19 @@ fn interpolate(
if let Some(user_edit) = user_edits.peek() {
if user_edit.old.start > model_offset_range.end {
edits.push((model_old_range.clone(), model_new_text.clone()));
edits.push(model_edit.clone());
} else if user_edit.old == model_offset_range {
let user_new_text = new_snapshot
.text_for_range(user_edit.new.clone())
.collect::<String>();
if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
if !model_suffix.is_empty() {
edits.push((
new_snapshot.anchor_after(user_edit.new.end)
..new_snapshot.anchor_before(user_edit.new.end),
model_suffix.into(),
));
if model_new_text.starts_with(&user_new_text) {
if user_new_text.len() < model_new_text.len() {
let mut modified_edit = model_edit.with_prefix_dropped(user_new_text.len());
*modified_edit.mut_old_range() = new_snapshot
.anchor_after(user_edit.new.end)
..new_snapshot.anchor_before(user_edit.new.end);
edits.push(modified_edit);
}
user_edits.next();
@@ -141,7 +148,7 @@ fn interpolate(
return None;
}
} else {
edits.push((model_old_range.clone(), model_new_text.clone()));
edits.push(model_edit.clone());
}
}
@@ -606,7 +613,7 @@ and then another
cx.spawn(|cx| async move {
let output_excerpt: Arc<str> = output_excerpt.into();
let edits: Arc<[(Range<Anchor>, String)]> = cx
let edits: Vec<PlainTextEdit<Anchor>> = cx
.background_executor()
.spawn({
let output_excerpt = output_excerpt.clone();
@@ -614,23 +621,23 @@ and then another
let snapshot = snapshot.clone();
async move { Self::parse_edits(output_excerpt, excerpt_range, &snapshot) }
})
.await?
.into();
.await?;
let (edits, snapshot, edit_preview) = buffer.read_with(&cx, {
let edits = edits.clone();
|buffer, cx| {
let (edits, snapshot) = buffer.read_with(&cx, {
move |buffer, cx| {
let new_snapshot = buffer.snapshot();
let edits: Arc<[(Range<Anchor>, String)]> =
interpolate(&snapshot, &new_snapshot, edits)
.context("Interpolated edits are empty")?
.into();
let edits: Vec<PlainTextEdit<Anchor>> =
interpolate(&snapshot, &new_snapshot, &edits)
.context("Interpolated edits are empty")?;
anyhow::Ok((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
anyhow::Ok((
buffer.highlight_edit_insertions(edits, cx.background_executor()),
new_snapshot,
))
}
})??;
let edit_preview = edit_preview.await;
let edits = edits.await.into();
Ok(InlineCompletion {
id: InlineCompletionId::new(),
@@ -638,7 +645,6 @@ and then another
excerpt_range,
cursor_offset,
edits,
edit_preview,
snapshot,
input_outline: input_outline.into(),
input_events: input_events.into(),
@@ -654,7 +660,7 @@ and then another
output_excerpt: Arc<str>,
excerpt_range: Range<usize>,
snapshot: &BufferSnapshot,
) -> Result<Vec<(Range<Anchor>, String)>> {
) -> Result<Vec<PlainTextEdit<Anchor>>> {
let content = output_excerpt.replace(CURSOR_MARKER, "");
let start_markers = content
@@ -712,7 +718,7 @@ and then another
new_text: &str,
offset: usize,
snapshot: &BufferSnapshot,
) -> Vec<(Range<Anchor>, String)> {
) -> Vec<PlainTextEdit<Anchor>> {
let diff = similar::TextDiff::from_words(old_text.as_str(), new_text);
let mut edits: Vec<(Range<usize>, String)> = Vec::new();
@@ -765,10 +771,11 @@ and then another
old_range.end = old_range.end.saturating_sub(suffix_len);
let new_text = new_text[prefix_len..new_text.len() - suffix_len].to_string();
(
snapshot.anchor_after(old_range.start)..snapshot.anchor_before(old_range.end),
PlainTextEdit {
old_range: snapshot.anchor_after(old_range.start)
..snapshot.anchor_before(old_range.end),
new_text,
)
}
})
.collect()
}
@@ -1024,9 +1031,9 @@ impl CurrentInlineCompletion {
};
if old_edits.len() == 1 && new_edits.len() == 1 {
let (old_range, old_text) = &old_edits[0];
let (new_range, new_text) = &new_edits[0];
new_range == old_range && new_text.starts_with(old_text)
let old = &old_edits[0];
let new = &new_edits[0];
new.old_range() == old.old_range() && new.new_text().starts_with(old.new_text())
} else {
true
}
@@ -1226,17 +1233,20 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
};
let cursor_row = cursor_position.to_point(buffer).row;
let (closest_edit_ix, (closest_edit_range, _)) =
edits.iter().enumerate().min_by_key(|(_, (range, _))| {
let distance_from_start = cursor_row.abs_diff(range.start.to_point(buffer).row);
let distance_from_end = cursor_row.abs_diff(range.end.to_point(buffer).row);
let (closest_edit_ix, closest_edit) =
edits.iter().enumerate().min_by_key(|(_, edit)| {
let distance_from_start =
cursor_row.abs_diff(edit.old_range().start.to_point(buffer).row);
let distance_from_end =
cursor_row.abs_diff(edit.old_range().end.to_point(buffer).row);
cmp::min(distance_from_start, distance_from_end)
})?;
let closest_edit_range = &closest_edit.old_range();
let mut edit_start_ix = closest_edit_ix;
for (range, _) in edits[..edit_start_ix].iter().rev() {
let distance_from_closest_edit =
closest_edit_range.start.to_point(buffer).row - range.end.to_point(buffer).row;
for edit in edits[..edit_start_ix].iter().rev() {
let distance_from_closest_edit = closest_edit_range.start.to_point(buffer).row
- edit.old_range().end.to_point(buffer).row;
if distance_from_closest_edit <= 1 {
edit_start_ix -= 1;
} else {
@@ -1245,9 +1255,9 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
}
let mut edit_end_ix = closest_edit_ix + 1;
for (range, _) in &edits[edit_end_ix..] {
let distance_from_closest_edit =
range.start.to_point(buffer).row - closest_edit_range.end.to_point(buffer).row;
for edit in &edits[edit_end_ix..] {
let distance_from_closest_edit = edit.old_range().start.to_point(buffer).row
- closest_edit_range.end.to_point(buffer).row;
if distance_from_closest_edit <= 1 {
edit_end_ix += 1;
} else {
@@ -1257,7 +1267,6 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
Some(inline_completion::InlineCompletion {
edits: edits[edit_start_ix..edit_end_ix].to_vec(),
edit_preview: Some(completion.edit_preview.clone()),
})
}
}
@@ -1278,22 +1287,25 @@ mod tests {
#[gpui::test]
async fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx));
let edits: Arc<[(Range<Anchor>, String)]> = cx.update(|cx| {
let edits: Vec<PlainTextEdit<Anchor>> = cx.update(|cx| {
to_completion_edits(
[(2..5, "REM".to_string()), (9..11, "".to_string())],
&buffer,
cx,
)
.into()
});
let edit_preview = cx
.read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx))
.await;
let edits = cx
.read(|cx| {
buffer
.read(cx)
.highlight_edit_insertions(edits.clone(), cx.background_executor())
})
.await
.into();
let completion = InlineCompletion {
edits,
edit_preview,
path: Path::new("").into(),
snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
id: InlineCompletionId::new(),
@@ -1444,7 +1456,11 @@ mod tests {
let completion = completion_task.await.unwrap();
buffer.update(cx, |buffer, cx| {
buffer.edit(completion.edits.iter().cloned(), None, cx)
buffer.edit(
completion.edits.iter().map(|edit| edit.to_tuple()),
None,
cx,
)
});
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -1456,32 +1472,29 @@ mod tests {
iterator: impl IntoIterator<Item = (Range<usize>, String)>,
buffer: &Entity<Buffer>,
cx: &App,
) -> Vec<(Range<Anchor>, String)> {
) -> Vec<PlainTextEdit<Anchor>> {
let buffer = buffer.read(cx);
iterator
.into_iter()
.map(|(range, text)| {
(
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
text,
)
.map(|(range, text)| PlainTextEdit {
old_range: buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
new_text: text,
})
.collect()
}
fn from_completion_edits(
editor_edits: &[(Range<Anchor>, String)],
editor_edits: &[TextEditWithNewHighlights<Anchor>],
buffer: &Entity<Buffer>,
cx: &App,
) -> Vec<(Range<usize>, String)> {
let buffer = buffer.read(cx);
editor_edits
.iter()
.map(|(range, text)| {
(
range.start.to_offset(buffer)..range.end.to_offset(buffer),
text.clone(),
)
.map(|edit| {
edit.clone()
.map_position(|position| position.to_offset(buffer))
.to_tuple()
})
.collect()
}