inline completion: Add syntax highlighting for edit prediction (#23361)

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus <agus@zed.dev>
This commit is contained in:
Bennet Bo Fenner 2025-01-23 18:32:43 +01:00 committed by GitHub
parent 75ae4dada4
commit 3dee32c43d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 796 additions and 485 deletions

View file

@ -256,6 +256,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
let position = cursor_position.bias_right(buffer); let position = cursor_position.bias_right(buffer);
Some(InlineCompletion { Some(InlineCompletion {
edits: vec![(position..position, completion_text.into())], edits: vec![(position..position, completion_text.into())],
edit_preview: None,
}) })
} }
} else { } else {

View file

@ -736,13 +736,13 @@ impl CompletionsMenu {
} }
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => { CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => {
match text { match text {
InlineCompletionText::Edit { text, highlights } => div() InlineCompletionText::Edit(highlighted_edits) => div()
.mx_1() .mx_1()
.rounded_md() .rounded_md()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.child( .child(
gpui::StyledText::new(text.clone()) gpui::StyledText::new(highlighted_edits.text.clone())
.with_highlights(&style.text, highlights.clone()), .with_highlights(&style.text, highlighted_edits.highlights.clone()),
), ),
InlineCompletionText::Move(text) => div().child(text.clone()), InlineCompletionText::Move(text) => div().child(text.clone()),
} }

View file

@ -100,8 +100,8 @@ use itertools::Itertools;
use language::{ use language::{
language_settings::{self, all_language_settings, language_settings, InlayHintSettings}, language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt, CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize,
Point, Selection, SelectionGoal, TransactionId, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
}; };
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges; use linked_editing_ranges::refresh_linked_ranges;
@ -120,6 +120,7 @@ use lsp::{
LanguageServerId, LanguageServerName, LanguageServerId, LanguageServerName,
}; };
use language::BufferSnapshot;
use movement::TextLayoutDetails; use movement::TextLayoutDetails;
pub use multi_buffer::{ pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
@ -479,10 +480,7 @@ impl InlineCompletionMenuHint {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum InlineCompletionText { enum InlineCompletionText {
Move(SharedString), Move(SharedString),
Edit { Edit(HighlightedEdits),
text: SharedString,
highlights: Vec<(Range<usize>, HighlightStyle)>,
},
} }
pub(crate) enum EditDisplayMode { pub(crate) enum EditDisplayMode {
@ -494,7 +492,9 @@ pub(crate) enum EditDisplayMode {
enum InlineCompletion { enum InlineCompletion {
Edit { Edit {
edits: Vec<(Range<Anchor>, String)>, edits: Vec<(Range<Anchor>, String)>,
edit_preview: Option<EditPreview>,
display_mode: EditDisplayMode, display_mode: EditDisplayMode,
snapshot: BufferSnapshot,
}, },
Move(Anchor), Move(Anchor),
} }
@ -4695,10 +4695,7 @@ impl Editor {
selections.select_anchor_ranges([position..position]); selections.select_anchor_ranges([position..position]);
}); });
} }
InlineCompletion::Edit { InlineCompletion::Edit { edits, .. } => {
edits,
display_mode: _,
} => {
if let Some(provider) = self.inline_completion_provider() { if let Some(provider) = self.inline_completion_provider() {
provider.accept(cx); provider.accept(cx);
} }
@ -4745,10 +4742,7 @@ impl Editor {
selections.select_anchor_ranges([position..position]); selections.select_anchor_ranges([position..position]);
}); });
} }
InlineCompletion::Edit { InlineCompletion::Edit { edits, .. } => {
edits,
display_mode: _,
} => {
// Find an insertion that starts at the cursor position. // Find an insertion that starts at the cursor position.
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor_offset = self.selections.newest::<usize>(cx).head(); let cursor_offset = self.selections.newest::<usize>(cx).head();
@ -4883,8 +4877,8 @@ impl Editor {
let (buffer, cursor_buffer_position) = let (buffer, cursor_buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
let completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
let edits = completion let edits = inline_completion
.edits .edits
.into_iter() .into_iter()
.flat_map(|(range, new_text)| { .flat_map(|(range, new_text)| {
@ -4909,13 +4903,12 @@ impl Editor {
let mut inlay_ids = Vec::new(); let mut inlay_ids = Vec::new();
let invalidation_row_range; let invalidation_row_range;
let completion; let completion = if cursor_row < edit_start_row {
if cursor_row < edit_start_row {
invalidation_row_range = cursor_row..edit_end_row; invalidation_row_range = cursor_row..edit_end_row;
completion = InlineCompletion::Move(first_edit_start); InlineCompletion::Move(first_edit_start)
} else if cursor_row > edit_end_row { } else if cursor_row > edit_end_row {
invalidation_row_range = edit_start_row..cursor_row; invalidation_row_range = edit_start_row..cursor_row;
completion = InlineCompletion::Move(first_edit_start); InlineCompletion::Move(first_edit_start)
} else { } else {
if edits if edits
.iter() .iter()
@ -4960,10 +4953,14 @@ impl Editor {
EditDisplayMode::DiffPopover EditDisplayMode::DiffPopover
}; };
completion = InlineCompletion::Edit { let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
InlineCompletion::Edit {
edits, edits,
edit_preview: inline_completion.edit_preview,
display_mode, display_mode,
}; snapshot,
}
}; };
let invalidation_range = multibuffer let invalidation_range = multibuffer
@ -5006,19 +5003,26 @@ impl Editor {
let text = match &self.active_inline_completion.as_ref()?.completion { let text = match &self.active_inline_completion.as_ref()?.completion {
InlineCompletion::Edit { InlineCompletion::Edit {
edits, edits,
edit_preview,
display_mode: _, display_mode: _,
} => inline_completion_edit_text(&editor_snapshot, edits, true, cx), snapshot,
} => edit_preview
.as_ref()
.and_then(|edit_preview| {
inline_completion_edit_text(&snapshot, &edits, edit_preview, true, cx)
})
.map(InlineCompletionText::Edit),
InlineCompletion::Move(target) => { InlineCompletion::Move(target) => {
let target_point = let target_point =
target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot); target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot);
let target_line = target_point.row + 1; let target_line = target_point.row + 1;
InlineCompletionText::Move( Some(InlineCompletionText::Move(
format!("Jump to edit in line {}", target_line).into(), format!("Jump to edit in line {}", target_line).into(),
) ))
} }
}; };
Some(InlineCompletionMenuHint::Loaded { text }) Some(InlineCompletionMenuHint::Loaded { text: text? })
} else if provider.is_refreshing(cx) { } else if provider.is_refreshing(cx) {
Some(InlineCompletionMenuHint::Loading) Some(InlineCompletionMenuHint::Loading)
} else if provider.needs_terms_acceptance(cx) { } else if provider.needs_terms_acceptance(cx) {
@ -14970,74 +14974,23 @@ pub fn diagnostic_block_renderer(
} }
fn inline_completion_edit_text( fn inline_completion_edit_text(
editor_snapshot: &EditorSnapshot, current_snapshot: &BufferSnapshot,
edits: &Vec<(Range<Anchor>, String)>, edits: &[(Range<Anchor>, String)],
edit_preview: &EditPreview,
include_deletions: bool, include_deletions: bool,
cx: &WindowContext, cx: &WindowContext,
) -> InlineCompletionText { ) -> Option<HighlightedEdits> {
let edit_start = edits let edits = edits
.first() .iter()
.unwrap() .map(|(anchor, text)| {
.0 (
.start anchor.start.text_anchor..anchor.end.text_anchor,
.to_display_point(editor_snapshot); text.clone(),
)
})
.collect::<Vec<_>>();
let mut text = String::new(); Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx))
let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
let mut highlights = Vec::new();
for (old_range, new_text) in edits {
let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..old_offset_range.start, false)
.map(|chunk| chunk.text),
);
offset = old_offset_range.end;
let start = text.len();
let color = if include_deletions && new_text.is_empty() {
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(old_offset_range.start..offset, false)
.map(|chunk| chunk.text),
);
cx.theme().status().deleted_background
} else {
text.push_str(new_text);
cx.theme().status().created_background
};
let end = text.len();
highlights.push((
start..end,
HighlightStyle {
background_color: Some(color),
..Default::default()
},
));
}
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
.to_offset(editor_snapshot, Bias::Right);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..end_of_line, false)
.map(|chunk| chunk.text),
);
InlineCompletionText::Edit {
text: text.into(),
highlights,
}
} }
pub fn highlight_diagnostic_message( pub fn highlight_diagnostic_message(

View file

@ -14733,241 +14733,205 @@ async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppCon
} }
#[gpui::test] #[gpui::test]
fn test_inline_completion_text(cx: &mut TestAppContext) { async fn test_inline_completion_text(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
// Simple insertion // Simple insertion
{ assert_highlighted_edits(
let window = cx.add_window(|cx| { "Hello, world!",
let buffer = MultiBuffer::build_simple("Hello, world!", cx); vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
Editor::new(EditorMode::Full, buffer, None, true, cx) true,
}); cx,
let cx = &mut VisualTestContext::from_window(*window, cx); |highlighted_edits, cx| {
assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
window assert_eq!(highlighted_edits.highlights.len(), 1);
.update(cx, |editor, cx| { assert_eq!(highlighted_edits.highlights[0].0, 6..16);
let snapshot = editor.snapshot(cx); assert_eq!(
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6)) highlighted_edits.highlights[0].1.background_color,
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6)); Some(cx.theme().status().created_background)
let edits = vec![(edit_range, " beautiful".to_string())]; );
},
let InlineCompletionText::Edit { text, highlights } = )
inline_completion_edit_text(&snapshot, &edits, false, cx) .await;
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Hello, beautiful world!");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 6..16);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Replacement // Replacement
{ assert_highlighted_edits(
let window = cx.add_window(|cx| { "This is a test.",
let buffer = MultiBuffer::build_simple("This is a test.", cx); vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
Editor::new(EditorMode::Full, buffer, None, true, cx) false,
}); cx,
let cx = &mut VisualTestContext::from_window(*window, cx); |highlighted_edits, cx| {
assert_eq!(highlighted_edits.text, "That is a test.");
window assert_eq!(highlighted_edits.highlights.len(), 1);
.update(cx, |editor, cx| { assert_eq!(highlighted_edits.highlights[0].0, 0..4);
let snapshot = editor.snapshot(cx); assert_eq!(
let edits = vec![( highlighted_edits.highlights[0].1.background_color,
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0)) Some(cx.theme().status().created_background)
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)), );
"That".to_string(), },
)]; )
.await;
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, false, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "That is a test.");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 0..4);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Multiple edits // Multiple edits
{ assert_highlighted_edits(
let window = cx.add_window(|cx| { "Hello, world!",
let buffer = MultiBuffer::build_simple("Hello, world!", cx); vec![
Editor::new(EditorMode::Full, buffer, None, true, cx) (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
}); (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
let cx = &mut VisualTestContext::from_window(*window, cx); ],
false,
window cx,
.update(cx, |editor, cx| { |highlighted_edits, cx| {
let snapshot = editor.snapshot(cx); assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
let edits = vec![ assert_eq!(highlighted_edits.highlights.len(), 2);
( assert_eq!(highlighted_edits.highlights[0].0, 0..9);
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0)) assert_eq!(highlighted_edits.highlights[1].0, 16..29);
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)), assert_eq!(
"Greetings".into(), highlighted_edits.highlights[0].1.background_color,
), Some(cx.theme().status().created_background)
( );
snapshot.buffer_snapshot.anchor_after(Point::new(0, 12)) assert_eq!(
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)), highlighted_edits.highlights[1].1.background_color,
" and universe".into(), Some(cx.theme().status().created_background)
), );
]; },
)
let InlineCompletionText::Edit { text, highlights } = .await;
inline_completion_edit_text(&snapshot, &edits, false, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Greetings, world and universe!");
assert_eq!(highlights.len(), 2);
assert_eq!(highlights[0].0, 0..9);
assert_eq!(highlights[1].0, 16..29);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
assert_eq!(
highlights[1].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Multiple lines with edits // Multiple lines with edits
{ assert_highlighted_edits(
let window = cx.add_window(|cx| { "First line\nSecond line\nThird line\nFourth line",
let buffer = vec![
MultiBuffer::build_simple("First line\nSecond line\nThird line\nFourth line", cx); (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
Editor::new(EditorMode::Full, buffer, None, true, cx) (
}); Point::new(2, 0)..Point::new(2, 10),
let cx = &mut VisualTestContext::from_window(*window, cx); "New third line".to_string(),
),
window (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
.update(cx, |editor, cx| { ],
let snapshot = editor.snapshot(cx); false,
let edits = vec![ cx,
( |highlighted_edits, cx| {
snapshot.buffer_snapshot.anchor_before(Point::new(1, 7)) assert_eq!(
..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)), highlighted_edits.text,
"modified".to_string(), "Second modified\nNew third line\nFourth updated line"
), );
( assert_eq!(highlighted_edits.highlights.len(), 3);
snapshot.buffer_snapshot.anchor_before(Point::new(2, 0)) assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)), assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
"New third line".to_string(), assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
), for highlight in &highlighted_edits.highlights {
( assert_eq!(
snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)) highlight.1.background_color,
..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)), Some(cx.theme().status().created_background)
" updated".to_string(), );
), }
]; },
)
let InlineCompletionText::Edit { text, highlights } = .await;
inline_completion_edit_text(&snapshot, &edits, false, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
assert_eq!(highlights.len(), 3);
assert_eq!(highlights[0].0, 7..15); // "modified"
assert_eq!(highlights[1].0, 16..30); // "New third line"
assert_eq!(highlights[2].0, 37..45); // " updated"
for highlight in &highlights {
assert_eq!(
highlight.1.background_color,
Some(cx.theme().status().created_background)
);
}
})
.unwrap();
}
} }
#[gpui::test] #[gpui::test]
fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) { async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
// Deletion // Deletion
{ assert_highlighted_edits(
let window = cx.add_window(|cx| { "Hello, world!",
let buffer = MultiBuffer::build_simple("Hello, world!", cx); vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
Editor::new(EditorMode::Full, buffer, None, true, cx) true,
}); cx,
let cx = &mut VisualTestContext::from_window(*window, cx); |highlighted_edits, cx| {
assert_eq!(highlighted_edits.text, "Hello, world!");
window assert_eq!(highlighted_edits.highlights.len(), 1);
.update(cx, |editor, cx| { assert_eq!(highlighted_edits.highlights[0].0, 5..11);
let snapshot = editor.snapshot(cx); assert_eq!(
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 5)) highlighted_edits.highlights[0].1.background_color,
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 11)); Some(cx.theme().status().deleted_background)
let edits = vec![(edit_range, "".to_string())]; );
},
let InlineCompletionText::Edit { text, highlights } = )
inline_completion_edit_text(&snapshot, &edits, true, cx) .await;
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Hello, world!");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 5..11);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().deleted_background)
);
})
.unwrap();
}
// Insertion // Insertion
{ assert_highlighted_edits(
let window = cx.add_window(|cx| { "Hello, world!",
let buffer = MultiBuffer::build_simple("Hello, world!", cx); vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
Editor::new(EditorMode::Full, buffer, None, true, cx) true,
}); cx,
let cx = &mut VisualTestContext::from_window(*window, cx); |highlighted_edits, cx| {
assert_eq!(highlighted_edits.highlights.len(), 1);
assert_eq!(highlighted_edits.highlights[0].0, 6..14);
assert_eq!(
highlighted_edits.highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
},
)
.await;
}
window async fn assert_highlighted_edits(
.update(cx, |editor, cx| { text: &str,
let snapshot = editor.snapshot(cx); edits: Vec<(Range<Point>, String)>,
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6)) include_deletions: bool,
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6)); cx: &mut TestAppContext,
let edits = vec![(edit_range, " digital".to_string())]; assertion_fn: impl Fn(HighlightedEdits, &AppContext),
) {
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(text, cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
let InlineCompletionText::Edit { text, highlights } = let (buffer, snapshot) = window
inline_completion_edit_text(&snapshot, &edits, true, cx) .update(cx, |editor, cx| {
else { (
panic!("Failed to generate inline completion text"); editor.buffer().clone(),
}; editor.buffer().read(cx).snapshot(cx),
)
})
.unwrap();
assert_eq!(text, "Hello, digital world!"); let edits = edits
assert_eq!(highlights.len(), 1); .into_iter()
assert_eq!(highlights[0].0, 6..14); .map(|(range, edit)| {
assert_eq!( (
highlights[0].1.background_color, snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
Some(cx.theme().status().created_background) edit,
); )
}) })
.unwrap(); .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, |_, cx| {
buffer
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.preview_edits(text_anchor_edits.into(), cx)
})
.unwrap()
.await;
cx.update(|cx| {
let highlighted_edits = inline_completion_edit_text(
&snapshot.as_singleton().unwrap().2,
&edits,
&edit_preview,
include_deletions,
cx,
)
.expect("Missing highlighted edits");
assertion_fn(highlighted_edits, cx)
});
} }
#[gpui::test] #[gpui::test]

