diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 79443f8d3a..73fc9e6a8e 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -256,6 +256,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider { let position = cursor_position.bias_right(buffer); Some(InlineCompletion { edits: vec![(position..position, completion_text.into())], + edit_preview: None, }) } } else { diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index fbe413d947..a9429995d8 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -736,13 +736,13 @@ impl CompletionsMenu { } CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => { match text { - InlineCompletionText::Edit { text, highlights } => div() + InlineCompletionText::Edit(highlighted_edits) => div() .mx_1() .rounded_md() .bg(cx.theme().colors().editor_background) .child( - gpui::StyledText::new(text.clone()) - .with_highlights(&style.text, highlights.clone()), + gpui::StyledText::new(highlighted_edits.text.clone()) + .with_highlights(&style.text, highlighted_edits.highlights.clone()), ), InlineCompletionText::Move(text) => div().child(text.clone()), } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ad13de90b6..7650129da2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -100,8 +100,8 @@ 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, IndentKind, IndentSize, Language, OffsetRangeExt, - Point, Selection, SelectionGoal, TransactionId, + CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize, + Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; @@ -120,6 +120,7 @@ use lsp::{ LanguageServerId, LanguageServerName, }; +use language::BufferSnapshot; use movement::TextLayoutDetails; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, @@ -479,10 +480,7 @@ impl InlineCompletionMenuHint { #[derive(Clone, Debug)] enum InlineCompletionText { Move(SharedString), - Edit { - text: SharedString, - highlights: Vec<(Range, HighlightStyle)>, - }, + Edit(HighlightedEdits), } pub(crate) enum EditDisplayMode { @@ -494,7 +492,9 @@ pub(crate) enum EditDisplayMode { enum InlineCompletion { Edit { edits: Vec<(Range, String)>, + edit_preview: Option, display_mode: EditDisplayMode, + snapshot: BufferSnapshot, }, Move(Anchor), } @@ -4695,10 +4695,7 @@ impl Editor { selections.select_anchor_ranges([position..position]); }); } - InlineCompletion::Edit { - edits, - display_mode: _, - } => { + InlineCompletion::Edit { edits, .. } => { if let Some(provider) = self.inline_completion_provider() { provider.accept(cx); } @@ -4745,10 +4742,7 @@ impl Editor { selections.select_anchor_ranges([position..position]); }); } - InlineCompletion::Edit { - edits, - display_mode: _, - } => { + InlineCompletion::Edit { edits, .. } => { // Find an insertion that starts at the cursor position. let snapshot = self.buffer.read(cx).snapshot(cx); let cursor_offset = self.selections.newest::(cx).head(); @@ -4883,8 +4877,8 @@ impl Editor { let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - let completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; - let edits = completion + let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = inline_completion .edits .into_iter() .flat_map(|(range, new_text)| { @@ -4909,13 +4903,12 @@ impl Editor { let mut inlay_ids = Vec::new(); let invalidation_row_range; - let completion; - if cursor_row < edit_start_row { + let completion = if cursor_row < edit_start_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 { invalidation_row_range = edit_start_row..cursor_row; - completion = InlineCompletion::Move(first_edit_start); + InlineCompletion::Move(first_edit_start) } else { if edits .iter() @@ -4960,10 +4953,14 @@ impl Editor { EditDisplayMode::DiffPopover }; - completion = InlineCompletion::Edit { + let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?; + + InlineCompletion::Edit { edits, + edit_preview: inline_completion.edit_preview, display_mode, - }; + snapshot, + } }; let invalidation_range = multibuffer @@ -5006,19 +5003,26 @@ impl Editor { let text = match &self.active_inline_completion.as_ref()?.completion { InlineCompletion::Edit { edits, + edit_preview, 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) => { let target_point = target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot); let target_line = target_point.row + 1; - InlineCompletionText::Move( + Some(InlineCompletionText::Move( format!("Jump to edit in line {}", target_line).into(), - ) + )) } }; - Some(InlineCompletionMenuHint::Loaded { text }) + Some(InlineCompletionMenuHint::Loaded { text: text? }) } else if provider.is_refreshing(cx) { Some(InlineCompletionMenuHint::Loading) } else if provider.needs_terms_acceptance(cx) { @@ -14970,74 +14974,23 @@ pub fn diagnostic_block_renderer( } fn inline_completion_edit_text( - editor_snapshot: &EditorSnapshot, - edits: &Vec<(Range, String)>, + current_snapshot: &BufferSnapshot, + edits: &[(Range, String)], + edit_preview: &EditPreview, include_deletions: bool, cx: &WindowContext, -) -> InlineCompletionText { - let edit_start = edits - .first() - .unwrap() - .0 - .start - .to_display_point(editor_snapshot); +) -> Option { + let edits = edits + .iter() + .map(|(anchor, text)| { + ( + anchor.start.text_anchor..anchor.end.text_anchor, + text.clone(), + ) + }) + .collect::>(); - let mut text = String::new(); - 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, - } + Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)) } pub fn highlight_diagnostic_message( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 4196f60c5f..990fc3f061 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -14733,241 +14733,205 @@ async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppCon } #[gpui::test] -fn test_inline_completion_text(cx: &mut TestAppContext) { +async fn test_inline_completion_text(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Simple insertion - { - let window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("Hello, world!", cx); - Editor::new(EditorMode::Full, buffer, None, true, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6)); - let edits = vec![(edit_range, " beautiful".to_string())]; - - let InlineCompletionText::Edit { text, highlights } = - inline_completion_edit_text(&snapshot, &edits, false, cx) - 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(); - } + assert_highlighted_edits( + "Hello, world!", + vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())], + true, + cx, + |highlighted_edits, cx| { + assert_eq!(highlighted_edits.text, "Hello, beautiful world!"); + assert_eq!(highlighted_edits.highlights.len(), 1); + assert_eq!(highlighted_edits.highlights[0].0, 6..16); + assert_eq!( + highlighted_edits.highlights[0].1.background_color, + Some(cx.theme().status().created_background) + ); + }, + ) + .await; // Replacement - { - let window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("This is a test.", cx); - Editor::new(EditorMode::Full, buffer, None, true, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let edits = vec![( - snapshot.buffer_snapshot.anchor_after(Point::new(0, 0)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)), - "That".to_string(), - )]; - - 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(); - } + assert_highlighted_edits( + "This is a test.", + vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())], + false, + cx, + |highlighted_edits, cx| { + assert_eq!(highlighted_edits.text, "That is a test."); + assert_eq!(highlighted_edits.highlights.len(), 1); + assert_eq!(highlighted_edits.highlights[0].0, 0..4); + assert_eq!( + highlighted_edits.highlights[0].1.background_color, + Some(cx.theme().status().created_background) + ); + }, + ) + .await; // Multiple edits - { - let window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("Hello, world!", cx); - Editor::new(EditorMode::Full, buffer, None, true, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let edits = vec![ - ( - snapshot.buffer_snapshot.anchor_after(Point::new(0, 0)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)), - "Greetings".into(), - ), - ( - snapshot.buffer_snapshot.anchor_after(Point::new(0, 12)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)), - " and universe".into(), - ), - ]; - - let InlineCompletionText::Edit { text, highlights } = - 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(); - } + assert_highlighted_edits( + "Hello, world!", + vec![ + (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()), + (Point::new(0, 12)..Point::new(0, 12), " and universe".into()), + ], + false, + cx, + |highlighted_edits, cx| { + assert_eq!(highlighted_edits.text, "Greetings, world and universe!"); + assert_eq!(highlighted_edits.highlights.len(), 2); + assert_eq!(highlighted_edits.highlights[0].0, 0..9); + assert_eq!(highlighted_edits.highlights[1].0, 16..29); + assert_eq!( + highlighted_edits.highlights[0].1.background_color, + Some(cx.theme().status().created_background) + ); + assert_eq!( + highlighted_edits.highlights[1].1.background_color, + Some(cx.theme().status().created_background) + ); + }, + ) + .await; // Multiple lines with edits - { - let window = cx.add_window(|cx| { - let buffer = - MultiBuffer::build_simple("First line\nSecond line\nThird line\nFourth line", cx); - Editor::new(EditorMode::Full, buffer, None, true, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let edits = vec![ - ( - snapshot.buffer_snapshot.anchor_before(Point::new(1, 7)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)), - "modified".to_string(), - ), - ( - snapshot.buffer_snapshot.anchor_before(Point::new(2, 0)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)), - "New third line".to_string(), - ), - ( - snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)), - " updated".to_string(), - ), - ]; - - let InlineCompletionText::Edit { text, highlights } = - 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(); - } + assert_highlighted_edits( + "First line\nSecond line\nThird line\nFourth line", + vec![ + (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()), + ( + Point::new(2, 0)..Point::new(2, 10), + "New third line".to_string(), + ), + (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()), + ], + false, + cx, + |highlighted_edits, cx| { + assert_eq!( + highlighted_edits.text, + "Second modified\nNew third line\nFourth updated line" + ); + assert_eq!(highlighted_edits.highlights.len(), 3); + assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified" + assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line" + assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated" + for highlight in &highlighted_edits.highlights { + assert_eq!( + highlight.1.background_color, + Some(cx.theme().status().created_background) + ); + } + }, + ) + .await; } #[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, |_| {}); // Deletion - { - let window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("Hello, world!", cx); - Editor::new(EditorMode::Full, buffer, None, true, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 5)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 11)); - let edits = vec![(edit_range, "".to_string())]; - - let InlineCompletionText::Edit { text, highlights } = - inline_completion_edit_text(&snapshot, &edits, true, cx) - 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(); - } + assert_highlighted_edits( + "Hello, world!", + vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())], + true, + cx, + |highlighted_edits, cx| { + assert_eq!(highlighted_edits.text, "Hello, world!"); + assert_eq!(highlighted_edits.highlights.len(), 1); + assert_eq!(highlighted_edits.highlights[0].0, 5..11); + assert_eq!( + highlighted_edits.highlights[0].1.background_color, + Some(cx.theme().status().deleted_background) + ); + }, + ) + .await; // Insertion - { - let window = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("Hello, world!", cx); - Editor::new(EditorMode::Full, buffer, None, true, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); + assert_highlighted_edits( + "Hello, world!", + vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())], + true, + 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 - .update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6)) - ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6)); - let edits = vec![(edit_range, " digital".to_string())]; +async fn assert_highlighted_edits( + text: &str, + edits: Vec<(Range, String)>, + include_deletions: bool, + cx: &mut TestAppContext, + 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 } = - inline_completion_edit_text(&snapshot, &edits, true, cx) - else { - panic!("Failed to generate inline completion text"); - }; + let (buffer, snapshot) = window + .update(cx, |editor, cx| { + ( + editor.buffer().clone(), + editor.buffer().read(cx).snapshot(cx), + ) + }) + .unwrap(); - assert_eq!(text, "Hello, digital world!"); - assert_eq!(highlights.len(), 1); - assert_eq!(highlights[0].0, 6..14); - assert_eq!( - highlights[0].1.background_color, - Some(cx.theme().status().created_background) - ); - }) - .unwrap(); - } + let edits = edits + .into_iter() + .map(|(range, edit)| { + ( + snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end), + edit, + ) + }) + .collect::>(); + + let text_anchor_edits = edits + .clone() + .into_iter() + .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit)) + .collect::>(); + + 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] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6bde90e622..dac1e6d043 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3390,7 +3390,9 @@ impl EditorElement { } InlineCompletion::Edit { edits, + edit_preview, display_mode, + snapshot, } => { if self.editor.read(cx).has_active_completions_menu() { return None; @@ -3442,13 +3444,11 @@ impl EditorElement { EditDisplayMode::DiffPopover => {} } - let crate::InlineCompletionText::Edit { text, highlights } = - crate::inline_completion_edit_text(editor_snapshot, edits, false, cx) - else { - return None; - }; + let highlighted_edits = edit_preview.as_ref().and_then(|edit_preview| { + crate::inline_completion_edit_text(&snapshot, edits, edit_preview, false, cx) + })?; - let line_count = text.lines().count() + 1; + let line_count = highlighted_edits.text.lines().count() + 1; let longest_row = editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); @@ -3466,15 +3466,14 @@ impl EditorElement { .width }; - let styled_text = - gpui::StyledText::new(text.clone()).with_highlights(&style.text, highlights); + let styled_text = gpui::StyledText::new(highlighted_edits.text.clone()) + .with_highlights(&style.text, highlighted_edits.highlights); let mut element = div() .bg(cx.theme().colors().editor_background) .border_1() .border_color(cx.theme().colors().border) .rounded_md() - .px_1() .child(styled_text) .into_any(); diff --git a/crates/editor/src/inline_completion_tests.rs b/crates/editor/src/inline_completion_tests.rs index 676b154aed..9e7ca3d450 100644 --- a/crates/editor/src/inline_completion_tests.rs +++ b/crates/editor/src/inline_completion_tests.rs @@ -333,6 +333,7 @@ fn propose_edits( provider.update(cx, |provider, _| { provider.set_inline_completion(Some(inline_completion::InlineCompletion { edits: edits.collect(), + edit_preview: None, })) }) }); diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/inline_completion/src/inline_completion.rs index 0ff4a593ef..be71dcf76e 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -15,6 +15,7 @@ pub enum Direction { #[derive(Clone)] pub struct InlineCompletion { pub edits: Vec<(Range, String)>, + pub edit_preview: Option, } pub trait InlineCompletionProvider: 'static + Sized { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 57557e5a36..7ac1263dec 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -26,7 +26,7 @@ use fs::MTime; use futures::channel::oneshot; use gpui::{ AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext, - Pixels, Task, TaskLabel, WindowContext, + Pixels, SharedString, Task, TaskLabel, WindowContext, }; use lsp::LanguageServerId; use parking_lot::Mutex; @@ -65,7 +65,7 @@ pub use text::{ Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16, Transaction, TransactionId, Unclipped, }; -use theme::SyntaxTheme; +use theme::{ActiveTheme as _, SyntaxTheme}; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; 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, HighlightStyle)>, +} + +impl EditPreview { + pub fn highlight_edits( + &self, + current_snapshot: &BufferSnapshot, + edits: &[(Range, 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, + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + override_style: Option, + 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) -> 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, String)], + snapshot: &BufferSnapshot, + ) -> Option> { + 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 { /// Create a new buffer with the given base text. pub fn local>(base_text: T, cx: &ModelContext) -> Self { @@ -825,6 +981,33 @@ impl Buffer { }) } + pub fn preview_edits( + &self, + edits: Arc<[(Range, String)]>, + cx: &AppContext, + ) -> Task { + 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 /// given `ranges` to its base buffer. /// diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index a90651d0d7..e0af4998a8 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -2718,6 +2718,148 @@ fn test_undo_after_merge_into_base(cx: &mut TestAppContext) { 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, + cx: &mut TestAppContext, + edits: impl IntoIterator, &'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::>() + }); + 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, + edits: impl IntoIterator, &'static str)>, + cx: &mut TestAppContext, + ) -> Arc<[(Range, 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::>() + }) + .into() + } +} + #[gpui::test(iterations = 100)] fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let min_peers = env::var("MIN_PEERS") diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index f51eeb9688..2a0c7eaa1c 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -263,7 +263,7 @@ impl SyntaxSnapshot { self.layers.is_empty() } - fn interpolate(&mut self, text: &BufferSnapshot) { + pub fn interpolate(&mut self, text: &BufferSnapshot) { let edits = text .anchored_edits_since::<(usize, Point)>(&self.interpolated_version) .collect::>(); diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index e9c2bfb37b..46468754ca 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -90,7 +90,10 @@ fn completion_from_diff( edits.push((edit_range, edit_text)); } - InlineCompletion { edits } + InlineCompletion { + edits, + edit_preview: None, + } } impl InlineCompletionProvider for SupermavenCompletionProvider { diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 32dec5d6f0..65cb779591 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -15,8 +15,8 @@ use gpui::{ }; use http_client::{HttpClient, Method}; use language::{ - language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt, - Point, ToOffset, ToPoint, + language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview, + OffsetRangeExt, Point, ToOffset, ToPoint, }; use language_models::LlmApiToken; use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME}; @@ -77,6 +77,7 @@ pub struct InlineCompletion { cursor_offset: usize, edits: Arc<[(Range, String)]>, snapshot: BufferSnapshot, + edit_preview: EditPreview, input_outline: Arc, input_events: Arc, input_excerpt: Arc, @@ -92,56 +93,64 @@ impl InlineCompletion { } fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option, String)>> { - let mut edits = Vec::new(); + interpolate(&self.snapshot, new_snapshot, self.edits.clone()) + } +} - let mut user_edits = new_snapshot - .edits_since::(&self.snapshot.version) - .peekable(); - for (model_old_range, model_new_text) in self.edits.iter() { - let model_offset_range = model_old_range.to_offset(&self.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; - } +fn interpolate( + old_snapshot: &BufferSnapshot, + new_snapshot: &BufferSnapshot, + current_edits: Arc<[(Range, String)]>, +) -> Option, String)>> { + let mut edits = Vec::new(); + + let mut user_edits = new_snapshot + .edits_since::(&old_snapshot.version) + .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 user_edit.old.start > model_offset_range.end { - edits.push((model_old_range.clone(), model_new_text.clone())); - } else if user_edit.old == model_offset_range { - let user_new_text = new_snapshot - .text_for_range(user_edit.new.clone()) - .collect::(); + 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())); + } else if user_edit.old == model_offset_range { + let user_new_text = new_snapshot + .text_for_range(user_edit.new.clone()) + .collect::(); - 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(), - )); - } - - user_edits.next(); - } else { - return None; + 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(), + )); } + + user_edits.next(); } else { return None; } } else { - edits.push((model_old_range.clone(), model_new_text.clone())); + return None; } - } - - if edits.is_empty() { - None } 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 { @@ -302,7 +311,8 @@ impl Zeta { F: FnOnce(Arc, LlmApiToken, PredictEditsParams) -> R + 'static, R: Future> + 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 offset = point.to_offset(&snapshot); let excerpt_range = excerpt_range_for_position(point, &snapshot); @@ -356,6 +366,7 @@ impl Zeta { Self::process_completion_response( output_excerpt, + buffer, &snapshot, excerpt_range, offset, @@ -581,6 +592,7 @@ and then another #[allow(clippy::too_many_arguments)] fn process_completion_response( output_excerpt: String, + buffer: Model, snapshot: &BufferSnapshot, excerpt_range: Range, cursor_offset: usize, @@ -592,70 +604,110 @@ and then another cx: &AsyncAppContext, ) -> Task> { let snapshot = snapshot.clone(); - cx.background_executor().spawn(async move { - let content = output_excerpt.replace(CURSOR_MARKER, ""); + cx.spawn(|cx| async move { + let output_excerpt: Arc = output_excerpt.into(); - let start_markers = content - .match_indices(EDITABLE_REGION_START_MARKER) - .collect::>(); - anyhow::ensure!( - start_markers.len() == 1, - "expected exactly one start marker, found {}", - start_markers.len() - ); + let edits: Arc<[(Range, String)]> = cx + .background_executor() + .spawn({ + let output_excerpt = output_excerpt.clone(); + let excerpt_range = excerpt_range.clone(); + let snapshot = snapshot.clone(); + async move { Self::parse_edits(output_excerpt, excerpt_range, &snapshot) } + }) + .await? + .into(); - let end_markers = content - .match_indices(EDITABLE_REGION_END_MARKER) - .collect::>(); - anyhow::ensure!( - end_markers.len() == 1, - "expected exactly one end marker, found {}", - end_markers.len() - ); + let (edits, snapshot, edit_preview) = buffer.read_with(&cx, { + let edits = edits.clone(); + |buffer, cx| { + let new_snapshot = buffer.snapshot(); + let edits: Arc<[(Range, String)]> = + interpolate(&snapshot, &new_snapshot, edits) + .context("Interpolated edits are empty")? + .into(); - let sof_markers = content - .match_indices(START_OF_FILE_MARKER) - .collect::>(); - anyhow::ensure!( - sof_markers.len() <= 1, - "expected at most one start-of-file marker, found {}", - sof_markers.len() - ); + anyhow::Ok((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx))) + } + })??; - 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::(); - - let edits = Self::compute_edits(old_text, new_text, excerpt_range.start, &snapshot); + let edit_preview = edit_preview.await; Ok(InlineCompletion { id: InlineCompletionId::new(), path, excerpt_range, cursor_offset, - edits: edits.into(), - snapshot: snapshot.clone(), + edits, + edit_preview, + snapshot, input_outline: input_outline.into(), input_events: input_events.into(), input_excerpt: input_excerpt.into(), - output_excerpt: output_excerpt.into(), + output_excerpt, request_sent_at, response_received_at: Instant::now(), }) }) } + fn parse_edits( + output_excerpt: Arc, + excerpt_range: Range, + snapshot: &BufferSnapshot, + ) -> Result, String)>> { + let content = output_excerpt.replace(CURSOR_MARKER, ""); + + let start_markers = content + .match_indices(EDITABLE_REGION_START_MARKER) + .collect::>(); + 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::>(); + 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::>(); + 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::(); + + Ok(Self::compute_edits( + old_text, + new_text, + excerpt_range.start, + &snapshot, + )) + } + pub fn compute_edits( old_text: String, new_text: &str, @@ -1206,6 +1258,7 @@ 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()), }) } } @@ -1224,17 +1277,26 @@ mod tests { use super::*; #[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 completion = InlineCompletion { - edits: to_completion_edits( + let edits: Arc<[(Range, String)]> = cx.update(|cx| { + to_completion_edits( [(2..5, "REM".to_string()), (9..11, "".to_string())], &buffer, 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(), - snapshot: buffer.read(cx).snapshot(), + snapshot: cx.read(|cx| buffer.read(cx).snapshot()), id: InlineCompletionId::new(), excerpt_range: 0..0, cursor_offset: 0, @@ -1246,87 +1308,89 @@ mod tests { response_received_at: Instant::now(), }; - assert_eq!( - from_completion_edits( - &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), - &buffer, - cx - ), - vec![(2..5, "REM".to_string()), (9..11, "".to_string())] - ); + cx.update(|cx| { + assert_eq!( + from_completion_edits( + &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(2..5, "REM".to_string()), (9..11, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx)); - assert_eq!( - from_completion_edits( - &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), - &buffer, - cx - ), - vec![(2..2, "REM".to_string()), (6..8, "".to_string())] - ); + buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(2..2, "REM".to_string()), (6..8, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - assert_eq!( - from_completion_edits( - &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), - &buffer, - cx - ), - vec![(2..5, "REM".to_string()), (9..11, "".to_string())] - ); + buffer.update(cx, |buffer, cx| buffer.undo(cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(2..5, "REM".to_string()), (9..11, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx)); - assert_eq!( - from_completion_edits( - &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), - &buffer, - cx - ), - vec![(3..3, "EM".to_string()), (7..9, "".to_string())] - ); + buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(3..3, "EM".to_string()), (7..9, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx)); - assert_eq!( - from_completion_edits( - &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), - &buffer, - cx - ), - vec![(4..4, "M".to_string()), (8..10, "".to_string())] - ); + buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(4..4, "M".to_string()), (8..10, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx)); - assert_eq!( - from_completion_edits( - &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), - &buffer, - cx - ), - vec![(9..11, "".to_string())] - ); + buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(9..11, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx)); - assert_eq!( - from_completion_edits( - &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), - &buffer, - cx - ), - vec![(4..4, "M".to_string()), (8..10, "".to_string())] - ); + buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(4..4, "M".to_string()), (8..10, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx)); - assert_eq!( - from_completion_edits( - &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), - &buffer, - cx - ), - vec![(4..4, "M".to_string())] - ); + buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(&buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(4..4, "M".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx)); - assert_eq!(completion.interpolate(&buffer.read(cx).snapshot()), None); + buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx)); + assert_eq!(completion.interpolate(&buffer.read(cx).snapshot()), None); + }) } #[gpui::test]