edit prediction: Add syntax highlighting for diff popover (#23899)

Bennet Bo Fenner and Antonio created

Co-Authored-by: Antonio <antonio@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>

Change summary

crates/copilot/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 
crates/inline_completion/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 
crates/supermaven/src/supermaven_completion_provider.rs |   5 
crates/zeta/src/zeta.rs                                 | 369 +++++----
12 files changed, 803 insertions(+), 480 deletions(-)

Detailed changes

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 {

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()),
                 }

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<usize>, HighlightStyle)>,
-    },
+    Edit(HighlightedEdits),
 }
 
 pub(crate) enum EditDisplayMode {
@@ -501,7 +500,9 @@ pub(crate) enum EditDisplayMode {
 enum InlineCompletion {
     Edit {
         edits: Vec<(Range<Anchor>, String)>,
+        edit_preview: Option<EditPreview>,
         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::<usize>(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<Anchor>, String)>,
+    current_snapshot: &BufferSnapshot,
+    edits: &[(Range<Anchor>, String)],
+    edit_preview: &EditPreview,
     include_deletions: bool,
     cx: &App,
-) -> InlineCompletionText {
-    let edit_start = edits
-        .first()
-        .unwrap()
-        .0
-        .start
-        .to_display_point(editor_snapshot);
-
-    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),
-    );
+) -> Option<HighlightedEdits> {
+    let edits = edits
+        .iter()
+        .map(|(anchor, text)| {
+            (
+                anchor.start.text_anchor..anchor.end.text_anchor,
+                text.clone(),
+            )
+        })
+        .collect::<Vec<_>>();
 
-    InlineCompletionText::Edit {
-        text: text.into(),
-        highlights,
-    }
+    Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx))
 }
 
 pub fn highlight_diagnostic_message(

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_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;
 
-                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)
-                );
+    // Multiple lines with edits
+    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!(
-                    highlights[1].1.background_color,
+                    highlight.1.background_color,
                     Some(cx.theme().status().created_background)
                 );
-            })
-            .unwrap();
-    }
-
-    // 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();
-    }
+            }
+        },
+    )
+    .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);
-
-        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())];
-
-                let InlineCompletionText::Edit { text, highlights } =
-                    inline_completion_edit_text(&snapshot, &edits, true, cx)
-                else {
-                    panic!("Failed to generate inline completion text");
-                };
+    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;
+}
 
-                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();
-    }
+async fn assert_highlighted_edits(
+    text: &str,
+    edits: Vec<(Range<Point>, 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 (buffer, snapshot) = window
+        .update(cx, |editor, _window, cx| {
+            (
+                editor.buffer().clone(),
+                editor.buffer().read(cx).snapshot(cx),
+            )
+        })
+        .unwrap();
+
+    let edits = edits
+        .into_iter()
+        .map(|(range, edit)| {
+            (
+                snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
+                edit,
+            )
+        })
+        .collect::<Vec<_>>();
+
+    let text_anchor_edits = edits
+        .clone()
+        .into_iter()
+        .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
+        .collect::<Vec<_>>();
+
+    let edit_preview = window
+        .update(cx, |_, _window, cx| {
+            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]

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();
 

crates/editor/src/inline_completion_tests.rs 🔗

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

crates/inline_completion/src/inline_completion.rs 🔗

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

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<usize>, HighlightStyle)>,
+}
+
+impl EditPreview {
+    pub fn highlight_edits(
+        &self,
+        current_snapshot: &BufferSnapshot,
+        edits: &[(Range<Anchor>, 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),
+                    &current_snapshot.text,
+                    &current_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<usize>,
+        text: &mut String,
+        highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
+        override_style: Option<HighlightStyle>,
+        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<usize>,
+        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<Anchor>, String)]) -> Option<Range<usize>> {
+        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<T: Into<String>>(base_text: T, cx: &Context<Self>) -> Self {
@@ -840,6 +1017,34 @@ impl Buffer {
         })
     }
 
+    pub fn preview_edits(
+        &self,
+        edits: Arc<[(Range<Anchor>, String)]>,
+        cx: &App,
+    ) -> Task<EditPreview> {
+        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.
     ///

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<Point>, &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::<Vec<_>>()
+        });
+        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")

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::<Vec<_>>();

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 {

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<Anchor>, String)]>,
     snapshot: BufferSnapshot,
+    edit_preview: EditPreview,
     input_outline: Arc<str>,
     input_events: Arc<str>,
     input_excerpt: Arc<str>,
@@ -116,55 +117,57 @@ impl InlineCompletion {
     }
 
     fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
-        let mut edits = Vec::new();
-
-        let mut user_edits = new_snapshot
-            .edits_since::<usize>(&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;
-                }
-            }
+        interpolate(&self.snapshot, new_snapshot, self.edits.clone())
+    }
+}
 