View file

@ -3390,7 +3390,9 @@ impl EditorElement {
} }
InlineCompletion::Edit { InlineCompletion::Edit {
edits, edits,
edit_preview,
display_mode, display_mode,
snapshot,
} => { } => {
if self.editor.read(cx).has_active_completions_menu() { if self.editor.read(cx).has_active_completions_menu() {
return None; return None;
@ -3442,13 +3444,11 @@ impl EditorElement {
EditDisplayMode::DiffPopover => {} EditDisplayMode::DiffPopover => {}
} }
let crate::InlineCompletionText::Edit { text, highlights } = let highlighted_edits = edit_preview.as_ref().and_then(|edit_preview| {
crate::inline_completion_edit_text(editor_snapshot, edits, false, cx) crate::inline_completion_edit_text(&snapshot, edits, edit_preview, false, cx)
else { })?;
return None;
};
let line_count = text.lines().count() + 1; let line_count = highlighted_edits.text.lines().count() + 1;
let longest_row = let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
@ -3466,15 +3466,14 @@ impl EditorElement {
.width .width
}; };
let styled_text = let styled_text = gpui::StyledText::new(highlighted_edits.text.clone())
gpui::StyledText::new(text.clone()).with_highlights(&style.text, highlights); .with_highlights(&style.text, highlighted_edits.highlights);
let mut element = div() let mut element = div()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.border_1() .border_1()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.rounded_md() .rounded_md()
.px_1()
.child(styled_text) .child(styled_text)
.into_any(); .into_any();

View file

@ -333,6 +333,7 @@ fn propose_edits<T: ToOffset>(
provider.update(cx, |provider, _| { provider.update(cx, |provider, _| {
provider.set_inline_completion(Some(inline_completion::InlineCompletion { provider.set_inline_completion(Some(inline_completion::InlineCompletion {
edits: edits.collect(), edits: edits.collect(),
edit_preview: None,
})) }))
}) })
}); });

