Overhaul inline assistant (#12846)

Antonio Scandurra created

This pull request introduces a new diff mechanism that helps users
understand exactly which lines were changed by the LLM.

Release Notes:

- N/A

Change summary

Cargo.lock                               |   1 
Cargo.toml                               |   1 
assets/icons/context.svg                 |   6 
assets/icons/rotate_cw.svg               |   1 
assets/icons/stop.svg                    |   3 
crates/assistant/Cargo.toml              |   1 
crates/assistant/src/assistant_panel.rs  |   4 
crates/assistant/src/inline_assistant.rs | 714 ++++++++++++++++++++-----
crates/editor/src/display_map.rs         |  18 
crates/editor/src/editor.rs              |  60 ++
crates/editor/src/element.rs             |  41 +
crates/language/Cargo.toml               |   2 
crates/language/src/buffer.rs            |   4 
crates/ui/src/components/icon.rs         |   6 
14 files changed, 707 insertions(+), 155 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -373,6 +373,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
+ "similar",
  "smol",
  "strsim 0.11.1",
  "strum",

Cargo.toml πŸ”—

@@ -340,6 +340,7 @@ serde_repr = "0.1"
 sha2 = "0.10"
 shellexpand = "2.1.0"
 shlex = "1.3.0"
+similar = "1.3"
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 strum = { version = "0.25.0", features = ["derive"] }

assets/icons/context.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 5H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.5 8H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 10.9502H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/rotate_cw.svg πŸ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>

assets/icons/stop.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.88889 1H2.11111C1.49746 1 1 1.49746 1 2.11111V9.88889C1 10.5025 1.49746 11 2.11111 11H9.88889C10.5025 11 11 10.5025 11 9.88889V2.11111C11 1.49746 10.5025 1 9.88889 1Z" stroke="#C56757" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/assistant/Cargo.toml πŸ”—

@@ -47,6 +47,7 @@ semantic_index.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
+similar.workspace = true
 smol.workspace = true
 strsim = "0.11"
 strum.workspace = true

crates/assistant/src/assistant_panel.rs πŸ”—

@@ -1222,6 +1222,10 @@ impl Context {
         }
     }
 