-            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::<String>();
-
-                    if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
-                        if !model_suffix.is_empty() {
-                            edits.push((
-                                new_snapshot.anchor_after(user_edit.new.end)
-                                    ..new_snapshot.anchor_before(user_edit.new.end),
-                                model_suffix.into(),
-                            ));
-                        }
+fn interpolate(
+    old_snapshot: &BufferSnapshot,
+    new_snapshot: &BufferSnapshot,
+    current_edits: Arc<[(Range<Anchor>, String)]>,
+) -> Option<Vec<(Range<Anchor>, String)>> {
+    let mut edits = Vec::new();
+
+    let mut model_edits = current_edits.into_iter().peekable();
+    for user_edit in new_snapshot.edits_since::<usize>(&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;
+            }
+        }
 
-                        user_edits.next();
-                    } else {
-                        return None;
+        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::<String>();
+
+                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()));
                     }
-                } else {
-                    return None;
+
+                    model_edits.next();
+                    continue;
                 }
-            } else {
-                edits.push((model_old_range.clone(), model_new_text.clone()));
             }
         }
 
-        if edits.is_empty() {
-            None
-        } else {
-            Some(edits)
-        }
+        return None;
+    }
+
+    edits.extend(model_edits.cloned());
+
+    if edits.is_empty() {
+        None
+    } else {
+        Some(edits)
     }
 }
 
@@ -324,7 +327,8 @@ impl Zeta {
         F: FnOnce(Arc<Client>, LlmApiToken, bool, PredictEditsParams) -> R + 'static,
         R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
     {
-        let snapshot = self.report_changes_for_buffer(buffer, cx);
+        let buffer = buffer.clone();
+        let snapshot = self.report_changes_for_buffer(&buffer, cx);
         let 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<Buffer>,
         snapshot: &BufferSnapshot,
         excerpt_range: Range<usize>,
         cursor_offset: usize,
@@ -617,52 +623,110 @@ and then another
         cx: &AsyncApp,
     ) -> Task<Result<Option<InlineCompletion>>> {
         let snapshot = snapshot.clone();
-        cx.background_executor().spawn(async move {
-            let content = output_excerpt.replace(CURSOR_MARKER, "");
-
-            let start_markers = content
-                .match_indices(EDITABLE_REGION_START_MARKER)
-                .collect::<Vec<_>>();
-            anyhow::ensure!(
-                start_markers.len() == 1,
-                "expected exactly one start marker, found {}",
-                start_markers.len()
-            );
-
-            let 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];
+        cx.spawn(|cx| async move {
+            let output_excerpt: Arc<str> = output_excerpt.into();
 
-            let old_text = snapshot
-                .text_for_range(excerpt_range.clone())
-                .collect::<String>();
+            let edits: Arc<[(Range<Anchor>, 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 Some((edits, snapshot, edit_preview)) = buffer.read_with(&cx, {
+                let edits = edits.clone();
+                |buffer, cx| {
+                    let new_snapshot = buffer.snapshot();
+                    let edits: Arc<[(Range<Anchor>, String)]> =
+                        interpolate(&snapshot, &new_snapshot, edits)?.into();
+                    Some((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
+                }
+            })?
+            else {
+                return anyhow::Ok(None);
+            };
 
-            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<str>,
+        excerpt_range: Range<usize>,
+        snapshot: &BufferSnapshot,
+    ) -> Result<Vec<(Range<Anchor>, String)>> {
+        let content = output_excerpt.replace(CURSOR_MARKER, "");
+
+        let start_markers = content
+            .match_indices(EDITABLE_REGION_START_MARKER)
+            .collect::<Vec<_>>();
+        anyhow::ensure!(
+            start_markers.len() == 1,
+            "expected exactly one start marker, found {}",
+            start_markers.len()
+        );
+
+        let end_markers = content
+            .match_indices(EDITABLE_REGION_END_MARKER)
+            .collect::<Vec<_>>();
+        anyhow::ensure!(
+            end_markers.len() == 1,
+            "expected exactly one end marker, found {}",
+            end_markers.len()
+        );
+
+        let sof_markers = content
+            .match_indices(START_OF_FILE_MARKER)
+            .collect::<Vec<_>>();
+        anyhow::ensure!(
+            sof_markers.len() <= 1,
+            "expected at most one start-of-file marker, found {}",
+            sof_markers.len()
+        );
+
+        let codefence_start = start_markers[0].0;
+        let content = &content[codefence_start..];
+
+        let newline_ix = content.find('\n').context("could not find newline")?;
+        let content = &content[newline_ix + 1..];
+
+        let codefence_end = content
+            .rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
+            .context("could not find end marker")?;
+        let new_text = &content[..codefence_end];
+
+        let old_text = snapshot
+            .text_for_range(excerpt_range.clone())
+            .collect::<String>();
+
+        Ok(Self::compute_edits(
+            old_text,
+            new_text,
+            excerpt_range.start,
+            &snapshot,
+        ))
+    }
+
     pub fn compute_edits(
         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<Anchor>, 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]