View file

@ -15,6 +15,7 @@ pub enum Direction {
#[derive(Clone)] #[derive(Clone)]
pub struct InlineCompletion { pub struct InlineCompletion {
pub edits: Vec<(Range<language::Anchor>, String)>, pub edits: Vec<(Range<language::Anchor>, String)>,
pub edit_preview: Option<language::EditPreview>,
} }
pub trait InlineCompletionProvider: 'static + Sized { pub trait InlineCompletionProvider: 'static + Sized {

View file

@ -26,7 +26,7 @@ use fs::MTime;
use futures::channel::oneshot; use futures::channel::oneshot;
use gpui::{ use gpui::{
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext, AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
Pixels, Task, TaskLabel, WindowContext, Pixels, SharedString, Task, TaskLabel, WindowContext,
}; };
use lsp::LanguageServerId; use lsp::LanguageServerId;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -65,7 +65,7 @@ pub use text::{
Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16, Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16,
Transaction, TransactionId, Unclipped, Transaction, TransactionId, Unclipped,
}; };
use theme::SyntaxTheme; use theme::{ActiveTheme as _, SyntaxTheme};
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter; use util::RandomCharIter;
use util::{debug_panic, maybe, RangeExt}; use util::{debug_panic, maybe, RangeExt};
@ -603,6 +603,162 @@ impl IndentGuide {
} }
} }
#[derive(Clone)]
pub struct EditPreview {
applied_edits_snapshot: text::BufferSnapshot,
syntax_snapshot: SyntaxSnapshot,
}
#[derive(Default, Clone, Debug)]
pub struct HighlightedEdits {
pub text: SharedString,
pub highlights: Vec<(Range<usize>, HighlightStyle)>,
}
impl EditPreview {
pub fn highlight_edits(
&self,
current_snapshot: &BufferSnapshot,
edits: &[(Range<Anchor>, String)],
include_deletions: bool,
cx: &AppContext,
) -> HighlightedEdits {
let mut text = String::new();
let mut highlights = Vec::new();
let Some(range) = self.compute_visible_range(edits, current_snapshot) else {
return HighlightedEdits::default();
};
let mut offset = range.start;
let mut delta = 0isize;
let status_colors = cx.theme().status();
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();
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 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,
&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;
}
self.highlight_text(
offset..(range.end as isize + delta) as usize,
&mut text,
&mut highlights,
None,
cx,
);
HighlightedEdits {
text: text.into(),
highlights,
}
}
fn highlight_text(
&self,
range: Range<usize>,
text: &mut String,
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
override_style: Option<HighlightStyle>,
cx: &AppContext,
) {
for chunk in self.highlighted_chunks(range) {
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()))
{
if let Some(override_style) = override_style {
highlight_style.highlight(override_style);
}
highlights.push((start..end, highlight_style));
} else if let Some(override_style) = override_style {
highlights.push((start..end, override_style));
}
}
}
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,
)
}
fn compute_visible_range(
&self,
edits: &[(Range<Anchor>, String)],
snapshot: &BufferSnapshot,
) -> Option<Range<usize>> {
let (first, _) = edits.first()?;
let (last, _) = edits.last()?;
let start = first.start.to_point(snapshot);
let end = last.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));
Some(range.to_offset(&snapshot))
}
}
impl Buffer { impl Buffer {
/// Create a new buffer with the given base text. /// Create a new buffer with the given base text.
pub fn local<T: Into<String>>(base_text: T, cx: &ModelContext<Self>) -> Self { pub fn local<T: Into<String>>(base_text: T, cx: &ModelContext<Self>) -> Self {
@ -825,6 +981,33 @@ impl Buffer {
}) })
} }
pub fn preview_edits(
&self,
edits: Arc<[(Range<Anchor>, String)]>,
cx: &AppContext,
) -> Task<EditPreview> {
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);
if let Some(language) = language {
syntax_snapshot.reparse(&snapshot, registry, language);
}
}
EditPreview {
applied_edits_snapshot: branch_buffer.snapshot(),
syntax_snapshot,
}
})
}
/// Applies all of the changes in this buffer that intersect any of the /// Applies all of the changes in this buffer that intersect any of the
/// given `ranges` to its base buffer. /// given `ranges` to its base buffer.
/// ///