+    pub(crate) fn token_count(&self) -> Option<usize> {
+        self.token_count
+    }
+
     pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
         let request = self.to_completion_request(cx);
         self.pending_token_count = cx.spawn(|this, mut cx| {

crates/assistant/src/inline_assistant.rs πŸ”—

@@ -11,8 +11,8 @@ use editor::{
         BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
     },
     scroll::{Autoscroll, AutoscrollStrategy},
-    Anchor, Editor, EditorElement, EditorEvent, EditorStyle, GutterDimensions, MultiBuffer,
-    MultiBufferSnapshot, ToOffset, ToPoint,
+    Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorStyle, ExcerptRange,
+    GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
 };
 use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
 use gpui::{
@@ -20,12 +20,18 @@ use gpui::{
     Global, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View,
     ViewContext, WeakView, WhiteSpace, WindowContext,
 };
-use language::{Point, TransactionId};
+use language::{Buffer, Point, TransactionId};
 use multi_buffer::MultiBufferRow;
 use parking_lot::Mutex;
 use rope::Rope;
 use settings::Settings;
-use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
+use similar::TextDiff;
+use std::{
+    cmp, future, mem,
+    ops::{Range, RangeInclusive},
+    sync::Arc,
+    time::Instant,
+};
 use theme::ThemeSettings;
 use ui::{prelude::*, Tooltip};
 use workspace::{notifications::NotificationId, Toast, Workspace};
@@ -108,37 +114,53 @@ impl InlineAssistant {
         });
 
         let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
-        let inline_assist_editor = cx.new_view(|cx| {
+        let prompt_editor = cx.new_view(|cx| {
             InlineAssistEditor::new(
                 inline_assist_id,
                 gutter_dimensions.clone(),
                 self.prompt_history.clone(),
                 codegen.clone(),
+                workspace.clone(),
                 cx,
             )
         });
-        let block_id = editor.update(cx, |editor, cx| {
-            editor.change_selections(None, cx, |selections| {
-                selections.select_anchor_ranges([selection.head()..selection.head()])
+        let (prompt_block_id, end_block_id) = editor.update(cx, |editor, cx| {
+            let start_anchor = snapshot.anchor_before(point_selection.start);
+            let end_anchor = snapshot.anchor_after(point_selection.end);
+            editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
+                selections.select_anchor_ranges([start_anchor..start_anchor])
             });
-            editor.insert_blocks(
-                [BlockProperties {
-                    style: BlockStyle::Sticky,
-                    position: snapshot.anchor_before(Point::new(point_selection.head().row, 0)),
-                    height: inline_assist_editor.read(cx).height_in_lines,
-                    render: build_inline_assist_editor_renderer(
-                        &inline_assist_editor,
-                        gutter_dimensions,
-                    ),
-                    disposition: if selection.reversed {
-                        BlockDisposition::Above
-                    } else {
-                        BlockDisposition::Below
+            let block_ids = editor.insert_blocks(
+                [
+                    BlockProperties {
+                        style: BlockStyle::Sticky,
+                        position: start_anchor,
+                        height: prompt_editor.read(cx).height_in_lines,
+                        render: build_inline_assist_editor_renderer(
+                            &prompt_editor,
+                            gutter_dimensions,
+                        ),
+                        disposition: BlockDisposition::Above,
+                    },
+                    BlockProperties {
+                        style: BlockStyle::Sticky,
+                        position: end_anchor,
+                        height: 1,
+                        render: Box::new(|cx| {
+                            v_flex()
+                                .h_full()
+                                .w_full()
+                                .border_t_1()
+                                .border_color(cx.theme().status().info_border)
+                                .into_any_element()
+                        }),
+                        disposition: BlockDisposition::Below,
                     },
-                }],
+                ],
                 Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
                 cx,
-            )[0]
+            );
+            (block_ids[0], block_ids[1])
         });
 
         self.pending_assists.insert(
@@ -146,17 +168,22 @@ impl InlineAssistant {
             PendingInlineAssist {
                 include_context,
                 editor: editor.downgrade(),
-                inline_assist_editor: Some((block_id, inline_assist_editor.clone())),
+                editor_decorations: Some(PendingInlineAssistDecorations {
+                    prompt_block_id,
+                    prompt_editor: prompt_editor.clone(),
+                    removed_line_block_ids: HashSet::default(),
+                    end_block_id,
+                }),
                 codegen: codegen.clone(),
                 workspace,
                 _subscriptions: vec![
-                    cx.subscribe(&inline_assist_editor, |inline_assist_editor, event, cx| {
+                    cx.subscribe(&prompt_editor, |inline_assist_editor, event, cx| {
                         InlineAssistant::update_global(cx, |this, cx| {
                             this.handle_inline_assistant_event(inline_assist_editor, event, cx)
                         })
                     }),
                     cx.subscribe(editor, {
-                        let inline_assist_editor = inline_assist_editor.downgrade();
+                        let inline_assist_editor = prompt_editor.downgrade();
                         move |editor, event, cx| {
                             if let Some(inline_assist_editor) = inline_assist_editor.upgrade() {
                                 if let EditorEvent::SelectionsChanged { local } = event {
@@ -176,7 +203,8 @@ impl InlineAssistant {
                         move |_, cx| {
                             if let Some(editor) = editor.upgrade() {
                                 InlineAssistant::update_global(cx, |this, cx| {
-                                    this.update_highlights_for_editor(&editor, cx);
+                                    this.update_editor_highlights(&editor, cx);
+                                    this.update_editor_blocks(&editor, inline_assist_id, cx);
                                 })
                             }
                         }
@@ -195,17 +223,15 @@ impl InlineAssistant {
                                     return;
                                 };
 
-                                let error = codegen
-                                    .read(cx)
-                                    .error()
-                                    .map(|error| format!("Inline assistant error: {}", error));
-                                if let Some(error) = error {
-                                    if pending_assist.inline_assist_editor.is_none() {
+                                if let CodegenStatus::Error(error) = &codegen.read(cx).status {
+                                    if pending_assist.editor_decorations.is_none() {
                                         if let Some(workspace) = pending_assist
                                             .workspace
                                             .as_ref()
                                             .and_then(|workspace| workspace.upgrade())
                                         {
+                                            let error =
+                                                format!("Inline assistant error: {}", error);
                                             workspace.update(cx, |workspace, cx| {
                                                 struct InlineAssistantError;
 
@@ -218,10 +244,10 @@ impl InlineAssistant {
                                                 workspace.show_toast(Toast::new(id, error), cx);
                                             })
                                         }
-
-                                        this.finish_inline_assist(inline_assist_id, false, cx);
                                     }
-                                } else {
+                                }
+
+                                if pending_assist.editor_decorations.is_none() {
                                     this.finish_inline_assist(inline_assist_id, false, cx);
                                 }
                             }
@@ -239,7 +265,7 @@ impl InlineAssistant {
             })
             .assist_ids
             .push(inline_assist_id);
-        self.update_highlights_for_editor(editor, cx);
+        self.update_editor_highlights(editor, cx);
     }
 
     fn handle_inline_assistant_event(
@@ -250,14 +276,20 @@ impl InlineAssistant {
     ) {
         let assist_id = inline_assist_editor.read(cx).id;
         match event {
-            InlineAssistEditorEvent::Confirmed { prompt } => {
-                self.confirm_inline_assist(assist_id, prompt, cx);
+            InlineAssistEditorEvent::Started => {
+                self.start_inline_assist(assist_id, cx);
+            }
+            InlineAssistEditorEvent::Stopped => {
+                self.stop_inline_assist(assist_id, cx);
+            }
+            InlineAssistEditorEvent::Confirmed => {
+                self.finish_inline_assist(assist_id, false, cx);
             }
             InlineAssistEditorEvent::Canceled => {
                 self.finish_inline_assist(assist_id, true, cx);
             }
             InlineAssistEditorEvent::Dismissed => {
-                self.hide_inline_assist(assist_id, cx);
+                self.hide_inline_assist_decorations(assist_id, cx);
             }
             InlineAssistEditorEvent::Resized { height_in_lines } => {
                 self.resize_inline_assist(assist_id, *height_in_lines, cx);
@@ -287,7 +319,7 @@ impl InlineAssistant {
         undo: bool,
         cx: &mut WindowContext,
     ) {
-        self.hide_inline_assist(assist_id, cx);
+        self.hide_inline_assist_decorations(assist_id, cx);
 
         if let Some(pending_assist) = self.pending_assists.remove(&assist_id) {
             if let hash_map::Entry::Occupied(mut entry) = self
@@ -301,7 +333,7 @@ impl InlineAssistant {
             }
 
             if let Some(editor) = pending_assist.editor.upgrade() {
-                self.update_highlights_for_editor(&editor, cx);
+                self.update_editor_highlights(&editor, cx);
 
                 if undo {
                     pending_assist
@@ -312,21 +344,37 @@ impl InlineAssistant {
         }
     }
 
-    fn hide_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
-        if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) {
-            if let Some(editor) = pending_assist.editor.upgrade() {
-                if let Some((block_id, inline_assist_editor)) =
-                    pending_assist.inline_assist_editor.take()
-                {
-                    editor.update(cx, |editor, cx| {
-                        editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
-                        if inline_assist_editor.focus_handle(cx).contains_focused(cx) {
-                            editor.focus(cx);
-                        }
-                    });
-                }
+    fn hide_inline_assist_decorations(
+        &mut self,
+        assist_id: InlineAssistId,
+        cx: &mut WindowContext,
+    ) -> bool {
+        let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
+            return false;
+        };
+        let Some(editor) = pending_assist.editor.upgrade() else {
+            return false;
+        };
+        let Some(decorations) = pending_assist.editor_decorations.take() else {
+            return false;
+        };
+
+        editor.update(cx, |editor, cx| {
+            let mut to_remove = decorations.removed_line_block_ids;
+            to_remove.insert(decorations.prompt_block_id);
+            to_remove.insert(decorations.end_block_id);
+            editor.remove_blocks(to_remove, None, cx);
+            if decorations
+                .prompt_editor
+                .focus_handle(cx)
+                .contains_focused(cx)
+            {
+                editor.focus(cx);
             }
-        }
+        });
+
+        self.update_editor_highlights(&editor, cx);
+        true
     }
 
     fn resize_inline_assist(
@@ -337,17 +385,16 @@ impl InlineAssistant {
     ) {
         if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) {
             if let Some(editor) = pending_assist.editor.upgrade() {
-                if let Some((block_id, inline_assist_editor)) =
-                    pending_assist.inline_assist_editor.as_ref()
-                {
-                    let gutter_dimensions = inline_assist_editor.read(cx).gutter_dimensions.clone();
+                if let Some(decorations) = pending_assist.editor_decorations.as_ref() {
+                    let gutter_dimensions =
+                        decorations.prompt_editor.read(cx).gutter_dimensions.clone();
                     let mut new_blocks = HashMap::default();
                     new_blocks.insert(
-                        *block_id,
+                        decorations.prompt_block_id,
                         (
                             Some(height_in_lines),
                             build_inline_assist_editor_renderer(
-                                inline_assist_editor,
+                                &decorations.prompt_editor,
                                 gutter_dimensions,
                             ),
                         ),
@@ -362,12 +409,7 @@ impl InlineAssistant {
         }
     }
 
-    fn confirm_inline_assist(
-        &mut self,
-        assist_id: InlineAssistId,
-        user_prompt: &str,
-        cx: &mut WindowContext,
-    ) {
+    fn start_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
         let pending_assist = if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id)
         {
             pending_assist
@@ -375,6 +417,18 @@ impl InlineAssistant {
             return;
         };
 
+        pending_assist
+            .codegen
+            .update(cx, |codegen, cx| codegen.undo(cx));
+
+        let Some(user_prompt) = pending_assist
+            .editor_decorations
+            .as_ref()
+            .map(|decorations| decorations.prompt_editor.read(cx).prompt(cx))
+        else {
+            return;
+        };
+
         let context = if pending_assist.include_context {
             pending_assist.workspace.as_ref().and_then(|workspace| {
                 let workspace = workspace.upgrade()?.read(cx);
@@ -404,8 +458,8 @@ impl InlineAssistant {
             )
         });
 
-        self.prompt_history.retain(|prompt| prompt != user_prompt);
-        self.prompt_history.push_back(user_prompt.into());
+        self.prompt_history.retain(|prompt| *prompt != user_prompt);
+        self.prompt_history.push_back(user_prompt.clone());
         if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
             self.prompt_history.pop_front();
         }
@@ -453,8 +507,6 @@ impl InlineAssistant {
             1.0
         };
 
-        let user_prompt = user_prompt.to_string();
-
         let prompt = cx.background_executor().spawn(async move {
             let language_name = language_name.as_deref();
             generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
@@ -488,9 +540,24 @@ impl InlineAssistant {
         .detach_and_log_err(cx);
     }
 
-    fn update_highlights_for_editor(&self, editor: &View<Editor>, cx: &mut WindowContext) {
-        let mut background_ranges = Vec::new();
+    fn stop_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
+        let pending_assist = if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id)
+        {
+            pending_assist
+        } else {
+            return;
+        };
+
+        pending_assist
+            .codegen
+            .update(cx, |codegen, cx| codegen.stop(cx));
+    }
+
+    fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
+        let mut gutter_pending_ranges = Vec::new();
+        let mut gutter_transformed_ranges = Vec::new();
         let mut foreground_ranges = Vec::new();
+        let mut inserted_row_ranges = Vec::new();
         let empty_inline_assist_ids = Vec::new();
         let inline_assist_ids = self
             .pending_assist_ids_by_editor
@@ -502,23 +569,47 @@ impl InlineAssistant {
         for inline_assist_id in inline_assist_ids {
             if let Some(pending_assist) = self.pending_assists.get(inline_assist_id) {
                 let codegen = pending_assist.codegen.read(cx);
-                background_ranges.push(codegen.range());
                 foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
+
+                if codegen.edit_position != codegen.range().end {
+                    gutter_pending_ranges.push(codegen.edit_position..codegen.range().end);
+                }
+
+                if codegen.range().start != codegen.edit_position {
+                    gutter_transformed_ranges.push(codegen.range().start..codegen.edit_position);
+                }
+
+                if pending_assist.editor_decorations.is_some() {
+                    inserted_row_ranges.extend(codegen.diff.inserted_row_ranges.iter().cloned());
+                }
             }
         }
 
         let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
-        merge_ranges(&mut background_ranges, &snapshot);
         merge_ranges(&mut foreground_ranges, &snapshot);
+        merge_ranges(&mut gutter_pending_ranges, &snapshot);
+        merge_ranges(&mut gutter_transformed_ranges, &snapshot);
         editor.update(cx, |editor, cx| {
-            if background_ranges.is_empty() {
-                editor.clear_background_highlights::<PendingInlineAssist>(cx);
+            enum GutterPendingRange {}
+            if gutter_pending_ranges.is_empty() {
+                editor.clear_gutter_highlights::<GutterPendingRange>(cx);
             } else {
-                editor.highlight_background::<PendingInlineAssist>(
-                    &background_ranges,
-                    |theme| theme.editor_active_line_background, // TODO use the appropriate color
+                editor.highlight_gutter::<GutterPendingRange>(
+                    &gutter_pending_ranges,
+                    |cx| cx.theme().status().info_background,
                     cx,
-                );
+                )
+            }
+
+            enum GutterTransformedRange {}
+            if gutter_transformed_ranges.is_empty() {
+                editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
+            } else {
+                editor.highlight_gutter::<GutterTransformedRange>(
+                    &gutter_transformed_ranges,
+                    |cx| cx.theme().status().info,
+                    cx,
+                )
             }
 
             if foreground_ranges.is_empty() {
@@ -533,8 +624,108 @@ impl InlineAssistant {
                     cx,
                 );
             }
+
+            editor.clear_row_highlights::<PendingInlineAssist>();
+            for row_range in inserted_row_ranges {
+                editor.highlight_rows::<PendingInlineAssist>(
+                    row_range,
+                    Some(cx.theme().status().info_background),
+                    false,
+                    cx,
+                );
+            }
         });
     }
+
+    fn update_editor_blocks(
+        &mut self,
+        editor: &View<Editor>,
+        assist_id: InlineAssistId,
+        cx: &mut WindowContext,
+    ) {
+        let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
+            return;
+        };
+        let Some(decorations) = pending_assist.editor_decorations.as_mut() else {
+            return;
+        };
+
+        let codegen = pending_assist.codegen.read(cx);
+        let old_snapshot = codegen.snapshot.clone();
+        let old_buffer = codegen.old_buffer.clone();
+        let deleted_row_ranges = codegen.diff.deleted_row_ranges.clone();
+
+        editor.update(cx, |editor, cx| {
+            let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
+            editor.remove_blocks(old_blocks, None, cx);
+
+            let mut new_blocks = Vec::new();
+            for (new_row, old_row_range) in deleted_row_ranges {
+                let (_, buffer_start) = old_snapshot
+                    .point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
+                    .unwrap();
+                let (_, buffer_end) = old_snapshot
+                    .point_to_buffer_offset(Point::new(
+                        *old_row_range.end(),
+                        old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
+                    ))
+                    .unwrap();
+
+                let deleted_lines_editor = cx.new_view(|cx| {
+                    let multi_buffer = cx.new_model(|_| {
+                        MultiBuffer::without_headers(0, language::Capability::ReadOnly)
+                    });
+                    multi_buffer.update(cx, |multi_buffer, cx| {
+                        multi_buffer.push_excerpts(
+                            old_buffer.clone(),
+                            Some(ExcerptRange {
+                                context: buffer_start..buffer_end,
+                                primary: None,
+                            }),
+                            cx,
+                        );
+                    });
+
+                    enum DeletedLines {}
+                    let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
+                    editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
+                    editor.set_show_wrap_guides(false, cx);
+                    editor.set_show_gutter(false, cx);
+                    editor.scroll_manager.set_forbid_vertical_scroll(true);
+                    editor.set_read_only(true);
+                    editor.highlight_rows::<DeletedLines>(
+                        Anchor::min()..=Anchor::max(),
+                        Some(cx.theme().status().deleted_background),
+                        false,
+                        cx,
+                    );
+                    editor
+                });
+
+                let height = deleted_lines_editor
+                    .update(cx, |editor, cx| editor.max_point(cx).row().0 as u8 + 1);
+                new_blocks.push(BlockProperties {
+                    position: new_row,
+                    height,
+                    style: BlockStyle::Flex,
+                    render: Box::new(move |cx| {
+                        div()
+                            .bg(cx.theme().status().deleted_background)
+                            .size_full()
+                            .pl(cx.gutter_dimensions.full_width())
+                            .child(deleted_lines_editor.clone())
+                            .into_any_element()
+                    }),
+                    disposition: BlockDisposition::Above,
+                });
+            }
+
+            decorations.removed_line_block_ids = editor
+                .insert_blocks(new_blocks, None, cx)
+                .into_iter()
+                .collect();
+        })
+    }
 }
 
 fn build_inline_assist_editor_renderer(
@@ -560,7 +751,9 @@ impl InlineAssistId {
 }
 
 enum InlineAssistEditorEvent {
-    Confirmed { prompt: String },
+    Started,
+    Stopped,
+    Confirmed,
     Canceled,
     Dismissed,
     Resized { height_in_lines: u8 },
@@ -570,12 +763,13 @@ struct InlineAssistEditor {
     id: InlineAssistId,
     height_in_lines: u8,
     prompt_editor: View<Editor>,
-    confirmed: bool,
+    edited_since_done: bool,
     gutter_dimensions: Arc<Mutex<GutterDimensions>>,
     prompt_history: VecDeque<String>,
     prompt_history_ix: Option<usize>,
     pending_prompt: String,
     codegen: Model<Codegen>,
+    workspace: Option<WeakView<Workspace>>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -584,39 +778,170 @@ impl EventEmitter<InlineAssistEditorEvent> for InlineAssistEditor {}
 impl Render for InlineAssistEditor {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let gutter_dimensions = *self.gutter_dimensions.lock();
-        let icon_size = IconSize::default();
-        h_flex()
-            .w_full()
-            .py_1p5()
-            .border_y_1()
-            .border_color(cx.theme().colors().border)
-            .bg(cx.theme().colors().editor_background)
-            .on_action(cx.listener(Self::confirm))
-            .on_action(cx.listener(Self::cancel))
-            .on_action(cx.listener(Self::move_up))
-            .on_action(cx.listener(Self::move_down))
-            .child(
-                h_flex()
-                    .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
-                    .pr(gutter_dimensions.fold_area_width())
-                    .justify_end()
-                    .children(if let Some(error) = self.codegen.read(cx).error() {
-                        let error_message = SharedString::from(error.to_string());
-                        Some(
-                            div()
-                                .id("error")
-                                .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
-                                .child(
-                                    Icon::new(IconName::XCircle)
-                                        .size(icon_size)
-                                        .color(Color::Error),
-                                ),
-                        )
+
+        let buttons = match &self.codegen.read(cx).status {
+            CodegenStatus::Idle => {
+                vec![
+                    IconButton::new("start", IconName::Sparkle)
+                        .icon_color(Color::Muted)
+                        .size(ButtonSize::None)
+                        .icon_size(IconSize::XSmall)
+                        .tooltip(|cx| Tooltip::for_action("Transform", &menu::Confirm, cx))
+                        .on_click(
+                            cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Started)),
+                        ),
+                    IconButton::new("cancel", IconName::Close)
+                        .icon_color(Color::Muted)
+                        .size(ButtonSize::None)
+                        .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
+                        .on_click(
+                            cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)),
+                        ),
+                ]
+            }
+            CodegenStatus::Pending => {
+                vec![
+                    IconButton::new("stop", IconName::Stop)
+                        .icon_color(Color::Error)
+                        .size(ButtonSize::None)
+                        .icon_size(IconSize::XSmall)
+                        .tooltip(|cx| {
+                            Tooltip::with_meta(
+                                "Interrupt Transformation",
+                                Some(&menu::Cancel),
+                                "Changes won't be discarded",
+                                cx,
+                            )
+                        })
+                        .on_click(
+                            cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Stopped)),
+                        ),
+                    IconButton::new("cancel", IconName::Close)
+                        .icon_color(Color::Muted)
+                        .size(ButtonSize::None)
+                        .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
+                        .on_click(
+                            cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)),
+                        ),
+                ]
+            }
+            CodegenStatus::Error(_) | CodegenStatus::Done => {
+                vec![
+                    if self.edited_since_done {
+                        IconButton::new("restart", IconName::RotateCw)
+                            .icon_color(Color::Info)
+                            .icon_size(IconSize::XSmall)
+                            .size(ButtonSize::None)
+                            .tooltip(|cx| {
+                                Tooltip::with_meta(
+                                    "Restart Transformation",
+                                    Some(&menu::Confirm),
+                                    "Changes will be discarded",
+                                    cx,
+                                )
+                            })
+                            .on_click(cx.listener(|_, _, cx| {
+                                cx.emit(InlineAssistEditorEvent::Started);
+                            }))
                     } else {
-                        None
-                    }),
-            )
-            .child(div().flex_1().child(self.render_prompt_editor(cx)))
+                        IconButton::new("confirm", IconName::Check)
+                            .icon_color(Color::Info)
+                            .size(ButtonSize::None)
+                            .tooltip(|cx| Tooltip::for_action("Confirm Assist", &menu::Confirm, cx))
+                            .on_click(cx.listener(|_, _, cx| {
+                                cx.emit(InlineAssistEditorEvent::Confirmed);
+                            }))
+                    },
+                    IconButton::new("cancel", IconName::Close)
+                        .icon_color(Color::Muted)
+                        .size(ButtonSize::None)
+                        .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
+                        .on_click(
+                            cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::Canceled)),
+                        ),
+                ]
+            }
+        };
+
+        v_flex().h_full().w_full().justify_end().child(
+            h_flex()
+                .bg(cx.theme().colors().editor_background)
+                .border_y_1()
+                .border_color(cx.theme().status().info_border)
+                .py_1p5()
+                .w_full()
+                .on_action(cx.listener(Self::confirm))
+                .on_action(cx.listener(Self::cancel))
+                .on_action(cx.listener(Self::move_up))
+                .on_action(cx.listener(Self::move_down))
+                .child(
+                    h_flex()
+                        .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
+                        // .pr(gutter_dimensions.fold_area_width())
+                        .justify_center()
+                        .gap_2()
+                        .children(self.workspace.clone().map(|workspace| {
+                            IconButton::new("context", IconName::Context)
+                                .size(ButtonSize::None)
+                                .icon_size(IconSize::XSmall)
+                                .icon_color(Color::Muted)
+                                .on_click({
+                                    let workspace = workspace.clone();
+                                    cx.listener(move |_, _, cx| {
+                                        workspace
+                                            .update(cx, |workspace, cx| {
+                                                workspace.focus_panel::<AssistantPanel>(cx);
+                                            })
+                                            .ok();
+                                    })
+                                })
+                                .tooltip(move |cx| {
+                                    let token_count = workspace.upgrade().and_then(|workspace| {
+                                        let panel =
+                                            workspace.read(cx).panel::<AssistantPanel>(cx)?;
+                                        let context = panel.read(cx).active_context(cx)?;
+                                        context.read(cx).token_count()
+                                    });
+                                    if let Some(token_count) = token_count {
+                                        Tooltip::with_meta(
+                                            format!(
+                                                "{} Additional Context Tokens from Assistant",
+                                                token_count
+                                            ),
+                                            Some(&crate::ToggleFocus),
+                                            "Click to open…",
+                                            cx,
+                                        )
+                                    } else {
+                                        Tooltip::for_action(
+                                            "Toggle Assistant Panel",
+                                            &crate::ToggleFocus,
+                                            cx,
+                                        )
+                                    }
+                                })
+                        }))
+                        .children(
+                            if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
+                                let error_message = SharedString::from(error.to_string());
+                                Some(
+                                    div()
+                                        .id("error")
+                                        .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
+                                        .child(
+                                            Icon::new(IconName::XCircle)
+                                                .size(IconSize::Small)
+                                                .color(Color::Error),
+                                        ),
+                                )
+                            } else {
+                                None
+                            },
+                        ),
+                )
+                .child(div().flex_1().child(self.render_prompt_editor(cx)))
+                .child(h_flex().gap_2().pr_4().children(buttons)),
+        )
     }
 }
 
@@ -635,16 +960,13 @@ impl InlineAssistEditor {
         gutter_dimensions: Arc<Mutex<GutterDimensions>>,
         prompt_history: VecDeque<String>,
         codegen: Model<Codegen>,
+        workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let prompt_editor = cx.new_view(|cx| {
             let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
             editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
-            let placeholder = match codegen.read(cx).kind() {
-                CodegenKind::Transform { .. } => "Enter transformation prompt…",
-                CodegenKind::Generate { .. } => "Enter generation prompt…",
-            };
-            editor.set_placeholder_text(placeholder, cx);
+            editor.set_placeholder_text("Add a prompt…", cx);
             editor
         });
         cx.focus_view(&prompt_editor);
@@ -659,21 +981,26 @@ impl InlineAssistEditor {
             id,
             height_in_lines: 1,
             prompt_editor,
-            confirmed: false,
+            edited_since_done: false,
             gutter_dimensions,
             prompt_history,
             prompt_history_ix: None,
             pending_prompt: String::new(),
             codegen,
+            workspace,
             _subscriptions: subscriptions,
         };
         this.count_lines(cx);
         this
     }
 
+    fn prompt(&self, cx: &AppContext) -> String {
+        self.prompt_editor.read(cx).text(cx)
+    }
+
     fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
         let height_in_lines = cmp::max(
-            2, // Make the editor at least two lines tall, to account for padding.
+            2, // Make the editor at least two lines tall, to account for padding and buttons.
             cmp::min(
                 self.prompt_editor
                     .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
@@ -699,12 +1026,25 @@ impl InlineAssistEditor {
     ) {
         match event {
             EditorEvent::Edited => {
+                self.edited_since_done = true;
                 self.pending_prompt = self.prompt_editor.read(cx).text(cx);
                 cx.notify();
             }
             EditorEvent::Blurred => {
-                if !self.confirmed {
-                    cx.emit(InlineAssistEditorEvent::Canceled);
+                if let CodegenStatus::Idle = &self.codegen.read(cx).status {
+                    let assistant_panel_is_focused = self
+                        .workspace
+                        .as_ref()
+                        .and_then(|workspace| {
+                            let panel =
+                                workspace.upgrade()?.read(cx).panel::<AssistantPanel>(cx)?;
+                            Some(panel.focus_handle(cx).contains_focused(cx))
+                        })
+                        .unwrap_or(false);
+
+                    if !assistant_panel_is_focused {
+                        cx.emit(InlineAssistEditorEvent::Canceled);
+                    }
                 }
             }
             _ => {}
@@ -712,35 +1052,49 @@ impl InlineAssistEditor {
     }
 
     fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
-        let is_read_only = !self.codegen.read(cx).idle();
-        self.prompt_editor.update(cx, |editor, cx| {
-            let was_read_only = editor.read_only(cx);
-            if was_read_only != is_read_only {
-                if is_read_only {
-                    editor.set_read_only(true);
-                } else {
-                    self.confirmed = false;
-                    editor.set_read_only(false);
-                }
+        match &self.codegen.read(cx).status {
+            CodegenStatus::Idle => {
+                self.prompt_editor
+                    .update(cx, |editor, _| editor.set_read_only(false));
             }
-        });
-        cx.notify();
+            CodegenStatus::Pending => {
+                self.prompt_editor
+                    .update(cx, |editor, _| editor.set_read_only(true));
+            }
+            CodegenStatus::Done | CodegenStatus::Error(_) => {
+                self.edited_since_done = false;
+                self.prompt_editor
+                    .update(cx, |editor, _| editor.set_read_only(false));
+            }
+        }
     }
 
     fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(InlineAssistEditorEvent::Canceled);
+        match &self.codegen.read(cx).status {
+            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
+                cx.emit(InlineAssistEditorEvent::Canceled);
+            }
+            CodegenStatus::Pending => {
+                cx.emit(InlineAssistEditorEvent::Stopped);
+            }
+        }
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        if self.confirmed {
-            cx.emit(InlineAssistEditorEvent::Dismissed);
-        } else {
-            let prompt = self.prompt_editor.read(cx).text(cx);
-            self.prompt_editor
-                .update(cx, |editor, _cx| editor.set_read_only(true));
-            cx.emit(InlineAssistEditorEvent::Confirmed { prompt });
-            self.confirmed = true;
-            cx.notify();
+        match &self.codegen.read(cx).status {
+            CodegenStatus::Idle => {
+                cx.emit(InlineAssistEditorEvent::Started);
+            }
+            CodegenStatus::Pending => {
+                cx.emit(InlineAssistEditorEvent::Dismissed);
+            }
+            CodegenStatus::Done | CodegenStatus::Error(_) => {
+                if self.edited_since_done {
+                    cx.emit(InlineAssistEditorEvent::Started);
+                } else {
+                    cx.emit(InlineAssistEditorEvent::Confirmed);
+                }
+            }
         }
     }
 
@@ -814,13 +1168,20 @@ impl InlineAssistEditor {
 
 struct PendingInlineAssist {
     editor: WeakView<Editor>,
-    inline_assist_editor: Option<(BlockId, View<InlineAssistEditor>)>,
+    editor_decorations: Option<PendingInlineAssistDecorations>,
     codegen: Model<Codegen>,
     _subscriptions: Vec<Subscription>,
     workspace: Option<WeakView<Workspace>>,
     include_context: bool,
 }
 
+struct PendingInlineAssistDecorations {
+    prompt_block_id: BlockId,
+    prompt_editor: View<InlineAssistEditor>,
+    removed_line_block_ids: HashSet<BlockId>,
+    end_block_id: BlockId,
+}
+
 #[derive(Debug)]
 pub enum CodegenEvent {
     Finished,
@@ -833,19 +1194,45 @@ pub enum CodegenKind {
     Generate { position: Anchor },
 }
 
+impl CodegenKind {
+    fn range(&self, snapshot: &MultiBufferSnapshot) -> Range<Anchor> {
+        match self {
+            CodegenKind::Transform { range } => range.clone(),
+            CodegenKind::Generate { position } => position.bias_left(snapshot)..*position,
+        }
+    }
+}
+
 pub struct Codegen {
     buffer: Model<MultiBuffer>,
+    old_buffer: Model<Buffer>,
     snapshot: MultiBufferSnapshot,
     kind: CodegenKind,
+    edit_position: Anchor,
     last_equal_ranges: Vec<Range<Anchor>>,
     transaction_id: Option<TransactionId>,
-    error: Option<anyhow::Error>,
+    status: CodegenStatus,
     generation: Task<()>,
-    idle: bool,
+    diff: Diff,
     telemetry: Option<Arc<Telemetry>>,
     _subscription: gpui::Subscription,
 }
 
+enum CodegenStatus {
+    Idle,
+    Pending,
+    Done,
+    Error(anyhow::Error),
+}
+
+#[derive(Default)]
+struct Diff {
+    task: Option<Task<()>>,
+    should_update: bool,
+    deleted_row_ranges: Vec<(Anchor, RangeInclusive<u32>)>,
+    inserted_row_ranges: Vec<RangeInclusive<Anchor>>,
+}
+
 impl EventEmitter<CodegenEvent> for Codegen {}
 
 impl Codegen {
@@ -856,15 +1243,38 @@ impl Codegen {
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let snapshot = buffer.read(cx).snapshot(cx);
+
+        let (old_buffer, _, _) = buffer
+            .read(cx)
+            .range_to_buffer_ranges(kind.range(&snapshot), cx)
+            .pop()
+            .unwrap();
+        let old_buffer = cx.new_model(|cx| {
+            let old_buffer = old_buffer.read(cx);
+            let text = old_buffer.as_rope().clone();
+            let line_ending = old_buffer.line_ending();
+            let language = old_buffer.language().cloned();
+            let language_registry = old_buffer.language_registry();
+
+            let mut buffer = Buffer::local_normalized(text, line_ending, cx);
+            buffer.set_language(language, cx);
+            if let Some(language_registry) = language_registry {
+                buffer.set_language_registry(language_registry)
+            }
+            buffer
+        });
+
         Self {
             buffer: buffer.clone(),
+            old_buffer,
+            edit_position: kind.range(&snapshot).start,
             snapshot,
             kind,
             last_equal_ranges: Default::default(),
             transaction_id: Default::default(),
-            error: Default::default(),
-            idle: true,
+            status: CodegenStatus::Idle,
             generation: Task::ready(()),
+            diff: Diff::default(),
             telemetry,
             _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
         }

crates/editor/src/display_map.rs πŸ”—

@@ -52,8 +52,14 @@ use multi_buffer::{
     ToOffset, ToPoint,
 };
 use serde::Deserialize;
-use std::ops::Add;
-use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
+use std::{
+    any::TypeId,
+    borrow::Cow,
+    fmt::Debug,
+    num::NonZeroU32,
+    ops::{Add, Range, Sub},
+    sync::Arc,
+};
 use sum_tree::{Bias, TreeMap};
 use tab_map::{TabMap, TabSnapshot};
 use text::LineIndent;
@@ -1027,6 +1033,14 @@ impl Add for DisplayRow {
     }
 }
 
+impl Sub for DisplayRow {
+    type Output = Self;
+
+    fn sub(self, other: Self) -> Self::Output {
+        DisplayRow(self.0 - other.0)
+    }
+}
+
 impl DisplayPoint {
     pub fn new(row: DisplayRow, column: u32) -> Self {
         Self(BlockPoint(Point::new(row.0, column)))

crates/editor/src/editor.rs πŸ”—

@@ -376,6 +376,7 @@ type CompletionId = usize;
 // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
 type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
+type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range<Anchor>]>);
 
 struct ScrollbarMarkerState {
     scrollbar_size: Size<Pixels>,
@@ -464,6 +465,7 @@ pub struct Editor {
     highlight_order: usize,
     highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
     background_highlights: TreeMap<TypeId, BackgroundHighlight>,
+    gutter_highlights: TreeMap<TypeId, GutterHighlight>,
     scrollbar_marker_state: ScrollbarMarkerState,
     active_indent_guides_state: ActiveIndentGuidesState,
     nav_history: Option<ItemNavHistory>,
@@ -1752,6 +1754,7 @@ impl Editor {
             highlight_order: 0,
             highlighted_rows: HashMap::default(),
             background_highlights: Default::default(),
+            gutter_highlights: TreeMap::default(),
             scrollbar_marker_state: ScrollbarMarkerState::default(),
             active_indent_guides_state: ActiveIndentGuidesState::default(),
             nav_history: None,
@@ -10263,6 +10266,25 @@ impl Editor {
         Some(text_highlights)
     }
 
+    pub fn highlight_gutter<T: 'static>(
+        &mut self,
+        ranges: &[Range<Anchor>],
+        color_fetcher: fn(&AppContext) -> Hsla,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.gutter_highlights
+            .insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
+        cx.notify();
+    }
+
+    pub fn clear_gutter_highlights<T: 'static>(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<GutterHighlight> {
+        cx.notify();
+        self.gutter_highlights.remove(&TypeId::of::<T>())
+    }
+
     #[cfg(feature = "test-support")]
     pub fn all_text_background_highlights(
         &mut self,
@@ -10452,6 +10474,44 @@ impl Editor {
         results
     }
 
+    pub fn gutter_highlights_in_range(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+        cx: &AppContext,
+    ) -> Vec<(Range<DisplayPoint>, Hsla)> {
+        let mut results = Vec::new();
+        for (color_fetcher, ranges) in self.gutter_highlights.values() {
+            let color = color_fetcher(cx);
+            let start_ix = match ranges.binary_search_by(|probe| {
+                let cmp = probe
+                    .end
+                    .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
+                if cmp.is_gt() {
+                    Ordering::Greater
+                } else {
+                    Ordering::Less
+                }
+            }) {
+                Ok(i) | Err(i) => i,
+            };
+            for range in &ranges[start_ix..] {
+                if range
+                    .start
+                    .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+                    .is_ge()
+                {
+                    break;
+                }
+
+                let start = range.start.to_display_point(&display_snapshot);
+                let end = range.end.to_display_point(&display_snapshot);
+                results.push((start..end, color))
+            }
+        }
+        results
+    }
+
     /// Get the text ranges corresponding to the redaction query
     pub fn redacted_ranges(
         &self,

crates/editor/src/element.rs πŸ”—

@@ -2837,6 +2837,8 @@ impl EditorElement {
             Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
         }
 
+        self.paint_gutter_highlights(layout, cx);
+
         if layout.blamed_display_rows.is_some() {
             self.paint_blamed_display_rows(layout, cx);
         }
@@ -3006,6 +3008,37 @@ impl EditorElement {
         }
     }
 
+    fn paint_gutter_highlights(&self, layout: &EditorLayout, cx: &mut WindowContext) {
+        let highlight_width = 0.275 * layout.position_map.line_height;
+        let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
+        cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
+            for (range, color) in &layout.highlighted_gutter_ranges {
+                let start_row = if range.start.row() < layout.visible_display_row_range.start {
+                    layout.visible_display_row_range.start - DisplayRow(1)
+                } else {
+                    range.start.row()
+                };
+                let end_row = if range.end.row() > layout.visible_display_row_range.end {
+                    layout.visible_display_row_range.end + DisplayRow(1)
+                } else {
+                    range.end.row()
+                };
+
+                let start_y = layout.gutter_hitbox.top()
+                    + start_row.0 as f32 * layout.position_map.line_height
+                    - layout.position_map.scroll_pixel_position.y;
+                let end_y = layout.gutter_hitbox.top()
+                    + (end_row.0 + 1) as f32 * layout.position_map.line_height
+                    - layout.position_map.scroll_pixel_position.y;
+                let bounds = Bounds::from_corners(
+                    point(layout.gutter_hitbox.left(), start_y),
+                    point(layout.gutter_hitbox.left() + highlight_width, end_y),
+                );
+                cx.paint_quad(fill(bounds, *color).corner_radii(highlight_corner_radii));
+            }
+        });
+    }
+
     fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut WindowContext) {
         let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
             return;
@@ -4631,6 +4664,12 @@ impl Element for EditorElement {
                         &snapshot.display_snapshot,
                         cx.theme().colors(),
                     );
+                    let highlighted_gutter_ranges =
+                        self.editor.read(cx).gutter_highlights_in_range(
+                            start_anchor..end_anchor,
+                            &snapshot.display_snapshot,
+                            cx,
+                        );
 
                     let redacted_ranges = self.editor.read(cx).redacted_ranges(
                         start_anchor..end_anchor,
@@ -4991,6 +5030,7 @@ impl Element for EditorElement {
                         active_rows,
                         highlighted_rows,
                         highlighted_ranges,
+                        highlighted_gutter_ranges,
                         redacted_ranges,
                         line_elements,
                         line_numbers,
@@ -5121,6 +5161,7 @@ pub struct EditorLayout {
     inline_blame: Option<AnyElement>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
+    highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
     redacted_ranges: Vec<Range<DisplayPoint>>,
     cursors: Vec<(DisplayPoint, Hsla)>,
     visible_cursors: Vec<CursorLayout>,

crates/language/Cargo.toml πŸ”—

@@ -49,7 +49,7 @@ schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
-similar = "1.3"
+similar.workspace = true
 smallvec.workspace = true
 smol.workspace = true
 sum_tree.workspace = true

crates/language/src/buffer.rs πŸ”—

@@ -797,6 +797,10 @@ impl Buffer {
             .set_language_registry(language_registry);
     }
 
+    pub fn language_registry(&self) -> Option<Arc<LanguageRegistry>> {
+        self.syntax_map.lock().language_registry()
+    }
+
     /// Assign the buffer a new [Capability].
     pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext<Self>) {
         self.capability = capability;

crates/ui/src/components/icon.rs πŸ”—

@@ -106,6 +106,7 @@ pub enum IconName {
     Code,
     Collab,
     Command,
+    Context,
     Control,
     Copilot,
     CopilotDisabled,
@@ -170,6 +171,7 @@ pub enum IconName {
     Rerun,
     Return,
     Reveal,
+    RotateCw,
     Save,
     Screen,
     SelectAll,
@@ -186,6 +188,7 @@ pub enum IconName {
     Split,
     Star,
     StarFilled,
+    Stop,
     Strikethrough,
     Supermaven,
     SupermavenDisabled,
@@ -233,6 +236,7 @@ impl IconName {
             IconName::Code => "icons/code.svg",
             IconName::Collab => "icons/user_group_16.svg",
             IconName::Command => "icons/command.svg",
+            IconName::Context => "icons/context.svg",
             IconName::Control => "icons/control.svg",
             IconName::Copilot => "icons/copilot.svg",
             IconName::CopilotDisabled => "icons/copilot_disabled.svg",
@@ -297,6 +301,7 @@ impl IconName {
             IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
             IconName::Rerun => "icons/rerun.svg",
             IconName::Return => "icons/return.svg",
+            IconName::RotateCw => "icons/rotate_cw.svg",
             IconName::Save => "icons/save.svg",
             IconName::Screen => "icons/desktop.svg",
             IconName::SelectAll => "icons/select_all.svg",
@@ -313,6 +318,7 @@ impl IconName {
             IconName::Split => "icons/split.svg",
             IconName::Star => "icons/star.svg",
             IconName::StarFilled => "icons/star_filled.svg",
+            IconName::Stop => "icons/stop.svg",
             IconName::Strikethrough => "icons/strikethrough.svg",
             IconName::Supermaven => "icons/supermaven.svg",
             IconName::SupermavenDisabled => "icons/supermaven_disabled.svg",