From 5e449c84fec8638aa0532841221421998b8c6443 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Thu, 30 Jan 2025 11:53:51 +0100 Subject: [PATCH] edit prediction: Add syntax highlighting for diff popover (#23899) Co-Authored-by: Antonio Release Notes: - N/A --------- Co-authored-by: Antonio --- .../src/copilot_completion_provider.rs | 1 + crates/editor/src/code_context_menus.rs | 6 +- crates/editor/src/editor.rs | 136 ++---- crates/editor/src/editor_tests.rs | 394 ++++++++---------- crates/editor/src/element.rs | 17 +- crates/editor/src/inline_completion_tests.rs | 1 + .../src/inline_completion.rs | 1 + crates/language/src/buffer.rs | 211 +++++++++- crates/language/src/buffer_tests.rs | 140 ++++++- crates/language/src/syntax_map.rs | 2 +- .../src/supermaven_completion_provider.rs | 5 +- crates/zeta/src/zeta.rs | 367 +++++++++------- 12 files changed, 802 insertions(+), 479 deletions(-) diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index ebcc72c181..0dc03e4037 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 50f3eaa0bf..7238fc65fe 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -728,13 +728,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 a556327114..b3df175f14 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -96,8 +96,9 @@ use itertools::Itertools; use language::{ language_settings::{self, all_language_settings, language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, - CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt, - Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, + CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize, + Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, + TreeSitterOptions, }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; @@ -116,6 +117,7 @@ use lsp::{ LanguageServerId, LanguageServerName, }; +use language::BufferSnapshot; use movement::TextLayoutDetails; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo, @@ -486,10 +488,7 @@ impl InlineCompletionMenuHint { #[derive(Clone, Debug)] enum InlineCompletionText { Move(SharedString), - Edit { - text: SharedString, - highlights: Vec<(Range, HighlightStyle)>, - }, + Edit(HighlightedEdits), } pub(crate) enum EditDisplayMode { @@ -501,7 +500,9 @@ pub(crate) enum EditDisplayMode { enum InlineCompletion { Edit { edits: Vec<(Range, String)>, + edit_preview: Option, display_mode: EditDisplayMode, + snapshot: BufferSnapshot, }, Move(Anchor), } @@ -4847,10 +4848,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); } @@ -4898,10 +4896,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(); @@ -5040,8 +5035,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)| { @@ -5066,13 +5061,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() @@ -5117,10 +5111,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 @@ -5164,19 +5162,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) { @@ -15829,74 +15834,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: &App, -) -> 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 a3b0937fc6..d35866afed 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -15257,241 +15257,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(|window, cx| { - let buffer = MultiBuffer::build_simple("Hello, world!", cx); - Editor::new(EditorMode::Full, buffer, None, true, window, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, 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(|window, cx| { - let buffer = MultiBuffer::build_simple("This is a test.", cx); - Editor::new(EditorMode::Full, buffer, None, true, window, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, 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(|window, cx| { - let buffer = MultiBuffer::build_simple("Hello, world!", cx); - Editor::new(EditorMode::Full, buffer, None, true, window, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, 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(|window, cx| { - let buffer = - MultiBuffer::build_simple("First line\nSecond line\nThird line\nFourth line", cx); - Editor::new(EditorMode::Full, buffer, None, true, window, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, 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(|window, cx| { - let buffer = MultiBuffer::build_simple("Hello, world!", cx); - Editor::new(EditorMode::Full, buffer, None, true, window, cx) - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - - window - .update(cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, 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(|window, cx| { - let buffer = MultiBuffer::build_simple("Hello, world!", cx); - Editor::new(EditorMode::Full, buffer, None, true, window, 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, window, cx| { - let snapshot = editor.snapshot(window, 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, &App), +) { + let window = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(text, cx); + Editor::new(EditorMode::Full, buffer, None, true, window, 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, _window, 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, |_, _window, cx| { + buffer + .read(cx) + .as_singleton() + .unwrap() + .read(cx) + .preview_edits(text_anchor_edits.into(), cx) + }) + .unwrap() + .await; + + cx.update(|_window, 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 b25063f313..93eadc5393 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3468,7 +3468,9 @@ impl EditorElement { } InlineCompletion::Edit { edits, + edit_preview, display_mode, + snapshot, } => { if self.editor.read(cx).has_active_completions_menu() { return None; @@ -3521,13 +3523,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); @@ -3546,15 +3546,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 4d0d8de8dd..9ba036a939 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 db45f7778b..5b99dcbb79 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 2ed2523778..eba9b06b38 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -25,8 +25,8 @@ use collections::HashMap; use fs::MTime; use futures::channel::oneshot; use gpui::{ - AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels, Task, - TaskLabel, Window, + AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels, + SharedString, Task, TaskLabel, Window, }; 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}; @@ -588,6 +588,183 @@ pub struct Runnable { pub buffer: BufferId, } +#[derive(Clone)] +pub struct EditPreview { + old_snapshot: text::BufferSnapshot, + 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: &App, + ) -> HighlightedEdits { + let Some(visible_range_in_preview_snapshot) = self.compute_visible_range(edits) else { + return HighlightedEdits::default(); + }; + + let mut text = String::new(); + let mut highlights = Vec::new(); + + let mut offset_in_preview_snapshot = visible_range_in_preview_snapshot.start; + + let insertion_highlight_style = HighlightStyle { + background_color: Some(cx.theme().status().created_background), + ..Default::default() + }; + let deletion_highlight_style = HighlightStyle { + background_color: Some(cx.theme().status().deleted_background), + ..Default::default() + }; + + for (range, edit_text) in edits { + let edit_new_end_in_preview_snapshot = range + .end + .bias_right(&self.old_snapshot) + .to_offset(&self.applied_edits_snapshot); + let edit_start_in_preview_snapshot = edit_new_end_in_preview_snapshot - edit_text.len(); + + let unchanged_range_in_preview_snapshot = + offset_in_preview_snapshot..edit_start_in_preview_snapshot; + if !unchanged_range_in_preview_snapshot.is_empty() { + Self::highlight_text( + unchanged_range_in_preview_snapshot.clone(), + &mut text, + &mut highlights, + None, + &self.applied_edits_snapshot, + &self.syntax_snapshot, + cx, + ); + } + + let range_in_current_snapshot = range.to_offset(current_snapshot); + if include_deletions && !range_in_current_snapshot.is_empty() { + Self::highlight_text( + range_in_current_snapshot.clone(), + &mut text, + &mut highlights, + Some(deletion_highlight_style), + ¤t_snapshot.text, + ¤t_snapshot.syntax, + cx, + ); + } + + if !edit_text.is_empty() { + Self::highlight_text( + edit_start_in_preview_snapshot..edit_new_end_in_preview_snapshot, + &mut text, + &mut highlights, + Some(insertion_highlight_style), + &self.applied_edits_snapshot, + &self.syntax_snapshot, + cx, + ); + } + + offset_in_preview_snapshot = edit_new_end_in_preview_snapshot; + } + + Self::highlight_text( + offset_in_preview_snapshot..visible_range_in_preview_snapshot.end, + &mut text, + &mut highlights, + None, + &self.applied_edits_snapshot, + &self.syntax_snapshot, + cx, + ); + + HighlightedEdits { + text: text.into(), + highlights, + } + } + + fn highlight_text( + range: Range, + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + override_style: Option, + snapshot: &text::BufferSnapshot, + syntax_snapshot: &SyntaxSnapshot, + cx: &App, + ) { + for chunk in Self::highlighted_chunks(range, snapshot, syntax_snapshot) { + 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<'a>( + range: Range, + snapshot: &'a text::BufferSnapshot, + syntax_snapshot: &'a SyntaxSnapshot, + ) -> BufferChunks<'a> { + let captures = syntax_snapshot.captures(range.clone(), snapshot, |grammar| { + grammar.highlights_query.as_ref() + }); + + let highlight_maps = captures + .grammars() + .iter() + .map(|grammar| grammar.highlight_map()) + .collect(); + + BufferChunks::new( + snapshot.as_rope(), + range, + Some((captures, highlight_maps)), + false, + None, + ) + } + + fn compute_visible_range(&self, edits: &[(Range, String)]) -> Option> { + let (first, _) = edits.first()?; + let (last, _) = edits.last()?; + + let start = first + .start + .bias_left(&self.old_snapshot) + .to_point(&self.applied_edits_snapshot); + let end = last + .end + .bias_right(&self.old_snapshot) + .to_point(&self.applied_edits_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, self.applied_edits_snapshot.line_len(end.row)); + + Some(range.to_offset(&self.applied_edits_snapshot)) + } +} + impl Buffer { /// Create a new buffer with the given base text. pub fn local>(base_text: T, cx: &Context) -> Self { @@ -840,6 +1017,34 @@ impl Buffer { }) } + pub fn preview_edits( + &self, + edits: Arc<[(Range, String)]>, + cx: &App, + ) -> Task { + let registry = self.language_registry(); + let language = self.language().cloned(); + let old_snapshot = self.text.snapshot(); + 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 { + old_snapshot, + 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 7947bc58dc..2beae53e2f 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -6,8 +6,8 @@ use crate::Buffer; use clock::ReplicaId; use collections::BTreeMap; use futures::FutureExt as _; -use gpui::TestAppContext; use gpui::{App, AppContext as _, BorrowAppContext, Entity}; +use gpui::{HighlightStyle, TestAppContext}; use indoc::indoc; use proto::deserialize_operation; use rand::prelude::*; @@ -23,6 +23,7 @@ use syntax_map::TreeSitterOptions; use text::network::Network; use text::{BufferId, LineEnding}; use text::{Point, ToPoint}; +use theme::ActiveTheme; use unindent::Unindent as _; use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; @@ -2627,6 +2628,143 @@ 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 insertion_style = HighlightStyle { + background_color: Some(cx.read(|cx| cx.theme().status().created_background)), + ..Default::default() + }; + let deletion_style = HighlightStyle { + background_color: Some(cx.read(|cx| cx.theme().status().deleted_background)), + ..Default::default() + }; + + // no edits + assert_preview_edits( + indoc! {" + fn test_empty() -> bool { + false + }" + }, + vec![], + true, + cx, + |hl| { + assert!(hl.text.is_empty()); + assert!(hl.highlights.is_empty()); + }, + ) + .await; + + // only insertions + assert_preview_edits( + indoc! {" + fn calculate_area(: f64) -> f64 { + std::f64::consts::PI * .powi(2) + }" + }, + vec![ + (Point::new(0, 18)..Point::new(0, 18), "radius"), + (Point::new(1, 27)..Point::new(1, 27), "radius"), + ], + true, + cx, + |hl| { + assert_eq!( + hl.text, + indoc! {" + fn calculate_area(radius: f64) -> f64 { + std::f64::consts::PI * radius.powi(2)" + } + ); + + assert_eq!(hl.highlights.len(), 2); + assert_eq!(hl.highlights[0], ((18..24), insertion_style)); + assert_eq!(hl.highlights[1], ((67..73), insertion_style)); + }, + ) + .await; + + // insertions & deletions + assert_preview_edits( + indoc! {" + struct Person { + first_name: String, + } + + impl Person { + fn first_name(&self) -> &String { + &self.first_name + } + }" + }, + vec![ + (Point::new(1, 4)..Point::new(1, 9), "last"), + (Point::new(5, 7)..Point::new(5, 12), "last"), + (Point::new(6, 14)..Point::new(6, 19), "last"), + ], + true, + cx, + |hl| { + assert_eq!( + hl.text, + indoc! {" + firstlast_name: String, + } + + impl Person { + fn firstlast_name(&self) -> &String { + &self.firstlast_name" + } + ); + + assert_eq!(hl.highlights.len(), 6); + assert_eq!(hl.highlights[0], ((4..9), deletion_style)); + assert_eq!(hl.highlights[1], ((9..13), insertion_style)); + assert_eq!(hl.highlights[2], ((52..57), deletion_style)); + assert_eq!(hl.highlights[3], ((57..61), insertion_style)); + assert_eq!(hl.highlights[4], ((101..106), deletion_style)); + assert_eq!(hl.highlights[5], ((106..110), insertion_style)); + }, + ) + .await; + + async fn assert_preview_edits( + text: &str, + edits: Vec<(Range, &str)>, + include_deletions: bool, + cx: &mut TestAppContext, + assert_fn: impl Fn(HighlightedEdits), + ) { + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + 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; + let highlighted_edits = cx.read(|cx| { + edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, include_deletions, cx) + }); + assert_fn(highlighted_edits); + } +} + #[gpui::test(iterations = 100)] fn test_random_collaboration(cx: &mut App, 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 c1666c4c9a..a46b490e73 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 0eefc78f66..074be523bb 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}; @@ -101,6 +101,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, @@ -116,55 +117,57 @@ 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(); - 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; - } - } else { - return None; - } - } else { + let mut model_edits = current_edits.into_iter().peekable(); + for user_edit in new_snapshot.edits_since::(&old_snapshot.version) { + while let Some((model_old_range, _)) = model_edits.peek() { + let model_old_range = model_old_range.to_offset(old_snapshot); + if model_old_range.end < user_edit.old.start { + let (model_old_range, model_new_text) = model_edits.next().unwrap(); edits.push((model_old_range.clone(), model_new_text.clone())); + } else { + break; } } - if edits.is_empty() { - None - } else { - Some(edits) + if let Some((model_old_range, model_new_text)) = model_edits.peek() { + let model_old_offset_range = model_old_range.to_offset(old_snapshot); + if user_edit.old == model_old_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() { + let anchor = old_snapshot.anchor_after(user_edit.old.end); + edits.push((anchor..anchor, model_suffix.to_string())); + } + + model_edits.next(); + continue; + } + } } + + return None; + } + + edits.extend(model_edits.cloned()); + + if edits.is_empty() { + None + } else { + Some(edits) } } @@ -324,7 +327,8 @@ impl Zeta { F: FnOnce(Arc, LlmApiToken, bool, 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 cursor_point = cursor.to_point(&snapshot); let cursor_offset = cursor_point.to_offset(&snapshot); let events = self.events.clone(); @@ -375,6 +379,7 @@ impl Zeta { Self::process_completion_response( output_excerpt, + buffer, &snapshot, excerpt_range, cursor_offset, @@ -606,6 +611,7 @@ and then another #[allow(clippy::too_many_arguments)] fn process_completion_response( output_excerpt: String, + buffer: Entity, snapshot: &BufferSnapshot, excerpt_range: Range, cursor_offset: usize, @@ -617,52 +623,110 @@ and then another cx: &AsyncApp, ) -> 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 codefence_start = start_markers[0].0; - let content = &content[codefence_start..]; + let Some((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)?.into(); + Some((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx))) + } + })? + else { + return anyhow::Ok(None); + }; - 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(Some(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, @@ -721,10 +785,13 @@ and then another old_range.end = old_range.end.saturating_sub(suffix_len); let new_text = new_text[prefix_len..new_text.len() - suffix_len].to_string(); - ( - snapshot.anchor_after(old_range.start)..snapshot.anchor_before(old_range.end), - new_text, - ) + let range = if old_range.is_empty() { + let anchor = snapshot.anchor_after(old_range.start); + anchor..anchor + } else { + snapshot.anchor_after(old_range.start)..snapshot.anchor_before(old_range.end) + }; + (range, new_text) }) .collect() } @@ -1434,6 +1501,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()), }) } } @@ -1452,18 +1520,24 @@ mod tests { use super::*; #[gpui::test] - fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) { + async fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) { let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx)); + let edits: Arc<[(Range, String)]> = cx.update(|cx| { + to_completion_edits( + [(2..5, "REM".to_string()), (9..11, "".to_string())], + &buffer, + cx, + ) + .into() + }); + + let edit_preview = cx + .read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx)) + .await; + let completion = InlineCompletion { - edits: cx - .read(|cx| { - to_completion_edits( - [(2..5, "REM".to_string()), (9..11, "".to_string())], - &buffer, - cx, - ) - }) - .into(), + edits, + edit_preview, path: Path::new("").into(), snapshot: cx.read(|cx| buffer.read(cx).snapshot()), id: InlineCompletionId::new(), @@ -1477,106 +1551,89 @@ mod tests { response_received_at: Instant::now(), }; - assert_eq!( - cx.read(|cx| { + 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())] - ); + cx + ), + vec![(2..5, "REM".to_string()), (9..11, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx)); - assert_eq!( - cx.read(|cx| { + 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())] - ); + cx + ), + vec![(2..2, "REM".to_string()), (6..8, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - assert_eq!( - cx.read(|cx| { + 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())] - ); + 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!( - cx.read(|cx| { + 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())] - ); + 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!( - cx.read(|cx| { + 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())] - ); + 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!( - cx.read(|cx| { + 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())] - ); + cx + ), + vec![(9..11, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx)); - assert_eq!( - cx.read(|cx| { + 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())] - ); + cx + ), + vec![(4..4, "M".to_string()), (8..10, "".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx)); - assert_eq!( - cx.read(|cx| { + 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())] - ); + cx + ), + vec![(4..4, "M".to_string())] + ); - buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx)); - assert_eq!( - cx.read(|cx| 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]