View file

@ -2718,6 +2718,148 @@ fn test_undo_after_merge_into_base(cx: &mut TestAppContext) {
branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk")); branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
} }
#[gpui::test]
async fn test_preview_edits(cx: &mut TestAppContext) {
cx.update(|cx| {
init_settings(cx, |_| {});
theme::init(theme::LoadThemes::JustBase, cx);
});
let text = indoc! {r#"
struct Person {
first_name: String,
}
impl Person {
fn last_name(&self) -> &String {
&self.last_name
}
}"#
};
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
let highlighted_edits = preview_edits(
&buffer,
cx,
[
(Point::new(5, 7)..Point::new(5, 11), "first"),
(Point::new(6, 14)..Point::new(6, 18), "first"),
],
)
.await;
assert_eq!(
highlighted_edits.text,
" fn lastfirst_name(&self) -> &String {\n &self.lastfirst_name"
);
async fn preview_edits(
buffer: &Model<Buffer>,
cx: &mut TestAppContext,
edits: impl IntoIterator<Item = (Range<Point>, &'static str)>,
) -> HighlightedEdits {
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(),
)
})
.collect::<Vec<_>>()
});
let edit_preview = buffer
.read_with(cx, |buffer, cx| {
buffer.preview_edits(edits.clone().into(), cx)
})
.await;
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, true, cx))
}
}
#[gpui::test]
async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
use theme::ActiveTheme;
cx.update(|cx| {
init_settings(cx, |_| {});
theme::init(theme::LoadThemes::JustBase, cx);
});
let text = indoc! {r#"
struct Person {
_name: String
}"#
};
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let buffer = cx.new_model(|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))
.await;
let highlighted_edits =
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, false, cx));
let created_background = cx.read(|cx| cx.theme().status().created_background);
assert_eq!(highlighted_edits.text, " first_name: String");
assert_eq!(highlighted_edits.highlights.len(), 1);
assert_eq!(highlighted_edits.highlights[0].0, 4..9);
assert_eq!(
highlighted_edits.highlights[0].1.background_color,
Some(created_background)
);
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);
})
});
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));
assert_eq!(highlighted_edits.text, " first_name: String");
assert_eq!(highlighted_edits.highlights.len(), 1);
assert_eq!(highlighted_edits.highlights[0].0, (5..9));
assert_eq!(
highlighted_edits.highlights[0].1.background_color,
Some(created_background)
);
fn construct_edits(
buffer: &Model<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()
}
}
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
let min_peers = env::var("MIN_PEERS") let min_peers = env::var("MIN_PEERS")

View file

@ -263,7 +263,7 @@ impl SyntaxSnapshot {
self.layers.is_empty() self.layers.is_empty()
} }
fn interpolate(&mut self, text: &BufferSnapshot) { pub fn interpolate(&mut self, text: &BufferSnapshot) {
let edits = text let edits = text
.anchored_edits_since::<(usize, Point)>(&self.interpolated_version) .anchored_edits_since::<(usize, Point)>(&self.interpolated_version)
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -90,7 +90,10 @@ fn completion_from_diff(
edits.push((edit_range, edit_text)); edits.push((edit_range, edit_text));
} }
InlineCompletion { edits } InlineCompletion {
edits,
edit_preview: None,
}
} }
impl InlineCompletionProvider for SupermavenCompletionProvider { impl InlineCompletionProvider for SupermavenCompletionProvider {

View file

@ -15,8 +15,8 @@ use gpui::{
}; };
use http_client::{HttpClient, Method}; use http_client::{HttpClient, Method};
use language::{ use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt, language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview,
Point, ToOffset, ToPoint, OffsetRangeExt, Point, ToOffset, ToPoint,
}; };
use language_models::LlmApiToken; use language_models::LlmApiToken;
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME}; use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
@ -77,6 +77,7 @@ pub struct InlineCompletion {
cursor_offset: usize, cursor_offset: usize,
edits: Arc<[(Range<Anchor>, String)]>, edits: Arc<[(Range<Anchor>, String)]>,
snapshot: BufferSnapshot, snapshot: BufferSnapshot,
edit_preview: EditPreview,
input_outline: Arc<str>, input_outline: Arc<str>,
input_events: Arc<str>, input_events: Arc<str>,
input_excerpt: Arc<str>, input_excerpt: Arc<str>,
@ -92,56 +93,64 @@ impl InlineCompletion {
} }
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> { fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
let mut edits = Vec::new(); interpolate(&self.snapshot, new_snapshot, self.edits.clone())
}
}
let mut user_edits = new_snapshot fn interpolate(
.edits_since::<usize>(&self.snapshot.version) old_snapshot: &BufferSnapshot,
.peekable(); new_snapshot: &BufferSnapshot,
for (model_old_range, model_new_text) in self.edits.iter() { current_edits: Arc<[(Range<Anchor>, String)]>,
let model_offset_range = model_old_range.to_offset(&self.snapshot); ) -> Option<Vec<(Range<Anchor>, String)>> {
while let Some(next_user_edit) = user_edits.peek() { let mut edits = Vec::new();
if next_user_edit.old.end < model_offset_range.start {
user_edits.next(); let mut user_edits = new_snapshot
} else { .edits_since::<usize>(&old_snapshot.version)
break; .peekable();
} for (model_old_range, model_new_text) in current_edits.iter() {
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 {
user_edits.next();
} else {
break;
} }
}
if let Some(user_edit) = user_edits.peek() { if let Some(user_edit) = user_edits.peek() {
if user_edit.old.start > model_offset_range.end { if user_edit.old.start > model_offset_range.end {
edits.push((model_old_range.clone(), model_new_text.clone())); edits.push((model_old_range.clone(), model_new_text.clone()));
} else if user_edit.old == model_offset_range { } else if user_edit.old == model_offset_range {
let user_new_text = new_snapshot let user_new_text = new_snapshot
.text_for_range(user_edit.new.clone()) .text_for_range(user_edit.new.clone())
.collect::<String>(); .collect::<String>();
if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) { if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
if !model_suffix.is_empty() { if !model_suffix.is_empty() {
edits.push(( edits.push((
new_snapshot.anchor_after(user_edit.new.end) new_snapshot.anchor_after(user_edit.new.end)
..new_snapshot.anchor_before(user_edit.new.end), ..new_snapshot.anchor_before(user_edit.new.end),
model_suffix.into(), model_suffix.into(),
)); ));
}
user_edits.next();
} else {
return None;
} }
user_edits.next();
} else { } else {
return None; return None;
} }
} else { } else {
edits.push((model_old_range.clone(), model_new_text.clone())); return None;
} }
}
if edits.is_empty() {
None
} else { } else {
Some(edits) edits.push((model_old_range.clone(), model_new_text.clone()));
} }
} }
if edits.is_empty() {
None
} else {
Some(edits)
}
} }
impl std::fmt::Debug for InlineCompletion { impl std::fmt::Debug for InlineCompletion {
@ -302,7 +311,8 @@ impl Zeta {
F: FnOnce(Arc<Client>, LlmApiToken, PredictEditsParams) -> R + 'static, F: FnOnce(Arc<Client>, LlmApiToken, PredictEditsParams) -> R + 'static,
R: Future<Output = Result<PredictEditsResponse>> + Send + 'static, R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
{ {
let snapshot = self.report_changes_for_buffer(buffer, cx); let buffer = buffer.clone();
let snapshot = self.report_changes_for_buffer(&buffer, cx);
let point = position.to_point(&snapshot); let point = position.to_point(&snapshot);
let offset = point.to_offset(&snapshot); let offset = point.to_offset(&snapshot);
let excerpt_range = excerpt_range_for_position(point, &snapshot); let excerpt_range = excerpt_range_for_position(point, &snapshot);
@ -356,6 +366,7 @@ impl Zeta {
Self::process_completion_response( Self::process_completion_response(
output_excerpt, output_excerpt,
buffer,
&snapshot, &snapshot,
excerpt_range, excerpt_range,
offset, offset,
@ -581,6 +592,7 @@ and then another
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn process_completion_response( fn process_completion_response(
output_excerpt: String, output_excerpt: String,
buffer: Model<Buffer>,
snapshot: &BufferSnapshot, snapshot: &BufferSnapshot,
excerpt_range: Range<usize>, excerpt_range: Range<usize>,
cursor_offset: usize, cursor_offset: usize,
@ -592,70 +604,110 @@ and then another
cx: &AsyncAppContext, cx: &AsyncAppContext,
) -> Task<Result<InlineCompletion>> { ) -> Task<Result<InlineCompletion>> {
let snapshot = snapshot.clone(); let snapshot = snapshot.clone();
cx.background_executor().spawn(async move { cx.spawn(|cx| async move {
let content = output_excerpt.replace(CURSOR_MARKER, ""); let output_excerpt: Arc<str> = output_excerpt.into();
let start_markers = content let edits: Arc<[(Range<Anchor>, String)]> = cx
.match_indices(EDITABLE_REGION_START_MARKER) .background_executor()
.collect::<Vec<_>>(); .spawn({
anyhow::ensure!( let output_excerpt = output_excerpt.clone();
start_markers.len() == 1, let excerpt_range = excerpt_range.clone();
"expected exactly one start marker, found {}", let snapshot = snapshot.clone();
start_markers.len() async move { Self::parse_edits(output_excerpt, excerpt_range, &snapshot) }
); })
.await?
.into();
let end_markers = content let (edits, snapshot, edit_preview) = buffer.read_with(&cx, {
.match_indices(EDITABLE_REGION_END_MARKER) let edits = edits.clone();
.collect::<Vec<_>>(); |buffer, cx| {
anyhow::ensure!( let new_snapshot = buffer.snapshot();
end_markers.len() == 1, let edits: Arc<[(Range<Anchor>, String)]> =
"expected exactly one end marker, found {}", interpolate(&snapshot, &new_snapshot, edits)
end_markers.len() .context("Interpolated edits are empty")?
); .into();
let sof_markers = content anyhow::Ok((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
.match_indices(START_OF_FILE_MARKER) }
.collect::<Vec<_>>(); })??;
anyhow::ensure!(
sof_markers.len() <= 1,
"expected at most one start-of-file marker, found {}",
sof_markers.len()
);
let codefence_start = start_markers[0].0; let edit_preview = edit_preview.await;
let content = &content[codefence_start..];
let newline_ix = content.find('\n').context("could not find newline")?;
let content = &content[newline_ix + 1..];
let codefence_end = content
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
.context("could not find end marker")?;
let new_text = &content[..codefence_end];
let old_text = snapshot
.text_for_range(excerpt_range.clone())
.collect::<String>();
let edits = Self::compute_edits(old_text, new_text, excerpt_range.start, &snapshot);
Ok(InlineCompletion { Ok(InlineCompletion {
id: InlineCompletionId::new(), id: InlineCompletionId::new(),
path, path,
excerpt_range, excerpt_range,
cursor_offset, cursor_offset,
edits: edits.into(), edits,
snapshot: snapshot.clone(), edit_preview,
snapshot,
input_outline: input_outline.into(), input_outline: input_outline.into(),
input_events: input_events.into(), input_events: input_events.into(),
input_excerpt: input_excerpt.into(), input_excerpt: input_excerpt.into(),
output_excerpt: output_excerpt.into(), output_excerpt,
request_sent_at, request_sent_at,
response_received_at: Instant::now(), response_received_at: Instant::now(),
}) })
}) })
} }
fn parse_edits(
output_excerpt: Arc<str>,
excerpt_range: Range<usize>,
snapshot: &BufferSnapshot,
) -> Result<Vec<(Range<Anchor>, String)>> {
let content = output_excerpt.replace(CURSOR_MARKER, "");
let start_markers = content
.match_indices(EDITABLE_REGION_START_MARKER)
.collect::<Vec<_>>();
anyhow::ensure!(
start_markers.len() == 1,
"expected exactly one start marker, found {}",
start_markers.len()
);
let end_markers = content
.match_indices(EDITABLE_REGION_END_MARKER)
.collect::<Vec<_>>();
anyhow::ensure!(
end_markers.len() == 1,
"expected exactly one end marker, found {}",
end_markers.len()
);
let sof_markers = content
.match_indices(START_OF_FILE_MARKER)
.collect::<Vec<_>>();
anyhow::ensure!(
sof_markers.len() <= 1,
"expected at most one start-of-file marker, found {}",
sof_markers.len()
);
let codefence_start = start_markers[0].0;
let content = &content[codefence_start..];
let newline_ix = content.find('\n').context("could not find newline")?;
let content = &content[newline_ix + 1..];
let codefence_end = content
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
.context("could not find end marker")?;
let new_text = &content[..codefence_end];
let old_text = snapshot
.text_for_range(excerpt_range.clone())
.collect::<String>();
Ok(Self::compute_edits(
old_text,
new_text,
excerpt_range.start,
&snapshot,
))
}
pub fn compute_edits( pub fn compute_edits(
old_text: String, old_text: String,
new_text: &str, new_text: &str,
@ -1206,6 +1258,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
Some(inline_completion::InlineCompletion { Some(inline_completion::InlineCompletion {
edits: edits[edit_start_ix..edit_end_ix].to_vec(), edits: edits[edit_start_ix..edit_end_ix].to_vec(),
edit_preview: Some(completion.edit_preview.clone()),
}) })
} }
} }
@ -1224,17 +1277,26 @@ mod tests {
use super::*; use super::*;
#[gpui::test] #[gpui::test]
fn test_inline_completion_basic_interpolation(cx: &mut AppContext) { async fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) {
let buffer = cx.new_model(|cx| Buffer::local("Lorem ipsum dolor", cx)); let buffer = cx.new_model(|cx| Buffer::local("Lorem ipsum dolor", cx));
let completion = InlineCompletion { let edits: Arc<[(Range<Anchor>, String)]> = cx.update(|cx| {
edits: to_completion_edits( to_completion_edits(
[(2..5, "REM".to_string()), (9..11, "".to_string())], [(2..5, "REM".to_string()), (9..11, "".to_string())],
&buffer, &buffer,
cx, cx,
) )
.into(), .into()
});
let edit_preview = cx
.read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx))
.await;
let completion = InlineCompletion {
edits,
edit_preview,
path: Path::new("").into(), path: Path::new("").into(),
snapshot: buffer.read(cx).snapshot(), snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
id: InlineCompletionId::new(), id: InlineCompletionId::new(),
excerpt_range: 0..0, excerpt_range: 0..0,
cursor_offset: 0, cursor_offset: 0,
@ -1246,87 +1308,89 @@ mod tests {
response_received_at: Instant::now(), response_received_at: Instant::now(),
}; };
assert_eq!( cx.update(|cx| {
from_completion_edits( assert_eq!(
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), from_completion_edits(
&buffer, &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
cx &buffer,
), cx
vec![(2..5, "REM".to_string()), (9..11, "".to_string())] ),
); vec![(2..5, "REM".to_string()), (9..11, "".to_string())]
);
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx)); buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
assert_eq!( assert_eq!(
from_completion_edits( from_completion_edits(
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer, &buffer,
cx cx
), ),
vec![(2..2, "REM".to_string()), (6..8, "".to_string())] vec![(2..2, "REM".to_string()), (6..8, "".to_string())]
); );
buffer.update(cx, |buffer, cx| buffer.undo(cx)); buffer.update(cx, |buffer, cx| buffer.undo(cx));
assert_eq!( assert_eq!(
from_completion_edits( from_completion_edits(
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer, &buffer,
cx cx
), ),
vec![(2..5, "REM".to_string()), (9..11, "".to_string())] vec![(2..5, "REM".to_string()), (9..11, "".to_string())]
); );
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx)); buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
assert_eq!( assert_eq!(
from_completion_edits( from_completion_edits(
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer, &buffer,
cx cx
), ),
vec![(3..3, "EM".to_string()), (7..9, "".to_string())] vec![(3..3, "EM".to_string()), (7..9, "".to_string())]
); );
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx)); buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
assert_eq!( assert_eq!(
from_completion_edits( from_completion_edits(
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer, &buffer,
cx cx
), ),
vec![(4..4, "M".to_string()), (8..10, "".to_string())] vec![(4..4, "M".to_string()), (8..10, "".to_string())]
); );
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx)); buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
assert_eq!( assert_eq!(
from_completion_edits( from_completion_edits(
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer, &buffer,
cx cx
), ),
vec![(9..11, "".to_string())] vec![(9..11, "".to_string())]
); );
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx)); buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
assert_eq!( assert_eq!(
from_completion_edits( from_completion_edits(
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer, &buffer,
cx cx
), ),
vec![(4..4, "M".to_string()), (8..10, "".to_string())] vec![(4..4, "M".to_string()), (8..10, "".to_string())]
); );
buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx)); buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
assert_eq!( assert_eq!(
from_completion_edits( from_completion_edits(
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer, &buffer,
cx cx
), ),
vec![(4..4, "M".to_string())] vec![(4..4, "M".to_string())]
); );
buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx)); buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
assert_eq!(completion.interpolate(&buffer.read(cx).snapshot()), None); assert_eq!(completion.interpolate(&buffer.read(cx).snapshot()), None);
})
} }
#[gpui::test] #[gpui::test]