editor: Extract more git related stuff out of `editor.rs` (#56198)

Mikhail Pertsev created

cc @SomeoneToIgnore

## Summary

Follow-up to #56155. I extracted the remaining git related things (again
not all of them, leftovers are more tricky as there are git + fold +
some others things combined) into `git.rs`

We nod reached a good milestone, the `editor.rs` would be below 20k
lines now

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs | 2190 +++-----------------------------------
crates/editor/src/git.rs    | 1350 ++++++++++++++++++++++-
2 files changed, 1,484 insertions(+), 2,056 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -89,6 +89,13 @@ pub use element::{
     render_breadcrumb_text,
 };
 pub use git::blame::BlameRenderer;
+pub(crate) use git::{DiffHunkKey, StoredReviewComment};
+use git::{
+    DiffReviewDragState, DiffReviewOverlay, InlineBlamePopover, render_diff_hunk_controls,
+    update_uncommitted_diff_for_buffer,
+};
+pub(crate) use git::{DisplayDiffHunk, PhantomDiffReviewIndicator};
+pub use git::{RenderDiffHunkControlsFn, set_blame_renderer};
 pub use hover_popover::hover_markdown_style;
 pub use inlays::Inlay;
 pub use items::MAX_TAB_TITLE_LEN;
@@ -104,11 +111,10 @@ pub use split::{SplittableEditor, ToggleSplitDiff};
 pub use split_editor_view::SplitEditorView;
 pub use text::Bias;
 
-use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
+use ::git::status::FileStatus;
 use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
 use anyhow::{Context as _, Result, anyhow, bail};
 use blink_manager::BlinkManager;
-use buffer_diff::DiffHunkStatus;
 use client::{Collaborator, ParticipantIndex, parse_zed_link};
 use clock::ReplicaId;
 use code_context_menus::{
@@ -282,19 +288,6 @@ pub const LSP_REQUEST_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(50);
 pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
 pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
 
-pub type RenderDiffHunkControlsFn = Arc<
-    dyn Fn(
-        u32,
-        &DiffHunkStatus,
-        Range<Anchor>,
-        bool,
-        Pixels,
-        &Entity<Editor>,
-        &mut Window,
-        &mut App,
-    ) -> AnyElement,
->;
-
 enum ReportEditorEvent {
     Saved { auto_saved: bool },
     EditorOpened,
@@ -335,21 +328,6 @@ impl Navigated {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
-enum DisplayDiffHunk {
-    Folded {
-        display_row: DisplayRow,
-    },
-    Unfolded {
-        is_created_file: bool,
-        diff_base_byte_range: Range<usize>,
-        display_row_range: Range<DisplayRow>,
-        multi_buffer_range: Range<Anchor>,
-        status: DiffHunkStatus,
-        word_diffs: Vec<Range<MultiBufferOffset>>,
-    },
-}
-
 pub fn init(cx: &mut App) {
     cx.set_global(GlobalBlameRenderer(Arc::new(())));
     cx.set_global(breadcrumbs::RenderBreadcrumbText(render_breadcrumb_text));
@@ -402,10 +380,6 @@ pub fn init(cx: &mut App) {
     _ = multi_buffer::EXCERPT_CONTEXT_LINES.set(multibuffer_context_lines);
 }
 
-pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) {
-    cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));
-}
-
 pub struct SearchWithinRange;
 
 trait InvalidationRegion {
@@ -957,21 +931,6 @@ impl ChangeList {
     }
 }
 
-#[derive(Clone)]
-struct InlineBlamePopoverState {
-    scroll_handle: ScrollHandle,
-    commit_message: Option<ParsedCommitMessage>,
-    markdown: Entity<Markdown>,
-}
-
-struct InlineBlamePopover {
-    position: gpui::Point<Pixels>,
-    hide_task: Option<Task<()>>,
-    popover_bounds: Option<Bounds<Pixels>>,
-    popover_state: InlineBlamePopoverState,
-    keyboard_grace: bool,
-}
-
 enum SelectionDragState {
     /// State when no drag related activity is detected.
     None,
@@ -1009,94 +968,6 @@ struct GutterHoverButton {
     is_active: bool,
 }
 
-/// Represents a diff review button indicator that shows up when hovering over lines in the gutter
-/// in diff view mode.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub(crate) struct PhantomDiffReviewIndicator {
-    /// The starting anchor of the selection (or the only row if not dragging).
-    pub start: Anchor,
-    /// The ending anchor of the selection. Equal to start_anchor for single-line selection.
-    pub end: Anchor,
-    /// There's a small debounce between hovering over the line and showing the indicator.
-    /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel.
-    pub is_active: bool,
-}
-
-#[derive(Clone, Debug)]
-pub(crate) struct DiffReviewDragState {
-    pub start_anchor: Anchor,
-    pub current_anchor: Anchor,
-}
-
-impl DiffReviewDragState {
-    pub fn row_range(&self, snapshot: &DisplaySnapshot) -> std::ops::RangeInclusive<DisplayRow> {
-        let start = self.start_anchor.to_display_point(snapshot).row();
-        let current = self.current_anchor.to_display_point(snapshot).row();
-
-        (start..=current).sorted()
-    }
-}
-
-/// Identifies a specific hunk in the diff buffer.
-/// Used as a key to group comments by their location.
-#[derive(Clone, Debug)]
-pub struct DiffHunkKey {
-    /// The file path (relative to worktree) this hunk belongs to.
-    pub file_path: Arc<util::rel_path::RelPath>,
-    /// An anchor at the start of the hunk. This tracks position as the buffer changes.
-    pub hunk_start_anchor: Anchor,
-}
-
-/// A review comment stored locally before being sent to the Agent panel.
-#[derive(Clone)]
-pub struct StoredReviewComment {
-    /// Unique identifier for this comment (for edit/delete operations).
-    pub id: usize,
-    /// The comment text entered by the user.
-    pub comment: String,
-    /// Anchors for the code range being reviewed.
-    pub range: Range<Anchor>,
-    /// Timestamp when the comment was created (for chronological ordering).
-    pub created_at: Instant,
-    /// Whether this comment is currently being edited inline.
-    pub is_editing: bool,
-}
-
-impl StoredReviewComment {
-    pub fn new(id: usize, comment: String, anchor_range: Range<Anchor>) -> Self {
-        Self {
-            id,
-            comment,
-            range: anchor_range,
-            created_at: Instant::now(),
-            is_editing: false,
-        }
-    }
-}
-
-/// Represents an active diff review overlay that appears when clicking the "Add Review" button.
-pub(crate) struct DiffReviewOverlay {
-    pub anchor_range: Range<Anchor>,
-    /// The block ID for the overlay.
-    pub block_id: CustomBlockId,
-    /// The editor entity for the review input.
-    pub prompt_editor: Entity<Editor>,
-    /// The hunk key this overlay belongs to.
-    pub hunk_key: DiffHunkKey,
-    /// Whether the comments section is expanded.
-    pub comments_expanded: bool,
-    /// Editors for comments currently being edited inline.
-    /// Key: comment ID, Value: Editor entity for inline editing.
-    pub inline_edit_editors: HashMap<usize, Entity<Editor>>,
-    /// Subscriptions for inline edit editors' action handlers.
-    /// Key: comment ID, Value: Subscription keeping the Newline action handler alive.
-    pub inline_edit_subscriptions: HashMap<usize, Subscription>,
-    /// The current user's avatar URI for display in comment rows.
-    pub user_avatar_uri: Option<SharedUri>,
-    /// Subscription to keep the action handler alive.
-    _subscription: Subscription,
-}
-
 enum CodeActionsForSelection {
     None,
     Fetching(Shared<Task<Option<ActionFetchReady>>>),
@@ -2913,6 +2784,61 @@ impl Editor {
         self.last_bounds.as_ref()
     }
 
+    pub fn working_directory(&self, cx: &App) -> Option<PathBuf> {
+        if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+            if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local())
+                && let Some(dir) = file.abs_path(cx).parent()
+            {
+                return Some(dir.to_owned());
+            }
+        }
+
+        None
+    }
+
+    pub fn target_file_abs_path(&self, cx: &mut Context<Self>) -> Option<PathBuf> {
+        self.active_buffer(cx).and_then(|buffer| {
+            let buffer = buffer.read(cx);
+            if let Some(project_path) = buffer.project_path(cx) {
+                let project = self.project()?.read(cx);
+                project.absolute_path(&project_path, cx)
+            } else {
+                buffer
+                    .file()
+                    .and_then(|file| file.as_local().map(|file| file.abs_path(cx)))
+            }
+        })
+    }
+
+    /// Returns the project path for the editor's buffer, if any buffer is
+    /// opened in the editor.
+    pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
+        if let Some(buffer) = self.buffer.read(cx).as_singleton() {
+            buffer.read(cx).project_path(cx)
+        } else {
+            None
+        }
+    }
+
+    pub fn selection_menu_enabled(&self, cx: &App) -> bool {
+        self.show_selection_menu
+            .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu)
+    }
+
+    pub fn toggle_selection_menu(
+        &mut self,
+        _: &ToggleSelectionMenu,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.show_selection_menu = self
+            .show_selection_menu
+            .map(|show_selections_menu| !show_selections_menu)
+            .or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu));
+
+        cx.notify();
+    }
+
     fn accept_edit_prediction_keystroke(
         &self,
         granularity: EditPredictionGranularity,
@@ -3820,162 +3746,10 @@ impl Editor {
         Ok(())
     }
 
-    fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() {
-            self.show_git_blame_inline = false;
-
-            self.show_git_blame_inline_delay_task =
-                Some(cx.spawn_in(window, async move |this, cx| {
-                    cx.background_executor().timer(delay).await;
-
-                    this.update(cx, |this, cx| {
-                        this.show_git_blame_inline = true;
-                        cx.notify();
-                    })
-                    .log_err();
-                }));
-        }
-    }
-
-    pub fn blame_hover(&mut self, _: &BlameHover, window: &mut Window, cx: &mut Context<Self>) {
-        let snapshot = self.snapshot(window, cx);
-        let cursor = self
-            .selections
-            .newest::<Point>(&snapshot.display_snapshot)
-            .head();
-        let Some((buffer, point)) = snapshot.buffer_snapshot().point_to_buffer_point(cursor) else {
-            return;
-        };
-
-        if self.blame.is_none() {
-            self.start_git_blame(true, window, cx);
-        }
-        let Some(blame) = self.blame.as_ref() else {
-            return;
-        };
-
-        let row_info = RowInfo {
-            buffer_id: Some(buffer.remote_id()),
-            buffer_row: Some(point.row),
-            ..Default::default()
-        };
-        let Some((buffer, blame_entry)) = blame
-            .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
-            .flatten()
-        else {
-            return;
-        };
-
-        let anchor = self.selections.newest_anchor().head();
-        let position = self.to_pixel_point(anchor, &snapshot, window, cx);
-        if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) {
-            self.show_blame_popover(
-                buffer,
-                &blame_entry,
-                position + last_bounds.origin,
-                true,
-                cx,
-            );
-        };
-    }
-
-    fn show_blame_popover(
-        &mut self,
-        buffer: BufferId,
-        blame_entry: &BlameEntry,
-        position: gpui::Point<Pixels>,
-        ignore_timeout: bool,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(state) = &mut self.inline_blame_popover {
-            state.hide_task.take();
-        } else {
-            let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
-            let blame_entry = blame_entry.clone();
-            let show_task = cx.spawn(async move |editor, cx| {
-                if !ignore_timeout {
-                    cx.background_executor()
-                        .timer(std::time::Duration::from_millis(blame_popover_delay))
-                        .await;
-                }
-                editor
-                    .update(cx, |editor, cx| {
-                        editor.inline_blame_popover_show_task.take();
-                        let Some(blame) = editor.blame.as_ref() else {
-                            return;
-                        };
-                        let blame = blame.read(cx);
-                        let details = blame.details_for_entry(buffer, &blame_entry);
-                        let markdown = cx.new(|cx| {
-                            Markdown::new(
-                                details
-                                    .as_ref()
-                                    .map(|message| message.message.clone())
-                                    .unwrap_or_default(),
-                                None,
-                                None,
-                                cx,
-                            )
-                        });
-                        editor.inline_blame_popover = Some(InlineBlamePopover {
-                            position,
-                            hide_task: None,
-                            popover_bounds: None,
-                            popover_state: InlineBlamePopoverState {
-                                scroll_handle: ScrollHandle::new(),
-                                commit_message: details,
-                                markdown,
-                            },
-                            keyboard_grace: ignore_timeout,
-                        });
-                        cx.notify();
-                    })
-                    .ok();
-            });
-            self.inline_blame_popover_show_task = Some(show_task);
-        }
-    }
-
     pub fn has_mouse_context_menu(&self) -> bool {
         self.mouse_context_menu.is_some()
     }
 
-    /// Hides the inline blame popover element, in case it's already visible, or
-    /// interrupts the task meant to show it, in case the task is running.
-    ///
-    /// When `ignore_timeout` is set to `true`, the popover is hidden
-    /// immediately, otherwise it'll be hidden after a short delay.
-    ///
-    /// Returns `true` if the popover was visible and was hidden, `false`
-    /// otherwise.
-    pub fn hide_blame_popover(&mut self, ignore_timeout: bool, cx: &mut Context<Self>) -> bool {
-        self.inline_blame_popover_show_task.take();
-
-        if let Some(state) = &mut self.inline_blame_popover {
-            if ignore_timeout {
-                self.inline_blame_popover.take();
-                cx.notify();
-            } else {
-                state.hide_task = Some(cx.spawn(async move |editor, cx| {
-                    cx.background_executor()
-                        .timer(std::time::Duration::from_millis(100))
-                        .await;
-
-                    editor
-                        .update(cx, |editor, cx| {
-                            editor.inline_blame_popover.take();
-                            cx.notify();
-                        })
-                        .ok();
-                }));
-            }
-
-            true
-        } else {
-            false
-        }
-    }
-
     fn refresh_document_highlights(&mut self, cx: &mut Context<Self>) -> Option<()> {
         if self.pending_rename.is_some() {
             return None;
@@ -8287,152 +8061,6 @@ impl Editor {
         self.detach_and_notify_err(task, window, cx);
     }
 
-    pub fn restore_file(
-        &mut self,
-        _: &::git::RestoreFile,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.read_only(cx) {
-            return;
-        }
-        let mut buffer_ids = HashSet::default();
-        let snapshot = self.buffer().read(cx).snapshot(cx);
-        for selection in self
-            .selections
-            .all::<MultiBufferOffset>(&self.display_snapshot(cx))
-        {
-            buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range()))
-        }
-
-        let ranges = buffer_ids
-            .into_iter()
-            .flat_map(|buffer_id| snapshot.range_for_buffer(buffer_id))
-            .collect::<Vec<_>>();
-
-        self.restore_hunks_in_ranges(ranges, window, cx);
-    }
-
-    pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context<Self>) {
-        if self.read_only(cx) {
-            return;
-        }
-        let selections = self
-            .selections
-            .all(&self.display_snapshot(cx))
-            .into_iter()
-            .map(|s| s.range())
-            .collect();
-        self.restore_hunks_in_ranges(selections, window, cx);
-    }
-
-    /// Restores the diff hunks in the editor's selections and moves the cursor
-    /// to the next diff hunk. Wraps around to the beginning of the buffer if
-    /// not all diff hunks are expanded.
-    pub fn restore_and_next(
-        &mut self,
-        _: &::git::RestoreAndNext,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.read_only(cx) {
-            return;
-        }
-        let selections = self
-            .selections
-            .all(&self.display_snapshot(cx))
-            .into_iter()
-            .map(|selection| selection.range())
-            .collect();
-
-        self.restore_hunks_in_ranges(selections, window, cx);
-
-        let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
-        let wrap_around = !all_diff_hunks_expanded;
-        let snapshot = self.snapshot(window, cx);
-        let position = self
-            .selections
-            .newest::<Point>(&snapshot.display_snapshot)
-            .head();
-
-        self.go_to_hunk_before_or_after_position(
-            &snapshot,
-            position,
-            Direction::Next,
-            wrap_around,
-            window,
-            cx,
-        );
-    }
-
-    pub fn restore_hunks_in_ranges(
-        &mut self,
-        ranges: Vec<Range<Point>>,
-        window: &mut Window,
-        cx: &mut Context<Editor>,
-    ) {
-        if self.delegate_stage_and_restore {
-            let hunks = self.snapshot(window, cx).hunks_for_ranges(ranges);
-            if !hunks.is_empty() {
-                cx.emit(EditorEvent::RestoreRequested { hunks });
-            }
-            return;
-        }
-        let hunks = self.snapshot(window, cx).hunks_for_ranges(ranges);
-        self.transact(window, cx, |editor, window, cx| {
-            editor.restore_diff_hunks(hunks, cx);
-            let selections = editor
-                .selections
-                .all::<MultiBufferOffset>(&editor.display_snapshot(cx));
-            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
-                s.select(selections);
-            });
-        });
-    }
-
-    pub(crate) fn restore_diff_hunks(&self, hunks: Vec<MultiBufferDiffHunk>, cx: &mut App) {
-        let mut revert_changes = HashMap::default();
-        let chunk_by = hunks.into_iter().chunk_by(|hunk| hunk.buffer_id);
-        for (buffer_id, hunks) in &chunk_by {
-            let hunks = hunks.collect::<Vec<_>>();
-            for hunk in &hunks {
-                self.prepare_restore_change(&mut revert_changes, hunk, cx);
-            }
-            self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx);
-        }
-        if !revert_changes.is_empty() {
-            self.buffer().update(cx, |multi_buffer, cx| {
-                for (buffer_id, changes) in revert_changes {
-                    if let Some(buffer) = multi_buffer.buffer(buffer_id) {
-                        buffer.update(cx, |buffer, cx| {
-                            buffer.edit(
-                                changes
-                                    .into_iter()
-                                    .map(|(range, text)| (range, text.to_string())),
-                                None,
-                                cx,
-                            );
-                        });
-                    }
-                }
-            });
-        }
-    }
-
-    pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
-        if let Some(status) = self
-            .addons
-            .iter()
-            .find_map(|(_, addon)| addon.override_status_for_buffer_id(buffer_id, cx))
-        {
-            return Some(status);
-        }
-        self.project
-            .as_ref()?
-            .read(cx)
-            .status_for_buffer_id(buffer_id, cx)
-    }
-
     pub fn open_active_item_in_terminal(
         &mut self,
         _: &OpenInTerminal,
@@ -8923,43 +8551,64 @@ impl Editor {
         self.breakpoint_store.clone()
     }
 
-    pub fn prepare_restore_change(
-        &self,
-        revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
-        hunk: &MultiBufferDiffHunk,
-        cx: &mut App,
-    ) -> Option<()> {
-        if hunk.is_created_file() {
-            return None;
-        }
-        let multi_buffer = self.buffer.read(cx);
-        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
-        let diff_snapshot = multi_buffer_snapshot.diff_for_buffer_id(hunk.buffer_id)?;
-        let original_text = diff_snapshot
-            .base_text()
-            .as_rope()
-            .slice(hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0);
-        let buffer = multi_buffer.buffer(hunk.buffer_id)?;
-        let buffer = buffer.read(cx);
-        let buffer_snapshot = buffer.snapshot();
-        let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default();
-        if let Err(i) = buffer_revert_changes.binary_search_by(|probe| {
-            probe
-                .0
-                .start
-                .cmp(&hunk.buffer_range.start, &buffer_snapshot)
-                .then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot))
-        }) {
-            buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text));
-            Some(())
-        } else {
-            None
-        }
-    }
+    fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
+        maybe!({
+            let breakpoint_store = self.breakpoint_store.as_ref()?;
 
-    pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context<Self>) {
-        self.manipulate_immutable_lines(window, cx, |lines| lines.reverse())
-    }
+            let (active_stack_frame, debug_line_pane_id) = {
+                let store = breakpoint_store.read(cx);
+                let active_stack_frame = store.active_position().cloned();
+                let debug_line_pane_id = store.active_debug_line_pane_id();
+                (active_stack_frame, debug_line_pane_id)
+            };
+
+            let Some(active_stack_frame) = active_stack_frame else {
+                self.clear_row_highlights::<ActiveDebugLine>();
+                return None;
+            };
+
+            if let Some(debug_line_pane_id) = debug_line_pane_id {
+                if let Some(workspace) = self
+                    .workspace
+                    .as_ref()
+                    .and_then(|(workspace, _)| workspace.upgrade())
+                {
+                    let editor_pane_id = workspace
+                        .read(cx)
+                        .pane_for_item_id(cx.entity_id())
+                        .map(|pane| pane.entity_id());
+
+                    if editor_pane_id.is_some_and(|id| id != debug_line_pane_id) {
+                        self.clear_row_highlights::<ActiveDebugLine>();
+                        return None;
+                    }
+                }
+            }
+
+            let position = active_stack_frame.position;
+
+            let snapshot = self.buffer.read(cx).snapshot(cx);
+            let multibuffer_anchor = snapshot.anchor_in_excerpt(position)?;
+
+            self.clear_row_highlights::<ActiveDebugLine>();
+
+            self.go_to_line::<ActiveDebugLine>(
+                multibuffer_anchor,
+                Some(cx.theme().colors().editor_debugger_active_line_background),
+                window,
+                cx,
+            );
+
+            cx.notify();
+
+            Some(())
+        })
+        .is_some()
+    }
+
+    pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context<Self>) {
+        self.manipulate_immutable_lines(window, cx, |lines| lines.reverse())
+    }
 
     pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context<Self>) {
         self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut rand::rng()))
@@ -14054,102 +13703,6 @@ impl Editor {
         );
     }
 
-    pub fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
-        let snapshot = self.snapshot(window, cx);
-        let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
-        self.go_to_hunk_before_or_after_position(
-            &snapshot,
-            selection.head(),
-            Direction::Next,
-            true,
-            window,
-            cx,
-        );
-    }
-
-    pub fn go_to_hunk_before_or_after_position(
-        &mut self,
-        snapshot: &EditorSnapshot,
-        position: Point,
-        direction: Direction,
-        wrap_around: bool,
-        window: &mut Window,
-        cx: &mut Context<Editor>,
-    ) {
-        let row = if direction == Direction::Next {
-            self.hunk_after_position(snapshot, position, wrap_around)
-                .map(|hunk| hunk.row_range.start)
-        } else {
-            self.hunk_before_position(snapshot, position, wrap_around)
-        };
-
-        if let Some(row) = row {
-            let destination = Point::new(row.0, 0);
-            let autoscroll = Autoscroll::center();
-
-            self.unfold_ranges(&[destination..destination], false, false, cx);
-            self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
-                s.select_ranges([destination..destination]);
-            });
-        }
-    }
-
-    fn hunk_after_position(
-        &mut self,
-        snapshot: &EditorSnapshot,
-        position: Point,
-        wrap_around: bool,
-    ) -> Option<MultiBufferDiffHunk> {
-        let result = snapshot
-            .buffer_snapshot()
-            .diff_hunks_in_range(position..snapshot.buffer_snapshot().max_point())
-            .find(|hunk| hunk.row_range.start.0 > position.row);
-
-        if wrap_around {
-            result.or_else(|| {
-                snapshot
-                    .buffer_snapshot()
-                    .diff_hunks_in_range(Point::zero()..position)
-                    .find(|hunk| hunk.row_range.end.0 < position.row)
-            })
-        } else {
-            result
-        }
-    }
-
-    fn go_to_prev_hunk(
-        &mut self,
-        _: &GoToPreviousHunk,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let snapshot = self.snapshot(window, cx);
-        let selection = self.selections.newest::<Point>(&snapshot.display_snapshot);
-        self.go_to_hunk_before_or_after_position(
-            &snapshot,
-            selection.head(),
-            Direction::Prev,
-            true,
-            window,
-            cx,
-        );
-    }
-
-    fn hunk_before_position(
-        &mut self,
-        snapshot: &EditorSnapshot,
-        position: Point,
-        wrap_around: bool,
-    ) -> Option<MultiBufferRow> {
-        let result = snapshot.buffer_snapshot().diff_hunk_before(position);
-
-        if wrap_around {
-            result.or_else(|| snapshot.buffer_snapshot().diff_hunk_before(Point::MAX))
-        } else {
-            result
-        }
-    }
-
     fn go_to_next_change(
         &mut self,
         _: &GoToNextChange,
@@ -16093,49 +15646,6 @@ impl Editor {
         workspace.activate_item(&item, true, true, window, cx);
     }
 
-    pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) {
-        self.buffer.update(cx, |buffer, cx| {
-            buffer.set_all_diff_hunks_expanded(cx);
-        });
-    }
-
-    pub fn expand_all_diff_hunks(
-        &mut self,
-        _: &ExpandAllDiffHunks,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.buffer.update(cx, |buffer, cx| {
-            buffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx)
-        });
-    }
-
-    pub fn collapse_all_diff_hunks(
-        &mut self,
-        _: &CollapseAllDiffHunks,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.buffer.update(cx, |buffer, cx| {
-            buffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], cx)
-        });
-    }
-
-    pub fn toggle_selected_diff_hunks(
-        &mut self,
-        _: &ToggleSelectedDiffHunks,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let ranges: Vec<_> = self
-            .selections
-            .disjoint_anchors()
-            .iter()
-            .map(|s| s.range())
-            .collect();
-        self.toggle_diff_hunks_in_ranges(ranges, cx);
-    }
-
     pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut Context<Self>) {
         if hovered != self.gutter_hovered {
             self.gutter_hovered = hovered;
@@ -16331,1400 +15841,144 @@ impl Editor {
             .filter(|_| self.minimap_visibility.visible())
     }
 
-    pub fn show_diff_review_button(&self) -> bool {
-        self.show_diff_review_button
+    pub fn set_masked(&mut self, masked: bool, cx: &mut Context<Self>) {
+        if self.display_map.read(cx).masked != masked {
+            self.display_map.update(cx, |map, _| map.masked = masked);
+        }
+        cx.notify()
     }
 
-    pub fn render_diff_review_button(
-        &self,
-        display_row: DisplayRow,
-        width: Pixels,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let text_color = cx.theme().colors().text;
-        let icon_color = cx.theme().colors().icon_accent;
-
-        h_flex()
-            .id("diff_review_button")
-            .cursor_pointer()
-            .w(width - px(1.))
-            .h(relative(0.9))
-            .justify_center()
-            .rounded_sm()
-            .border_1()
-            .border_color(text_color.opacity(0.1))
-            .bg(text_color.opacity(0.15))
-            .hover(|s| {
-                s.bg(icon_color.opacity(0.4))
-                    .border_color(icon_color.opacity(0.5))
-            })
-            .child(Icon::new(IconName::Plus).size(IconSize::Small))
-            .tooltip(Tooltip::text("Add Review (drag to select multiple lines)"))
-            .on_mouse_down(
-                gpui::MouseButton::Left,
-                cx.listener(move |editor, _event: &gpui::MouseDownEvent, window, cx| {
-                    editor.start_diff_review_drag(display_row, window, cx);
-                }),
-            )
+    fn target_file<'a>(&self, cx: &'a App) -> Option<&'a dyn language::LocalFile> {
+        self.active_buffer(cx)?
+            .read(cx)
+            .file()
+            .and_then(|f| f.as_local())
     }
 
-    pub fn start_diff_review_drag(
+    fn reveal_in_finder(
         &mut self,
-        display_row: DisplayRow,
-        window: &mut Window,
+        _: &RevealInFileManager,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let snapshot = self.snapshot(window, cx);
-        let point = snapshot
-            .display_snapshot
-            .display_point_to_point(DisplayPoint::new(display_row, 0), Bias::Left);
-        let anchor = snapshot.buffer_snapshot().anchor_before(point);
-        self.diff_review_drag_state = Some(DiffReviewDragState {
-            start_anchor: anchor,
-            current_anchor: anchor,
-        });
-        cx.notify();
+        if let Some(path) = self.target_file_abs_path(cx) {
+            if let Some(project) = self.project() {
+                project.update(cx, |project, cx| project.reveal_path(&path, cx));
+            } else {
+                cx.reveal_path(&path);
+            }
+        }
     }
 
-    pub fn update_diff_review_drag(
+    fn copy_path(
         &mut self,
-        display_row: DisplayRow,
-        window: &mut Window,
+        _: &zed_actions::workspace::CopyPath,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if self.diff_review_drag_state.is_none() {
-            return;
-        }
-        let snapshot = self.snapshot(window, cx);
-        let point = snapshot
-            .display_snapshot
-            .display_point_to_point(display_row.as_display_point(), Bias::Left);
-        let anchor = snapshot.buffer_snapshot().anchor_before(point);
-        if let Some(drag_state) = &mut self.diff_review_drag_state {
-            drag_state.current_anchor = anchor;
-            cx.notify();
+        if let Some(path) = self.target_file_abs_path(cx)
+            && let Some(path) = path.to_str()
+        {
+            cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
+        } else {
+            cx.propagate();
         }
     }
 
-    pub fn end_diff_review_drag(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(drag_state) = self.diff_review_drag_state.take() {
-            let snapshot = self.snapshot(window, cx);
-            let range = drag_state.row_range(&snapshot.display_snapshot);
-            self.show_diff_review_overlay(*range.start()..*range.end(), window, cx);
+    fn copy_relative_path(
+        &mut self,
+        _: &zed_actions::workspace::CopyRelativePath,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(path) = self.active_buffer(cx).and_then(|buffer| {
+            let project = self.project()?.read(cx);
+            let path = buffer.read(cx).file()?.path();
+            let path = path.display(project.path_style(cx));
+            Some(path)
+        }) {
+            cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
+        } else {
+            cx.propagate();
         }
-        cx.notify();
     }
 
-    pub fn cancel_diff_review_drag(&mut self, cx: &mut Context<Self>) {
-        self.diff_review_drag_state = None;
-        cx.notify();
+    pub fn copy_file_name_without_extension(
+        &mut self,
+        _: &CopyFileNameWithoutExtension,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(file_stem) = self.active_buffer(cx).and_then(|buffer| {
+            let file = buffer.read(cx).file()?;
+            file.path().file_stem()
+        }) {
+            cx.write_to_clipboard(ClipboardItem::new_string(file_stem.to_string()));
+        }
     }
 
-    /// Calculates the appropriate block height for the diff review overlay.
-    /// Height is in lines: 2 for input row, 1 for header when comments exist,
-    /// and 2 lines per comment when expanded.
-    fn calculate_overlay_height(
-        &self,
-        hunk_key: &DiffHunkKey,
-        comments_expanded: bool,
-        snapshot: &MultiBufferSnapshot,
-    ) -> u32 {
-        let comment_count = self.hunk_comment_count(hunk_key, snapshot);
-        let base_height: u32 = 2; // Input row with avatar and buttons
-
-        if comment_count == 0 {
-            base_height
-        } else if comments_expanded {
-            // Header (1 line) + 2 lines per comment
-            base_height + 1 + (comment_count as u32 * 2)
-        } else {
-            // Just header when collapsed
-            base_height + 1
+    pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) {
+        if let Some(file_name) = self.active_buffer(cx).and_then(|buffer| {
+            let file = buffer.read(cx).file()?;
+            Some(file.file_name(cx))
+        }) {
+            cx.write_to_clipboard(ClipboardItem::new_string(file_name.to_string()));
         }
     }
 
-    pub fn show_diff_review_overlay(
+    pub fn copy_file_location(
         &mut self,
-        display_range: Range<DisplayRow>,
-        window: &mut Window,
+        _: &CopyFileLocation,
+        _: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Range { start, end } = display_range.sorted();
+        let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
 
-        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
-        let editor_snapshot = self.snapshot(window, cx);
+        let start_line = selection.start.row + 1;
+        let end_line = selection.end.row + 1;
 
-        // Convert display rows to multibuffer points
-        let start_point = editor_snapshot
-            .display_snapshot
-            .display_point_to_point(start.as_display_point(), Bias::Left);
-        let end_point = editor_snapshot
-            .display_snapshot
-            .display_point_to_point(end.as_display_point(), Bias::Left);
-        let end_multi_buffer_row = MultiBufferRow(end_point.row);
-
-        // Create anchor range for the selected lines (start of first line to end of last line)
-        let line_end = Point::new(
-            end_point.row,
-            buffer_snapshot.line_len(end_multi_buffer_row),
-        );
-        let anchor_range =
-            buffer_snapshot.anchor_after(start_point)..buffer_snapshot.anchor_before(line_end);
-
-        // Compute the hunk key for this display row
-        let file_path = buffer_snapshot
-            .file_at(start_point)
-            .map(|file: &Arc<dyn language::File>| file.path().clone())
-            .unwrap_or_else(|| Arc::from(util::rel_path::RelPath::empty()));
-        let hunk_start_anchor = buffer_snapshot.anchor_before(start_point);
-        let new_hunk_key = DiffHunkKey {
-            file_path,
-            hunk_start_anchor,
+        let end_line = if selection.end.column == 0 && end_line > start_line {
+            end_line - 1
+        } else {
+            end_line
         };
 
-        // Check if we already have an overlay for this hunk
-        if let Some(existing_overlay) = self.diff_review_overlays.iter().find(|overlay| {
-            Self::hunk_keys_match(&overlay.hunk_key, &new_hunk_key, &buffer_snapshot)
+        if let Some(file_location) = self.active_buffer(cx).and_then(|buffer| {
+            let project = self.project()?.read(cx);
+            let file = buffer.read(cx).file()?;
+            let path = file.path().display(project.path_style(cx));
+
+            let location = if start_line == end_line {
+                format!("{path}:{start_line}")
+            } else {
+                format!("{path}:{start_line}-{end_line}")
+            };
+            Some(location)
         }) {
-            // Just focus the existing overlay's prompt editor
-            let focus_handle = existing_overlay.prompt_editor.focus_handle(cx);
-            window.focus(&focus_handle, cx);
-            return;
+            cx.write_to_clipboard(ClipboardItem::new_string(file_location));
         }
+    }
 
-        // Dismiss overlays that have no comments for their hunks
-        self.dismiss_overlays_without_comments(cx);
-
-        // Get the current user's avatar URI from the project's user_store
-        let user_avatar_uri = self.project.as_ref().and_then(|project| {
-            let user_store = project.read(cx).user_store();
-            user_store
-                .read(cx)
-                .current_user()
-                .map(|user| user.avatar_uri.clone())
-        });
-
-        // Create anchor at the end of the last row so the block appears immediately below it
-        // Use multibuffer coordinates for anchor creation
-        let line_len = buffer_snapshot.line_len(end_multi_buffer_row);
-        let anchor = buffer_snapshot.anchor_after(Point::new(end_multi_buffer_row.0, line_len));
-
-        // Use the hunk key we already computed
-        let hunk_key = new_hunk_key;
-
-        // Create the prompt editor for the review input
-        let prompt_editor = cx.new(|cx| {
-            let mut editor = Editor::single_line(window, cx);
-            editor.set_placeholder_text("Add a review comment...", window, cx);
-            editor
-        });
-
-        // Register the Newline action on the prompt editor to submit the review
-        let parent_editor = cx.entity().downgrade();
-        let subscription = prompt_editor.update(cx, |prompt_editor, _cx| {
-            prompt_editor.register_action({
-                let parent_editor = parent_editor.clone();
-                move |_: &crate::actions::Newline, window, cx| {
-                    if let Some(editor) = parent_editor.upgrade() {
-                        editor.update(cx, |editor, cx| {
-                            editor.submit_diff_review_comment(window, cx);
-                        });
-                    }
-                }
-            })
-        });
-
-        // Calculate initial height based on existing comments for this hunk
-        let initial_height = self.calculate_overlay_height(&hunk_key, true, &buffer_snapshot);
-
-        // Create the overlay block
-        let prompt_editor_for_render = prompt_editor.clone();
-        let hunk_key_for_render = hunk_key.clone();
-        let editor_handle = cx.entity().downgrade();
-        let block = BlockProperties {
-            style: BlockStyle::Sticky,
-            placement: BlockPlacement::Below(anchor),
-            height: Some(initial_height),
-            render: Arc::new(move |cx| {
-                Self::render_diff_review_overlay(
-                    &prompt_editor_for_render,
-                    &hunk_key_for_render,
-                    &editor_handle,
-                    cx,
-                )
-            }),
-            priority: 0,
-        };
-
-        let block_ids = self.insert_blocks([block], None, cx);
-        let Some(block_id) = block_ids.into_iter().next() else {
-            log::error!("Failed to insert diff review overlay block");
-            return;
-        };
-
-        self.diff_review_overlays.push(DiffReviewOverlay {
-            anchor_range,
-            block_id,
-            prompt_editor: prompt_editor.clone(),
-            hunk_key,
-            comments_expanded: true,
-            inline_edit_editors: HashMap::default(),
-            inline_edit_subscriptions: HashMap::default(),
-            user_avatar_uri,
-            _subscription: subscription,
-        });
-
-        // Focus the prompt editor
-        let focus_handle = prompt_editor.focus_handle(cx);
-        window.focus(&focus_handle, cx);
+    pub fn insert_uuid_v4(
+        &mut self,
+        _: &InsertUuidV4,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.insert_uuid(UuidVersion::V4, window, cx);
+    }
 
-        cx.notify();
+    pub fn insert_uuid_v7(
+        &mut self,
+        _: &InsertUuidV7,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.insert_uuid(UuidVersion::V7, window, cx);
     }
 
-    /// Dismisses all diff review overlays.
-    pub fn dismiss_all_diff_review_overlays(&mut self, cx: &mut Context<Self>) {
-        if self.diff_review_overlays.is_empty() {
-            return;
-        }
-        let block_ids: HashSet<_> = self
-            .diff_review_overlays
-            .drain(..)
-            .map(|overlay| overlay.block_id)
-            .collect();
-        self.remove_blocks(block_ids, None, cx);
-        cx.notify();
-    }
-
-    /// Dismisses overlays that have no comments stored for their hunks.
-    /// Keeps overlays that have at least one comment.
-    fn dismiss_overlays_without_comments(&mut self, cx: &mut Context<Self>) {
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-
-        // First, compute which overlays have comments (to avoid borrow issues with retain)
-        let overlays_with_comments: Vec<bool> = self
-            .diff_review_overlays
-            .iter()
-            .map(|overlay| self.hunk_comment_count(&overlay.hunk_key, &snapshot) > 0)
-            .collect();
-
-        // Now collect block IDs to remove and retain overlays
-        let mut block_ids_to_remove = HashSet::default();
-        let mut index = 0;
-        self.diff_review_overlays.retain(|overlay| {
-            let has_comments = overlays_with_comments[index];
-            index += 1;
-            if !has_comments {
-                block_ids_to_remove.insert(overlay.block_id);
-            }
-            has_comments
-        });
-
-        if !block_ids_to_remove.is_empty() {
-            self.remove_blocks(block_ids_to_remove, None, cx);
-            cx.notify();
-        }
-    }
-
-    /// Refreshes the diff review overlay block to update its height and render function.
-    /// Uses resize_blocks and replace_blocks to avoid visual flicker from remove+insert.
-    fn refresh_diff_review_overlay_height(
-        &mut self,
-        hunk_key: &DiffHunkKey,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        // Extract all needed data from overlay first to avoid borrow conflicts
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let (comments_expanded, block_id, prompt_editor) = {
-            let Some(overlay) = self
-                .diff_review_overlays
-                .iter()
-                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot))
-            else {
-                return;
-            };
-
-            (
-                overlay.comments_expanded,
-                overlay.block_id,
-                overlay.prompt_editor.clone(),
-            )
-        };
-
-        // Calculate new height
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let new_height = self.calculate_overlay_height(hunk_key, comments_expanded, &snapshot);
-
-        // Update the block height using resize_blocks (avoids flicker)
-        let mut heights = HashMap::default();
-        heights.insert(block_id, new_height);
-        self.resize_blocks(heights, None, cx);
-
-        // Update the render function using replace_blocks (avoids flicker)
-        let hunk_key_for_render = hunk_key.clone();
-        let editor_handle = cx.entity().downgrade();
-        let render: Arc<dyn Fn(&mut BlockContext) -> AnyElement + Send + Sync> =
-            Arc::new(move |cx| {
-                Self::render_diff_review_overlay(
-                    &prompt_editor,
-                    &hunk_key_for_render,
-                    &editor_handle,
-                    cx,
-                )
-            });
-
-        let mut renderers = HashMap::default();
-        renderers.insert(block_id, render);
-        self.replace_blocks(renderers, None, cx);
-    }
-
-    /// Action handler for SubmitDiffReviewComment.
-    pub fn submit_diff_review_comment_action(
-        &mut self,
-        _: &SubmitDiffReviewComment,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.submit_diff_review_comment(window, cx);
-    }
-
-    /// Stores the diff review comment locally.
-    /// Comments are stored per-hunk and can later be batch-submitted to the Agent panel.
-    pub fn submit_diff_review_comment(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        // Find the overlay that currently has focus
-        let overlay_index = self
-            .diff_review_overlays
-            .iter()
-            .position(|overlay| overlay.prompt_editor.focus_handle(cx).is_focused(window));
-        let Some(overlay_index) = overlay_index else {
-            return;
-        };
-        let overlay = &self.diff_review_overlays[overlay_index];
-
-        let comment_text = overlay.prompt_editor.read(cx).text(cx).trim().to_string();
-        if comment_text.is_empty() {
-            return;
-        }
-
-        let anchor_range = overlay.anchor_range.clone();
-        let hunk_key = overlay.hunk_key.clone();
-
-        self.add_review_comment(hunk_key.clone(), comment_text, anchor_range, cx);
-
-        // Clear the prompt editor but keep the overlay open
-        if let Some(overlay) = self.diff_review_overlays.get(overlay_index) {
-            overlay.prompt_editor.update(cx, |editor, cx| {
-                editor.clear(window, cx);
-            });
-        }
-
-        // Refresh the overlay to update the block height for the new comment
-        self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
-
-        cx.notify();
-    }
-
-    /// Returns the prompt editor for the diff review overlay, if one is active.
-    /// This is primarily used for testing.
-    pub fn diff_review_prompt_editor(&self) -> Option<&Entity<Editor>> {
-        self.diff_review_overlays
-            .first()
-            .map(|overlay| &overlay.prompt_editor)
-    }
-
-    /// Returns the line range for the first diff review overlay, if one is active.
-    /// Returns (start_row, end_row) as physical line numbers in the underlying file.
-    pub fn diff_review_line_range(&self, cx: &App) -> Option<(u32, u32)> {
-        let overlay = self.diff_review_overlays.first()?;
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let start_point = overlay.anchor_range.start.to_point(&snapshot);
-        let end_point = overlay.anchor_range.end.to_point(&snapshot);
-        let start_row = snapshot
-            .point_to_buffer_point(start_point)
-            .map(|(_, p)| p.row)
-            .unwrap_or(start_point.row);
-        let end_row = snapshot
-            .point_to_buffer_point(end_point)
-            .map(|(_, p)| p.row)
-            .unwrap_or(end_point.row);
-        Some((start_row, end_row))
-    }
-
-    /// Sets whether the comments section is expanded in the diff review overlay.
-    /// This is primarily used for testing.
-    pub fn set_diff_review_comments_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) {
-        for overlay in &mut self.diff_review_overlays {
-            overlay.comments_expanded = expanded;
-        }
-        cx.notify();
-    }
-
-    /// Compares two DiffHunkKeys for equality by resolving their anchors.
-    fn hunk_keys_match(a: &DiffHunkKey, b: &DiffHunkKey, snapshot: &MultiBufferSnapshot) -> bool {
-        a.file_path == b.file_path
-            && a.hunk_start_anchor.to_point(snapshot) == b.hunk_start_anchor.to_point(snapshot)
-    }
-
-    /// Returns comments for a specific hunk, ordered by creation time.
-    pub fn comments_for_hunk<'a>(
-        &'a self,
-        key: &DiffHunkKey,
-        snapshot: &MultiBufferSnapshot,
-    ) -> &'a [StoredReviewComment] {
-        let key_point = key.hunk_start_anchor.to_point(snapshot);
-        self.stored_review_comments
-            .iter()
-            .find(|(k, _)| {
-                k.file_path == key.file_path && k.hunk_start_anchor.to_point(snapshot) == key_point
-            })
-            .map(|(_, comments)| comments.as_slice())
-            .unwrap_or(&[])
-    }
-
-    /// Returns the total count of stored review comments across all hunks.
-    pub fn total_review_comment_count(&self) -> usize {
-        self.stored_review_comments
-            .iter()
-            .map(|(_, v)| v.len())
-            .sum()
-    }
-
-    /// Returns the count of comments for a specific hunk.
-    pub fn hunk_comment_count(&self, key: &DiffHunkKey, snapshot: &MultiBufferSnapshot) -> usize {
-        let key_point = key.hunk_start_anchor.to_point(snapshot);
-        self.stored_review_comments
-            .iter()
-            .find(|(k, _)| {
-                k.file_path == key.file_path && k.hunk_start_anchor.to_point(snapshot) == key_point
-            })
-            .map(|(_, v)| v.len())
-            .unwrap_or(0)
-    }
-
-    /// Adds a new review comment to a specific hunk.
-    pub fn add_review_comment(
-        &mut self,
-        hunk_key: DiffHunkKey,
-        comment: String,
-        anchor_range: Range<Anchor>,
-        cx: &mut Context<Self>,
-    ) -> usize {
-        let id = self.next_review_comment_id;
-        self.next_review_comment_id += 1;
-
-        let stored_comment = StoredReviewComment::new(id, comment, anchor_range);
-
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let key_point = hunk_key.hunk_start_anchor.to_point(&snapshot);
-
-        // Find existing entry for this hunk or add a new one
-        if let Some((_, comments)) = self.stored_review_comments.iter_mut().find(|(k, _)| {
-            k.file_path == hunk_key.file_path
-                && k.hunk_start_anchor.to_point(&snapshot) == key_point
-        }) {
-            comments.push(stored_comment);
-        } else {
-            self.stored_review_comments
-                .push((hunk_key, vec![stored_comment]));
-        }
-
-        cx.emit(EditorEvent::ReviewCommentsChanged {
-            total_count: self.total_review_comment_count(),
-        });
-        cx.notify();
-        id
-    }
-
-    /// Removes a review comment by ID from any hunk.
-    pub fn remove_review_comment(&mut self, id: usize, cx: &mut Context<Self>) -> bool {
-        for (_, comments) in self.stored_review_comments.iter_mut() {
-            if let Some(index) = comments.iter().position(|c| c.id == id) {
-                comments.remove(index);
-                cx.emit(EditorEvent::ReviewCommentsChanged {
-                    total_count: self.total_review_comment_count(),
-                });
-                cx.notify();
-                return true;
-            }
-        }
-        false
-    }
-
-    /// Updates a review comment's text by ID.
-    pub fn update_review_comment(
-        &mut self,
-        id: usize,
-        new_comment: String,
-        cx: &mut Context<Self>,
-    ) -> bool {
-        for (_, comments) in self.stored_review_comments.iter_mut() {
-            if let Some(comment) = comments.iter_mut().find(|c| c.id == id) {
-                comment.comment = new_comment;
-                comment.is_editing = false;
-                cx.emit(EditorEvent::ReviewCommentsChanged {
-                    total_count: self.total_review_comment_count(),
-                });
-                cx.notify();
-                return true;
-            }
-        }
-        false
-    }
-
-    /// Sets a comment's editing state.
-    pub fn set_comment_editing(&mut self, id: usize, is_editing: bool, cx: &mut Context<Self>) {
-        for (_, comments) in self.stored_review_comments.iter_mut() {
-            if let Some(comment) = comments.iter_mut().find(|c| c.id == id) {
-                comment.is_editing = is_editing;
-                cx.notify();
-                return;
-            }
-        }
-    }
-
-    /// Takes all stored comments from all hunks, clearing the storage.
-    /// Returns a Vec of (hunk_key, comments) pairs.
-    pub fn take_all_review_comments(
-        &mut self,
-        cx: &mut Context<Self>,
-    ) -> Vec<(DiffHunkKey, Vec<StoredReviewComment>)> {
-        // Dismiss all overlays when taking comments (e.g., when sending to agent)
-        self.dismiss_all_diff_review_overlays(cx);
-        let comments = std::mem::take(&mut self.stored_review_comments);
-        // Reset the ID counter since all comments have been taken
-        self.next_review_comment_id = 0;
-        cx.emit(EditorEvent::ReviewCommentsChanged { total_count: 0 });
-        cx.notify();
-        comments
-    }
-
-    /// Removes review comments whose anchors are no longer valid or whose
-    /// associated diff hunks no longer exist.
-    ///
-    /// This should be called when the buffer changes to prevent orphaned comments
-    /// from accumulating.
-    pub fn cleanup_orphaned_review_comments(&mut self, cx: &mut Context<Self>) {
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let original_count = self.total_review_comment_count();
-
-        // Remove comments with invalid hunk anchors
-        self.stored_review_comments
-            .retain(|(hunk_key, _)| hunk_key.hunk_start_anchor.is_valid(&snapshot));
-
-        // Also clean up individual comments with invalid anchor ranges
-        for (_, comments) in &mut self.stored_review_comments {
-            comments.retain(|comment| {
-                comment.range.start.is_valid(&snapshot) && comment.range.end.is_valid(&snapshot)
-            });
-        }
-
-        // Remove empty hunk entries
-        self.stored_review_comments
-            .retain(|(_, comments)| !comments.is_empty());
-
-        let new_count = self.total_review_comment_count();
-        if new_count != original_count {
-            cx.emit(EditorEvent::ReviewCommentsChanged {
-                total_count: new_count,
-            });
-            cx.notify();
-        }
-    }
-
-    /// Toggles the expanded state of the comments section in the overlay.
-    pub fn toggle_review_comments_expanded(
-        &mut self,
-        _: &ToggleReviewCommentsExpanded,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        // Find the overlay that currently has focus, or use the first one
-        let overlay_info = self.diff_review_overlays.iter_mut().find_map(|overlay| {
-            if overlay.prompt_editor.focus_handle(cx).is_focused(window) {
-                overlay.comments_expanded = !overlay.comments_expanded;
-                Some(overlay.hunk_key.clone())
-            } else {
-                None
-            }
-        });
-
-        // If no focused overlay found, toggle the first one
-        let hunk_key = overlay_info.or_else(|| {
-            self.diff_review_overlays.first_mut().map(|overlay| {
-                overlay.comments_expanded = !overlay.comments_expanded;
-                overlay.hunk_key.clone()
-            })
-        });
-
-        if let Some(hunk_key) = hunk_key {
-            self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
-            cx.notify();
-        }
-    }
-
-    /// Handles the EditReviewComment action - sets a comment into editing mode.
-    pub fn edit_review_comment(
-        &mut self,
-        action: &EditReviewComment,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let comment_id = action.id;
-
-        // Set the comment to editing mode
-        self.set_comment_editing(comment_id, true, cx);
-
-        // Find the overlay that contains this comment and create an inline editor if needed
-        // First, find which hunk this comment belongs to
-        let hunk_key = self
-            .stored_review_comments
-            .iter()
-            .find_map(|(key, comments)| {
-                if comments.iter().any(|c| c.id == comment_id) {
-                    Some(key.clone())
-                } else {
-                    None
-                }
-            });
-
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        if let Some(hunk_key) = hunk_key {
-            if let Some(overlay) = self
-                .diff_review_overlays
-                .iter_mut()
-                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
-            {
-                if let std::collections::hash_map::Entry::Vacant(entry) =
-                    overlay.inline_edit_editors.entry(comment_id)
-                {
-                    // Find the comment text
-                    let comment_text = self
-                        .stored_review_comments
-                        .iter()
-                        .flat_map(|(_, comments)| comments)
-                        .find(|c| c.id == comment_id)
-                        .map(|c| c.comment.clone())
-                        .unwrap_or_default();
-
-                    // Create inline editor
-                    let parent_editor = cx.entity().downgrade();
-                    let inline_editor = cx.new(|cx| {
-                        let mut editor = Editor::single_line(window, cx);
-                        editor.set_text(&*comment_text, window, cx);
-                        // Select all text for easy replacement
-                        editor.select_all(&crate::actions::SelectAll, window, cx);
-                        editor
-                    });
-
-                    // Register the Newline action to confirm the edit
-                    let subscription = inline_editor.update(cx, |inline_editor, _cx| {
-                        inline_editor.register_action({
-                            let parent_editor = parent_editor.clone();
-                            move |_: &crate::actions::Newline, window, cx| {
-                                if let Some(editor) = parent_editor.upgrade() {
-                                    editor.update(cx, |editor, cx| {
-                                        editor.confirm_edit_review_comment(comment_id, window, cx);
-                                    });
-                                }
-                            }
-                        })
-                    });
-
-                    // Store the subscription to keep the action handler alive
-                    overlay
-                        .inline_edit_subscriptions
-                        .insert(comment_id, subscription);
-
-                    // Focus the inline editor
-                    let focus_handle = inline_editor.focus_handle(cx);
-                    window.focus(&focus_handle, cx);
-
-                    entry.insert(inline_editor);
-                }
-            }
-        }
-
-        cx.notify();
-    }
-
-    /// Confirms an inline edit of a review comment.
-    pub fn confirm_edit_review_comment(
-        &mut self,
-        comment_id: usize,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        // Get the new text from the inline editor
-        // Find the overlay containing this comment's inline editor
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let hunk_key = self
-            .stored_review_comments
-            .iter()
-            .find_map(|(key, comments)| {
-                if comments.iter().any(|c| c.id == comment_id) {
-                    Some(key.clone())
-                } else {
-                    None
-                }
-            });
-
-        let new_text = hunk_key
-            .as_ref()
-            .and_then(|hunk_key| {
-                self.diff_review_overlays
-                    .iter()
-                    .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot))
-            })
-            .as_ref()
-            .and_then(|overlay| overlay.inline_edit_editors.get(&comment_id))
-            .map(|editor| editor.read(cx).text(cx).trim().to_string());
-
-        if let Some(new_text) = new_text {
-            if !new_text.is_empty() {
-                self.update_review_comment(comment_id, new_text, cx);
-            }
-        }
-
-        // Remove the inline editor and its subscription
-        if let Some(hunk_key) = hunk_key {
-            if let Some(overlay) = self
-                .diff_review_overlays
-                .iter_mut()
-                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
-            {
-                overlay.inline_edit_editors.remove(&comment_id);
-                overlay.inline_edit_subscriptions.remove(&comment_id);
-            }
-        }
-
-        // Clear editing state
-        self.set_comment_editing(comment_id, false, cx);
-    }
-
-    /// Cancels an inline edit of a review comment.
-    pub fn cancel_edit_review_comment(
-        &mut self,
-        comment_id: usize,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        // Find which hunk this comment belongs to
-        let hunk_key = self
-            .stored_review_comments
-            .iter()
-            .find_map(|(key, comments)| {
-                if comments.iter().any(|c| c.id == comment_id) {
-                    Some(key.clone())
-                } else {
-                    None
-                }
-            });
-
-        // Remove the inline editor and its subscription
-        if let Some(hunk_key) = hunk_key {
-            let snapshot = self.buffer.read(cx).snapshot(cx);
-            if let Some(overlay) = self
-                .diff_review_overlays
-                .iter_mut()
-                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
-            {
-                overlay.inline_edit_editors.remove(&comment_id);
-                overlay.inline_edit_subscriptions.remove(&comment_id);
-            }
-        }
-
-        // Clear editing state
-        self.set_comment_editing(comment_id, false, cx);
-    }
-
-    /// Action handler for ConfirmEditReviewComment.
-    pub fn confirm_edit_review_comment_action(
-        &mut self,
-        action: &ConfirmEditReviewComment,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.confirm_edit_review_comment(action.id, window, cx);
-    }
-
-    /// Action handler for CancelEditReviewComment.
-    pub fn cancel_edit_review_comment_action(
-        &mut self,
-        action: &CancelEditReviewComment,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.cancel_edit_review_comment(action.id, window, cx);
-    }
-
-    /// Handles the DeleteReviewComment action - removes a comment.
-    pub fn delete_review_comment(
-        &mut self,
-        action: &DeleteReviewComment,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        // Get the hunk key before removing the comment
-        // Find the hunk key from the comment itself
-        let comment_id = action.id;
-        let hunk_key = self
-            .stored_review_comments
-            .iter()
-            .find_map(|(key, comments)| {
-                if comments.iter().any(|c| c.id == comment_id) {
-                    Some(key.clone())
-                } else {
-                    None
-                }
-            });
-
-        // Also get it from the overlay for refresh purposes
-        let overlay_hunk_key = self
-            .diff_review_overlays
-            .first()
-            .map(|o| o.hunk_key.clone());
-
-        self.remove_review_comment(action.id, cx);
-
-        // Refresh the overlay height after removing a comment
-        if let Some(hunk_key) = hunk_key.or(overlay_hunk_key) {
-            self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
-        }
-    }
-
-    fn render_diff_review_overlay(
-        prompt_editor: &Entity<Editor>,
-        hunk_key: &DiffHunkKey,
-        editor_handle: &WeakEntity<Editor>,
-        cx: &mut BlockContext,
-    ) -> AnyElement {
-        fn format_line_ranges(ranges: &[(u32, u32)]) -> Option<String> {
-            if ranges.is_empty() {
-                return None;
-            }
-            let formatted: Vec<String> = ranges
-                .iter()
-                .map(|(start, end)| {
-                    let start_line = start + 1;
-                    let end_line = end + 1;
-                    if start_line == end_line {
-                        format!("Line {start_line}")
-                    } else {
-                        format!("Lines {start_line}-{end_line}")
-                    }
-                })
-                .collect();
-            // Don't show label for single line in single excerpt
-            if ranges.len() == 1 && ranges[0].0 == ranges[0].1 {
-                return None;
-            }
-            Some(formatted.join(" ⋯ "))
-        }
-
-        let theme = cx.theme();
-        let colors = theme.colors();
-
-        let (comments, comments_expanded, inline_editors, user_avatar_uri, line_ranges) =
-            editor_handle
-                .upgrade()
-                .map(|editor| {
-                    let editor = editor.read(cx);
-                    let snapshot = editor.buffer().read(cx).snapshot(cx);
-                    let comments = editor.comments_for_hunk(hunk_key, &snapshot).to_vec();
-                    let (expanded, editors, avatar_uri, line_ranges) = editor
-                        .diff_review_overlays
-                        .iter()
-                        .find(|overlay| {
-                            Editor::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot)
-                        })
-                        .map(|o| {
-                            let start_point = o.anchor_range.start.to_point(&snapshot);
-                            let end_point = o.anchor_range.end.to_point(&snapshot);
-                            // Get line ranges per excerpt to detect discontinuities
-                            let buffer_ranges =
-                                snapshot.range_to_buffer_ranges(start_point..end_point);
-                            let ranges: Vec<(u32, u32)> = buffer_ranges
-                                .iter()
-                                .map(|(buffer_snapshot, range, _)| {
-                                    let start = buffer_snapshot.offset_to_point(range.start.0).row;
-                                    let end = buffer_snapshot.offset_to_point(range.end.0).row;
-                                    (start, end)
-                                })
-                                .collect();
-                            (
-                                o.comments_expanded,
-                                o.inline_edit_editors.clone(),
-                                o.user_avatar_uri.clone(),
-                                if ranges.is_empty() {
-                                    None
-                                } else {
-                                    Some(ranges)
-                                },
-                            )
-                        })
-                        .unwrap_or((true, HashMap::default(), None, None));
-                    (comments, expanded, editors, avatar_uri, line_ranges)
-                })
-                .unwrap_or((Vec::new(), true, HashMap::default(), None, None));
-
-        let comment_count = comments.len();
-        let avatar_size = px(20.);
-        let action_icon_size = IconSize::XSmall;
-
-        v_flex()
-            .w_full()
-            .bg(colors.editor_background)
-            .border_b_1()
-            .border_color(colors.border)
-            .px_2()
-            .pb_2()
-            .gap_2()
-            // Line range indicator (only shown for multi-line selections or multiple excerpts)
-            .when_some(line_ranges, |el, ranges| {
-                let label = format_line_ranges(&ranges);
-                if let Some(label) = label {
-                    el.child(
-                        h_flex()
-                            .w_full()
-                            .px_2()
-                            .child(Label::new(label).size(LabelSize::Small).color(Color::Muted)),
-                    )
-                } else {
-                    el
-                }
-            })
-            // Top row: editable input with user's avatar
-            .child(
-                h_flex()
-                    .w_full()
-                    .items_center()
-                    .gap_2()
-                    .px_2()
-                    .py_1p5()
-                    .rounded_md()
-                    .bg(colors.surface_background)
-                    .child(
-                        div()
-                            .size(avatar_size)
-                            .flex_shrink_0()
-                            .rounded_full()
-                            .overflow_hidden()
-                            .child(if let Some(ref avatar_uri) = user_avatar_uri {
-                                Avatar::new(avatar_uri.clone())
-                                    .size(avatar_size)
-                                    .into_any_element()
-                            } else {
-                                Icon::new(IconName::Person)
-                                    .size(IconSize::Small)
-                                    .color(ui::Color::Muted)
-                                    .into_any_element()
-                            }),
-                    )
-                    .child(
-                        div()
-                            .flex_1()
-                            .border_1()
-                            .border_color(colors.border)
-                            .rounded_md()
-                            .bg(colors.editor_background)
-                            .px_2()
-                            .py_1()
-                            .child(prompt_editor.clone()),
-                    )
-                    .child(
-                        h_flex()
-                            .flex_shrink_0()
-                            .gap_1()
-                            .child(
-                                IconButton::new("diff-review-close", IconName::Close)
-                                    .icon_color(ui::Color::Muted)
-                                    .icon_size(action_icon_size)
-                                    .tooltip(Tooltip::text("Close"))
-                                    .on_click(|_, window, cx| {
-                                        window
-                                            .dispatch_action(Box::new(crate::actions::Cancel), cx);
-                                    }),
-                            )
-                            .child(
-                                IconButton::new("diff-review-add", IconName::Return)
-                                    .icon_color(ui::Color::Muted)
-                                    .icon_size(action_icon_size)
-                                    .tooltip(Tooltip::text("Add comment"))
-                                    .on_click(|_, window, cx| {
-                                        window.dispatch_action(
-                                            Box::new(crate::actions::SubmitDiffReviewComment),
-                                            cx,
-                                        );
-                                    }),
-                            ),
-                    ),
-            )
-            // Expandable comments section (only shown when there are comments)
-            .when(comment_count > 0, |el| {
-                el.child(Self::render_comments_section(
-                    comments,
-                    comments_expanded,
-                    inline_editors,
-                    user_avatar_uri,
-                    avatar_size,
-                    action_icon_size,
-                    colors,
-                ))
-            })
-            .into_any_element()
-    }
-
-    fn render_comments_section(
-        comments: Vec<StoredReviewComment>,
-        expanded: bool,
-        inline_editors: HashMap<usize, Entity<Editor>>,
-        user_avatar_uri: Option<SharedUri>,
-        avatar_size: Pixels,
-        action_icon_size: IconSize,
-        colors: &theme::ThemeColors,
-    ) -> impl IntoElement {
-        let comment_count = comments.len();
-
-        v_flex()
-            .w_full()
-            .gap_1()
-            // Header with expand/collapse toggle
-            .child(
-                h_flex()
-                    .id("review-comments-header")
-                    .w_full()
-                    .items_center()
-                    .gap_1()
-                    .px_2()
-                    .py_1()
-                    .cursor_pointer()
-                    .rounded_md()
-                    .hover(|style| style.bg(colors.ghost_element_hover))
-                    .on_click(|_, window: &mut Window, cx| {
-                        window.dispatch_action(
-                            Box::new(crate::actions::ToggleReviewCommentsExpanded),
-                            cx,
-                        );
-                    })
-                    .child(
-                        Icon::new(if expanded {
-                            IconName::ChevronDown
-                        } else {
-                            IconName::ChevronRight
-                        })
-                        .size(IconSize::Small)
-                        .color(ui::Color::Muted),
-                    )
-                    .child(
-                        Label::new(format!(
-                            "{} Comment{}",
-                            comment_count,
-                            if comment_count == 1 { "" } else { "s" }
-                        ))
-                        .size(LabelSize::Small)
-                        .color(Color::Muted),
-                    ),
-            )
-            // Comments list (when expanded)
-            .when(expanded, |el| {
-                el.children(comments.into_iter().map(|comment| {
-                    let inline_editor = inline_editors.get(&comment.id).cloned();
-                    Self::render_comment_row(
-                        comment,
-                        inline_editor,
-                        user_avatar_uri.clone(),
-                        avatar_size,
-                        action_icon_size,
-                        colors,
-                    )
-                }))
-            })
-    }
-
-    fn render_comment_row(
-        comment: StoredReviewComment,
-        inline_editor: Option<Entity<Editor>>,
-        user_avatar_uri: Option<SharedUri>,
-        avatar_size: Pixels,
-        action_icon_size: IconSize,
-        colors: &theme::ThemeColors,
-    ) -> impl IntoElement {
-        let comment_id = comment.id;
-        let is_editing = inline_editor.is_some();
-
-        h_flex()
-            .w_full()
-            .items_center()
-            .gap_2()
-            .px_2()
-            .py_1p5()
-            .rounded_md()
-            .bg(colors.surface_background)
-            .child(
-                div()
-                    .size(avatar_size)
-                    .flex_shrink_0()
-                    .rounded_full()
-                    .overflow_hidden()
-                    .child(if let Some(ref avatar_uri) = user_avatar_uri {
-                        Avatar::new(avatar_uri.clone())
-                            .size(avatar_size)
-                            .into_any_element()
-                    } else {
-                        Icon::new(IconName::Person)
-                            .size(IconSize::Small)
-                            .color(ui::Color::Muted)
-                            .into_any_element()
-                    }),
-            )
-            .child(if let Some(editor) = inline_editor {
-                // Inline edit mode: show an editable text field
-                div()
-                    .flex_1()
-                    .border_1()
-                    .border_color(colors.border)
-                    .rounded_md()
-                    .bg(colors.editor_background)
-                    .px_2()
-                    .py_1()
-                    .child(editor)
-                    .into_any_element()
-            } else {
-                // Display mode: show the comment text
-                div()
-                    .flex_1()
-                    .text_sm()
-                    .text_color(colors.text)
-                    .child(comment.comment)
-                    .into_any_element()
-            })
-            .child(if is_editing {
-                // Editing mode: show close and confirm buttons
-                h_flex()
-                    .gap_1()
-                    .child(
-                        IconButton::new(
-                            format!("diff-review-cancel-edit-{comment_id}"),
-                            IconName::Close,
-                        )
-                        .icon_color(ui::Color::Muted)
-                        .icon_size(action_icon_size)
-                        .tooltip(Tooltip::text("Cancel"))
-                        .on_click(move |_, window, cx| {
-                            window.dispatch_action(
-                                Box::new(crate::actions::CancelEditReviewComment {
-                                    id: comment_id,
-                                }),
-                                cx,
-                            );
-                        }),
-                    )
-                    .child(
-                        IconButton::new(
-                            format!("diff-review-confirm-edit-{comment_id}"),
-                            IconName::Return,
-                        )
-                        .icon_color(ui::Color::Muted)
-                        .icon_size(action_icon_size)
-                        .tooltip(Tooltip::text("Confirm"))
-                        .on_click(move |_, window, cx| {
-                            window.dispatch_action(
-                                Box::new(crate::actions::ConfirmEditReviewComment {
-                                    id: comment_id,
-                                }),
-                                cx,
-                            );
-                        }),
-                    )
-                    .into_any_element()
-            } else {
-                // Display mode: no action buttons for now (edit/delete not yet implemented)
-                gpui::Empty.into_any_element()
-            })
-    }
-
-    pub fn set_masked(&mut self, masked: bool, cx: &mut Context<Self>) {
-        if self.display_map.read(cx).masked != masked {
-            self.display_map.update(cx, |map, _| map.masked = masked);
-        }
-        cx.notify()
-    }
-
-    fn get_permalink_to_line(&self, cx: &mut Context<Self>) -> Task<Result<url::Url>> {
-        let buffer_and_selection = maybe!({
-            let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
-            let selection_range = selection.range();
-
-            let multi_buffer = self.buffer().read(cx);
-            let multi_buffer_snapshot = multi_buffer.snapshot(cx);
-            let buffer_ranges = multi_buffer_snapshot
-                .range_to_buffer_ranges(selection_range.start..selection_range.end);
-
-            let (buffer_snapshot, range, _) = if selection.reversed {
-                buffer_ranges.first()
-            } else {
-                buffer_ranges.last()
-            }?;
-
-            let buffer_range = range.to_point(buffer_snapshot);
-            let buffer = multi_buffer.buffer(buffer_snapshot.remote_id()).unwrap();
-
-            let Some(buffer_diff) = multi_buffer.diff_for(buffer_snapshot.remote_id()) else {
-                return Some((buffer, buffer_range.start.row..buffer_range.end.row));
-            };
-
-            let buffer_diff_snapshot = buffer_diff.read(cx).snapshot(cx);
-            let start = buffer_diff_snapshot
-                .buffer_point_to_base_text_point(buffer_range.start, &buffer_snapshot);
-            let end = buffer_diff_snapshot
-                .buffer_point_to_base_text_point(buffer_range.end, &buffer_snapshot);
-
-            Some((buffer, start.row..end.row))
-        });
-
-        let Some((buffer, selection)) = buffer_and_selection else {
-            return Task::ready(Err(anyhow!("failed to determine buffer and selection")));
-        };
-
-        let Some(project) = self.project() else {
-            return Task::ready(Err(anyhow!("editor does not have project")));
-        };
-
-        project.update(cx, |project, cx| {
-            project.get_permalink_to_line(&buffer, selection, cx)
-        })
-    }
-
-    pub fn copy_permalink_to_line(
-        &mut self,
-        _: &CopyPermalinkToLine,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let permalink_task = self.get_permalink_to_line(cx);
-        let workspace = self.workspace();
-
-        cx.spawn_in(window, async move |_, cx| match permalink_task.await {
-            Ok(permalink) => {
-                cx.update(|_, cx| {
-                    cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
-                })
-                .ok();
-            }
-            Err(err) => {
-                let message = format!("Failed to copy permalink: {err}");
-
-                anyhow::Result::<()>::Err(err).log_err();
-
-                if let Some(workspace) = workspace {
-                    workspace
-                        .update_in(cx, |workspace, _, cx| {
-                            struct CopyPermalinkToLine;
-
-                            workspace.show_toast(
-                                Toast::new(
-                                    NotificationId::unique::<CopyPermalinkToLine>(),
-                                    message,
-                                ),
-                                cx,
-                            )
-                        })
-                        .ok();
-                }
-            }
-        })
-        .detach();
-    }
-
-    pub fn copy_file_location(
-        &mut self,
-        _: &CopyFileLocation,
-        _: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
-
-        let start_line = selection.start.row + 1;
-        let end_line = selection.end.row + 1;
-
-        let end_line = if selection.end.column == 0 && end_line > start_line {
-            end_line - 1
-        } else {
-            end_line
-        };
-
-        if let Some(file_location) = self.active_buffer(cx).and_then(|buffer| {
-            let project = self.project()?.read(cx);
-            let file = buffer.read(cx).file()?;
-            let path = file.path().display(project.path_style(cx));
-
-            let location = if start_line == end_line {
-                format!("{path}:{start_line}")
-            } else {
-                format!("{path}:{start_line}-{end_line}")
-            };
-            Some(location)
-        }) {
-            cx.write_to_clipboard(ClipboardItem::new_string(file_location));
-        }
-    }
-
-    pub fn open_permalink_to_line(
-        &mut self,
-        _: &OpenPermalinkToLine,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let permalink_task = self.get_permalink_to_line(cx);
-        let workspace = self.workspace();
-
-        cx.spawn_in(window, async move |_, cx| match permalink_task.await {
-            Ok(permalink) => {
-                cx.update(|_, cx| {
-                    cx.open_url(permalink.as_ref());
-                })
-                .ok();
-            }
-            Err(err) => {
-                let message = format!("Failed to open permalink: {err}");
-
-                anyhow::Result::<()>::Err(err).log_err();
-
-                if let Some(workspace) = workspace {
-                    workspace.update(cx, |workspace, cx| {
-                        struct OpenPermalinkToLine;
-
-                        workspace.show_toast(
-                            Toast::new(NotificationId::unique::<OpenPermalinkToLine>(), message),
-                            cx,
-                        )
-                    });
-                }
-            }
-        })
-        .detach();
-    }
-
-    pub fn insert_uuid_v4(
-        &mut self,
-        _: &InsertUuidV4,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.insert_uuid(UuidVersion::V4, window, cx);
-    }
-
-    pub fn insert_uuid_v7(
-        &mut self,
-        _: &InsertUuidV7,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.insert_uuid(UuidVersion::V7, window, cx);
-    }
-
-    fn insert_uuid(&mut self, version: UuidVersion, window: &mut Window, cx: &mut Context<Self>) {
-        if self.read_only(cx) {
+    fn insert_uuid(&mut self, version: UuidVersion, window: &mut Window, cx: &mut Context<Self>) {
+        if self.read_only(cx) {
             return;
         }
         self.transact(window, cx, |this, window, cx| {

crates/editor/src/git.rs 🔗

@@ -1,6 +1,139 @@
-pub mod blame;
+pub(super) mod blame;
 
 use super::*;
+use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
+use buffer_diff::DiffHunkStatus;
+
+pub type RenderDiffHunkControlsFn = Arc<
+    dyn Fn(
+        u32,
+        &DiffHunkStatus,
+        Range<Anchor>,
+        bool,
+        Pixels,
+        &Entity<Editor>,
+        &mut Window,
+        &mut App,
+    ) -> AnyElement,
+>;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(super) enum DisplayDiffHunk {
+    Folded {
+        display_row: DisplayRow,
+    },
+    Unfolded {
+        is_created_file: bool,
+        diff_base_byte_range: Range<usize>,
+        display_row_range: Range<DisplayRow>,
+        multi_buffer_range: Range<Anchor>,
+        status: DiffHunkStatus,
+        word_diffs: Vec<Range<MultiBufferOffset>>,
+    },
+}
+
+#[derive(Clone)]
+pub(super) struct InlineBlamePopoverState {
+    pub(super) scroll_handle: ScrollHandle,
+    pub(super) commit_message: Option<ParsedCommitMessage>,
+    pub(super) markdown: Entity<Markdown>,
+}
+
+pub(super) struct InlineBlamePopover {
+    pub(super) position: gpui::Point<Pixels>,
+    pub(super) hide_task: Option<Task<()>>,
+    pub(super) popover_bounds: Option<Bounds<Pixels>>,
+    pub(super) popover_state: InlineBlamePopoverState,
+    pub(super) keyboard_grace: bool,
+}
+
+/// Represents a diff review button indicator that shows up when hovering over lines in the gutter
+/// in diff view mode.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(super) struct PhantomDiffReviewIndicator {
+    /// The starting anchor of the selection (or the only row if not dragging).
+    pub(super) start: Anchor,
+    /// The ending anchor of the selection. Equal to start_anchor for single-line selection.
+    pub(super) end: Anchor,
+    /// There's a small debounce between hovering over the line and showing the indicator.
+    /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel.
+    pub(super) is_active: bool,
+}
+
+#[derive(Clone, Debug)]
+pub(super) struct DiffReviewDragState {
+    start_anchor: Anchor,
+    current_anchor: Anchor,
+}
+
+/// Identifies a specific hunk in the diff buffer.
+/// Used as a key to group comments by their location.
+#[derive(Clone, Debug)]
+pub(super) struct DiffHunkKey {
+    /// The file path (relative to worktree) this hunk belongs to.
+    pub(super) file_path: Arc<util::rel_path::RelPath>,
+    /// An anchor at the start of the hunk. This tracks position as the buffer changes.
+    pub(super) hunk_start_anchor: Anchor,
+}
+
+/// A review comment stored locally before being sent to the Agent panel.
+#[derive(Clone)]
+pub(super) struct StoredReviewComment {
+    /// Unique identifier for this comment (for edit/delete operations).
+    pub(super) id: usize,
+    /// The comment text entered by the user.
+    pub(super) comment: String,
+    /// Anchors for the code range being reviewed.
+    pub(super) range: Range<Anchor>,
+    /// Whether this comment is currently being edited inline.
+    pub(super) is_editing: bool,
+}
+
+/// Represents an active diff review overlay that appears when clicking the "Add Review" button.
+pub(super) struct DiffReviewOverlay {
+    pub(super) anchor_range: Range<Anchor>,
+    /// The block ID for the overlay.
+    pub(super) block_id: CustomBlockId,
+    /// The editor entity for the review input.
+    pub(super) prompt_editor: Entity<Editor>,
+    /// The hunk key this overlay belongs to.
+    pub(super) hunk_key: DiffHunkKey,
+    /// Whether the comments section is expanded.
+    pub(super) comments_expanded: bool,
+    /// Editors for comments currently being edited inline.
+    /// Key: comment ID, Value: Editor entity for inline editing.
+    pub(super) inline_edit_editors: HashMap<usize, Entity<Editor>>,
+    /// Subscriptions for inline edit editors' action handlers.
+    /// Key: comment ID, Value: Subscription keeping the Newline action handler alive.
+    pub(super) inline_edit_subscriptions: HashMap<usize, Subscription>,
+    /// The current user's avatar URI for display in comment rows.
+    pub(super) user_avatar_uri: Option<SharedUri>,
+    /// Subscription to keep the action handler alive.
+    _subscription: Subscription,
+}
+
+impl DiffReviewDragState {
+    pub(super) fn row_range(
+        &self,
+        snapshot: &DisplaySnapshot,
+    ) -> std::ops::RangeInclusive<DisplayRow> {
+        let start = self.start_anchor.to_display_point(snapshot).row();
+        let current = self.current_anchor.to_display_point(snapshot).row();
+
+        (start..=current).sorted()
+    }
+}
+
+impl StoredReviewComment {
+    fn new(id: usize, comment: String, anchor_range: Range<Anchor>) -> Self {
+        Self {
+            id,
+            comment,
+            range: anchor_range,
+            is_editing: false,
+        }
+    }
+}
 
 impl Editor {
     pub fn diff_hunks_in_ranges<'a>(
@@ -33,139 +166,1180 @@ impl Editor {
         })
     }
 
-    pub fn set_render_diff_hunk_controls(
+    pub fn set_render_diff_hunk_controls(
+        &mut self,
+        render_diff_hunk_controls: RenderDiffHunkControlsFn,
+        cx: &mut Context<Self>,
+    ) {
+        self.render_diff_hunk_controls = render_diff_hunk_controls;
+        cx.notify();
+    }
+
+    pub fn git_blame_inline_enabled(&self) -> bool {
+        self.git_blame_inline_enabled
+    }
+
+    pub fn blame(&self) -> Option<&Entity<GitBlame>> {
+        self.blame.as_ref()
+    }
+
+    pub fn show_git_blame_gutter(&self) -> bool {
+        self.show_git_blame_gutter
+    }
+
+    pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
+        let ranges: Vec<_> = self
+            .selections
+            .disjoint_anchors()
+            .iter()
+            .map(|s| s.range())
+            .collect();
+        self.buffer
+            .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx))
+    }
+
+    pub fn toggle_git_blame(
+        &mut self,
+        _: &::git::Blame,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.show_git_blame_gutter = !self.show_git_blame_gutter;
+
+        if self.show_git_blame_gutter && !self.has_blame_entries(cx) {
+            self.start_git_blame(true, window, cx);
+        }
+
+        cx.notify();
+    }
+
+    pub fn toggle_git_blame_inline(
+        &mut self,
+        _: &ToggleGitBlameInline,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.toggle_git_blame_inline_internal(true, window, cx);
+        cx.notify();
+    }
+
+    pub fn start_temporary_diff_override(&mut self) {
+        self.load_diff_task.take();
+        self.temporary_diff_override = true;
+    }
+
+    pub fn end_temporary_diff_override(&mut self, cx: &mut Context<Self>) {
+        self.temporary_diff_override = false;
+        self.set_render_diff_hunk_controls(Arc::new(render_diff_hunk_controls), cx);
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.set_all_diff_hunks_collapsed(cx);
+        });
+
+        if let Some(project) = self.project.clone() {
+            self.load_diff_task = Some(
+                update_uncommitted_diff_for_buffer(
+                    cx.entity(),
+                    &project,
+                    self.buffer.read(cx).all_buffers(),
+                    self.buffer.clone(),
+                    cx,
+                )
+                .shared(),
+            );
+        }
+    }
+
+    /// Hides the inline blame popover element, in case it's already visible, or
+    /// interrupts the task meant to show it, in case the task is running.
+    ///
+    /// When `ignore_timeout` is set to `true`, the popover is hidden
+    /// immediately, otherwise it'll be hidden after a short delay.
+    ///
+    /// Returns `true` if the popover was visible and was hidden, `false`
+    /// otherwise.
+    pub fn hide_blame_popover(&mut self, ignore_timeout: bool, cx: &mut Context<Self>) -> bool {
+        self.inline_blame_popover_show_task.take();
+
+        if let Some(state) = &mut self.inline_blame_popover {
+            if ignore_timeout {
+                self.inline_blame_popover.take();
+                cx.notify();
+            } else {
+                state.hide_task = Some(cx.spawn(async move |editor, cx| {
+                    cx.background_executor()
+                        .timer(std::time::Duration::from_millis(100))
+                        .await;
+
+                    editor
+                        .update(cx, |editor, cx| {
+                            editor.inline_blame_popover.take();
+                            cx.notify();
+                        })
+                        .ok();
+                }));
+            }
+
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context<Self>) {
+        if self.read_only(cx) {
+            return;
+        }
+        let selections = self
+            .selections
+            .all(&self.display_snapshot(cx))
+            .into_iter()
+            .map(|s| s.range())
+            .collect();
+        self.restore_hunks_in_ranges(selections, window, cx);
+    }
+
+    pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
+        if let Some(status) = self
+            .addons
+            .iter()
+            .find_map(|(_, addon)| addon.override_status_for_buffer_id(buffer_id, cx))
+        {
+            return Some(status);
+        }
+        self.project
+            .as_ref()?
+            .read(cx)
+            .status_for_buffer_id(buffer_id, cx)
+    }
+
+    pub fn go_to_hunk_before_or_after_position(
+        &mut self,
+        snapshot: &EditorSnapshot,
+        position: Point,
+        direction: Direction,
+        wrap_around: bool,
+        window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) {
+        let row = if direction == Direction::Next {
+            self.hunk_after_position(snapshot, position, wrap_around)
+                .map(|hunk| hunk.row_range.start)
+        } else {
+            self.hunk_before_position(snapshot, position, wrap_around)
+        };
+
+        if let Some(row) = row {
+            let destination = Point::new(row.0, 0);
+            let autoscroll = Autoscroll::center();
+
+            self.unfold_ranges(&[destination..destination], false, false, cx);
+            self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
+                s.select_ranges([destination..destination]);
+            });
+        }
+    }
+
+    pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) {
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.set_all_diff_hunks_expanded(cx);
+        });
+    }
+
+    pub fn expand_all_diff_hunks(
+        &mut self,
+        _: &ExpandAllDiffHunks,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx)
+        });
+    }
+
+    pub fn show_diff_review_overlay(
+        &mut self,
+        display_range: Range<DisplayRow>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Range { start, end } = display_range.sorted();
+
+        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+        let editor_snapshot = self.snapshot(window, cx);
+
+        // Convert display rows to multibuffer points
+        let start_point = editor_snapshot
+            .display_snapshot
+            .display_point_to_point(start.as_display_point(), Bias::Left);
+        let end_point = editor_snapshot
+            .display_snapshot
+            .display_point_to_point(end.as_display_point(), Bias::Left);
+        let end_multi_buffer_row = MultiBufferRow(end_point.row);
+
+        // Create anchor range for the selected lines (start of first line to end of last line)
+        let line_end = Point::new(
+            end_point.row,
+            buffer_snapshot.line_len(end_multi_buffer_row),
+        );
+        let anchor_range =
+            buffer_snapshot.anchor_after(start_point)..buffer_snapshot.anchor_before(line_end);
+
+        // Compute the hunk key for this display row
+        let file_path = buffer_snapshot
+            .file_at(start_point)
+            .map(|file: &Arc<dyn language::File>| file.path().clone())
+            .unwrap_or_else(|| Arc::from(util::rel_path::RelPath::empty()));
+        let hunk_start_anchor = buffer_snapshot.anchor_before(start_point);
+        let new_hunk_key = DiffHunkKey {
+            file_path,
+            hunk_start_anchor,
+        };
+
+        // Check if we already have an overlay for this hunk
+        if let Some(existing_overlay) = self.diff_review_overlays.iter().find(|overlay| {
+            Self::hunk_keys_match(&overlay.hunk_key, &new_hunk_key, &buffer_snapshot)
+        }) {
+            // Just focus the existing overlay's prompt editor
+            let focus_handle = existing_overlay.prompt_editor.focus_handle(cx);
+            window.focus(&focus_handle, cx);
+            return;
+        }
+
+        // Dismiss overlays that have no comments for their hunks
+        self.dismiss_overlays_without_comments(cx);
+
+        // Get the current user's avatar URI from the project's user_store
+        let user_avatar_uri = self.project.as_ref().and_then(|project| {
+            let user_store = project.read(cx).user_store();
+            user_store
+                .read(cx)
+                .current_user()
+                .map(|user| user.avatar_uri.clone())
+        });
+
+        // Create anchor at the end of the last row so the block appears immediately below it
+        // Use multibuffer coordinates for anchor creation
+        let line_len = buffer_snapshot.line_len(end_multi_buffer_row);
+        let anchor = buffer_snapshot.anchor_after(Point::new(end_multi_buffer_row.0, line_len));
+
+        // Use the hunk key we already computed
+        let hunk_key = new_hunk_key;
+
+        // Create the prompt editor for the review input
+        let prompt_editor = cx.new(|cx| {
+            let mut editor = Editor::single_line(window, cx);
+            editor.set_placeholder_text("Add a review comment...", window, cx);
+            editor
+        });
+
+        // Register the Newline action on the prompt editor to submit the review
+        let parent_editor = cx.entity().downgrade();
+        let subscription = prompt_editor.update(cx, |prompt_editor, _cx| {
+            prompt_editor.register_action({
+                let parent_editor = parent_editor.clone();
+                move |_: &crate::actions::Newline, window, cx| {
+                    if let Some(editor) = parent_editor.upgrade() {
+                        editor.update(cx, |editor, cx| {
+                            editor.submit_diff_review_comment(window, cx);
+                        });
+                    }
+                }
+            })
+        });
+
+        // Calculate initial height based on existing comments for this hunk
+        let initial_height = self.calculate_overlay_height(&hunk_key, true, &buffer_snapshot);
+
+        // Create the overlay block
+        let prompt_editor_for_render = prompt_editor.clone();
+        let hunk_key_for_render = hunk_key.clone();
+        let editor_handle = cx.entity().downgrade();
+        let block = BlockProperties {
+            style: BlockStyle::Sticky,
+            placement: BlockPlacement::Below(anchor),
+            height: Some(initial_height),
+            render: Arc::new(move |cx| {
+                Self::render_diff_review_overlay(
+                    &prompt_editor_for_render,
+                    &hunk_key_for_render,
+                    &editor_handle,
+                    cx,
+                )
+            }),
+            priority: 0,
+        };
+
+        let block_ids = self.insert_blocks([block], None, cx);
+        let Some(block_id) = block_ids.into_iter().next() else {
+            log::error!("Failed to insert diff review overlay block");
+            return;
+        };
+
+        self.diff_review_overlays.push(DiffReviewOverlay {
+            anchor_range,
+            block_id,
+            prompt_editor: prompt_editor.clone(),
+            hunk_key,
+            comments_expanded: true,
+            inline_edit_editors: HashMap::default(),
+            inline_edit_subscriptions: HashMap::default(),
+            user_avatar_uri,
+            _subscription: subscription,
+        });
+
+        // Focus the prompt editor
+        let focus_handle = prompt_editor.focus_handle(cx);
+        window.focus(&focus_handle, cx);
+
+        cx.notify();
+    }
+
+    /// Stores the diff review comment locally.
+    /// Comments are stored per-hunk and can later be batch-submitted to the Agent panel.
+    pub fn submit_diff_review_comment(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        // Find the overlay that currently has focus
+        let overlay_index = self
+            .diff_review_overlays
+            .iter()
+            .position(|overlay| overlay.prompt_editor.focus_handle(cx).is_focused(window));
+        let Some(overlay_index) = overlay_index else {
+            return;
+        };
+        let overlay = &self.diff_review_overlays[overlay_index];
+
+        let comment_text = overlay.prompt_editor.read(cx).text(cx).trim().to_string();
+        if comment_text.is_empty() {
+            return;
+        }
+
+        let anchor_range = overlay.anchor_range.clone();
+        let hunk_key = overlay.hunk_key.clone();
+
+        self.add_review_comment(hunk_key.clone(), comment_text, anchor_range, cx);
+
+        // Clear the prompt editor but keep the overlay open
+        if let Some(overlay) = self.diff_review_overlays.get(overlay_index) {
+            overlay.prompt_editor.update(cx, |editor, cx| {
+                editor.clear(window, cx);
+            });
+        }
+
+        // Refresh the overlay to update the block height for the new comment
+        self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
+
+        cx.notify();
+    }
+
+    /// Returns the prompt editor for the diff review overlay, if one is active.
+    /// This is primarily used for testing.
+    pub fn diff_review_prompt_editor(&self) -> Option<&Entity<Editor>> {
+        self.diff_review_overlays
+            .first()
+            .map(|overlay| &overlay.prompt_editor)
+    }
+
+    /// Sets whether the comments section is expanded in the diff review overlay.
+    /// This is primarily used for testing.
+    pub fn set_diff_review_comments_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) {
+        for overlay in &mut self.diff_review_overlays {
+            overlay.comments_expanded = expanded;
+        }
+        cx.notify();
+    }
+
+    /// Returns the total count of stored review comments across all hunks.
+    pub(super) fn total_review_comment_count(&self) -> usize {
+        self.stored_review_comments
+            .iter()
+            .map(|(_, v)| v.len())
+            .sum()
+    }
+
+    /// Adds a new review comment to a specific hunk.
+    pub(super) fn add_review_comment(
+        &mut self,
+        hunk_key: DiffHunkKey,
+        comment: String,
+        anchor_range: Range<Anchor>,
+        cx: &mut Context<Self>,
+    ) -> usize {
+        let id = self.next_review_comment_id;
+        self.next_review_comment_id += 1;
+
+        let stored_comment = StoredReviewComment::new(id, comment, anchor_range);
+
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let key_point = hunk_key.hunk_start_anchor.to_point(&snapshot);
+
+        // Find existing entry for this hunk or add a new one
+        if let Some((_, comments)) = self.stored_review_comments.iter_mut().find(|(k, _)| {
+            k.file_path == hunk_key.file_path
+                && k.hunk_start_anchor.to_point(&snapshot) == key_point
+        }) {
+            comments.push(stored_comment);
+        } else {
+            self.stored_review_comments
+                .push((hunk_key, vec![stored_comment]));
+        }
+
+        cx.emit(EditorEvent::ReviewCommentsChanged {
+            total_count: self.total_review_comment_count(),
+        });
+        cx.notify();
+        id
+    }
+
+    pub(super) fn blame_hover(
+        &mut self,
+        _: &BlameHover,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let snapshot = self.snapshot(window, cx);
+        let cursor = self
+            .selections
+            .newest::<Point>(&snapshot.display_snapshot)
+            .head();
+        let Some((buffer, point)) = snapshot.buffer_snapshot().point_to_buffer_point(cursor) else {
+            return;
+        };
+
+        if self.blame.is_none() {
+            self.start_git_blame(true, window, cx);
+        }
+        let Some(blame) = self.blame.as_ref() else {
+            return;
+        };
+
+        let row_info = RowInfo {
+            buffer_id: Some(buffer.remote_id()),
+            buffer_row: Some(point.row),
+            ..Default::default()
+        };
+        let Some((buffer, blame_entry)) = blame
+            .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
+            .flatten()
+        else {
+            return;
+        };
+
+        let anchor = self.selections.newest_anchor().head();
+        let position = self.to_pixel_point(anchor, &snapshot, window, cx);
+        if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) {
+            self.show_blame_popover(
+                buffer,
+                &blame_entry,
+                position + last_bounds.origin,
+                true,
+                cx,
+            );
+        };
+    }
+
+    pub(super) fn restore_file(
+        &mut self,
+        _: &::git::RestoreFile,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self.read_only(cx) {
+            return;
+        }
+        let mut buffer_ids = HashSet::default();
+        let snapshot = self.buffer().read(cx).snapshot(cx);
+        for selection in self
+            .selections
+            .all::<MultiBufferOffset>(&self.display_snapshot(cx))
+        {
+            buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range()))
+        }
+
+        let ranges = buffer_ids
+            .into_iter()
+            .flat_map(|buffer_id| snapshot.range_for_buffer(buffer_id))
+            .collect::<Vec<_>>();
+
+        self.restore_hunks_in_ranges(ranges, window, cx);
+    }
+
+    /// Restores the diff hunks in the editor's selections and moves the cursor
+    /// to the next diff hunk. Wraps around to the beginning of the buffer if
+    /// not all diff hunks are expanded.
+    pub(super) fn restore_and_next(
+        &mut self,
+        _: &::git::RestoreAndNext,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self.read_only(cx) {
+            return;
+        }
+        let selections = self
+            .selections
+            .all(&self.display_snapshot(cx))
+            .into_iter()
+            .map(|selection| selection.range())
+            .collect();
+
+        self.restore_hunks_in_ranges(selections, window, cx);
+
+        let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
+        let wrap_around = !all_diff_hunks_expanded;
+        let snapshot = self.snapshot(window, cx);
+        let position = self
+            .selections
+            .newest::<Point>(&snapshot.display_snapshot)
+            .head();
+
+        self.go_to_hunk_before_or_after_position(
+            &snapshot,
+            position,
+            Direction::Next,
+            wrap_around,
+            window,
+            cx,
+        );
+    }
+
+    pub(super) fn restore_diff_hunks(&self, hunks: Vec<MultiBufferDiffHunk>, cx: &mut App) {
+        let mut revert_changes = HashMap::default();
+        let chunk_by = hunks.into_iter().chunk_by(|hunk| hunk.buffer_id);
+        for (buffer_id, hunks) in &chunk_by {
+            let hunks = hunks.collect::<Vec<_>>();
+            for hunk in &hunks {
+                self.prepare_restore_change(&mut revert_changes, hunk, cx);
+            }
+            self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx);
+        }
+        if !revert_changes.is_empty() {
+            self.buffer().update(cx, |multi_buffer, cx| {
+                for (buffer_id, changes) in revert_changes {
+                    if let Some(buffer) = multi_buffer.buffer(buffer_id) {
+                        buffer.update(cx, |buffer, cx| {
+                            buffer.edit(
+                                changes
+                                    .into_iter()
+                                    .map(|(range, text)| (range, text.to_string())),
+                                None,
+                                cx,
+                            );
+                        });
+                    }
+                }
+            });
+        }
+    }
+
+    pub(super) fn go_to_next_hunk(
+        &mut self,
+        _: &GoToHunk,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let snapshot = self.snapshot(window, cx);
+        let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
+        self.go_to_hunk_before_or_after_position(
+            &snapshot,
+            selection.head(),
+            Direction::Next,
+            true,
+            window,
+            cx,
+        );
+    }
+
+    pub(super) fn collapse_all_diff_hunks(
+        &mut self,
+        _: &CollapseAllDiffHunks,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.buffer.update(cx, |buffer, cx| {
+            buffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], cx)
+        });
+    }
+
+    pub(super) fn toggle_selected_diff_hunks(
+        &mut self,
+        _: &ToggleSelectedDiffHunks,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let ranges: Vec<_> = self
+            .selections
+            .disjoint_anchors()
+            .iter()
+            .map(|s| s.range())
+            .collect();
+        self.toggle_diff_hunks_in_ranges(ranges, cx);
+    }
+
+    pub(super) fn show_diff_review_button(&self) -> bool {
+        self.show_diff_review_button
+    }
+
+    pub(super) fn render_diff_review_button(
+        &self,
+        display_row: DisplayRow,
+        width: Pixels,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let text_color = cx.theme().colors().text;
+        let icon_color = cx.theme().colors().icon_accent;
+
+        h_flex()
+            .id("diff_review_button")
+            .cursor_pointer()
+            .w(width - px(1.))
+            .h(relative(0.9))
+            .justify_center()
+            .rounded_sm()
+            .border_1()
+            .border_color(text_color.opacity(0.1))
+            .bg(text_color.opacity(0.15))
+            .hover(|s| {
+                s.bg(icon_color.opacity(0.4))
+                    .border_color(icon_color.opacity(0.5))
+            })
+            .child(Icon::new(IconName::Plus).size(IconSize::Small))
+            .tooltip(Tooltip::text("Add Review (drag to select multiple lines)"))
+            .on_mouse_down(
+                gpui::MouseButton::Left,
+                cx.listener(move |editor, _event: &gpui::MouseDownEvent, window, cx| {
+                    editor.start_diff_review_drag(display_row, window, cx);
+                }),
+            )
+    }
+
+    pub(super) fn start_diff_review_drag(
+        &mut self,
+        display_row: DisplayRow,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let snapshot = self.snapshot(window, cx);
+        let point = snapshot
+            .display_snapshot
+            .display_point_to_point(DisplayPoint::new(display_row, 0), Bias::Left);
+        let anchor = snapshot.buffer_snapshot().anchor_before(point);
+        self.diff_review_drag_state = Some(DiffReviewDragState {
+            start_anchor: anchor,
+            current_anchor: anchor,
+        });
+        cx.notify();
+    }
+
+    pub(super) fn update_diff_review_drag(
+        &mut self,
+        display_row: DisplayRow,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self.diff_review_drag_state.is_none() {
+            return;
+        }
+        let snapshot = self.snapshot(window, cx);
+        let point = snapshot
+            .display_snapshot
+            .display_point_to_point(display_row.as_display_point(), Bias::Left);
+        let anchor = snapshot.buffer_snapshot().anchor_before(point);
+        if let Some(drag_state) = &mut self.diff_review_drag_state {
+            drag_state.current_anchor = anchor;
+            cx.notify();
+        }
+    }
+
+    pub(super) fn end_diff_review_drag(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        if let Some(drag_state) = self.diff_review_drag_state.take() {
+            let snapshot = self.snapshot(window, cx);
+            let range = drag_state.row_range(&snapshot.display_snapshot);
+            self.show_diff_review_overlay(*range.start()..*range.end(), window, cx);
+        }
+        cx.notify();
+    }
+
+    pub(super) fn cancel_diff_review_drag(&mut self, cx: &mut Context<Self>) {
+        self.diff_review_drag_state = None;
+        cx.notify();
+    }
+
+    /// Dismisses all diff review overlays.
+    pub(super) fn dismiss_all_diff_review_overlays(&mut self, cx: &mut Context<Self>) {
+        if self.diff_review_overlays.is_empty() {
+            return;
+        }
+        let block_ids: HashSet<_> = self
+            .diff_review_overlays
+            .drain(..)
+            .map(|overlay| overlay.block_id)
+            .collect();
+        self.remove_blocks(block_ids, None, cx);
+        cx.notify();
+    }
+
+    /// Action handler for SubmitDiffReviewComment.
+    pub(super) fn submit_diff_review_comment_action(
+        &mut self,
+        _: &SubmitDiffReviewComment,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.submit_diff_review_comment(window, cx);
+    }
+
+    /// Returns comments for a specific hunk, ordered by creation time.
+    pub(super) fn comments_for_hunk<'a>(
+        &'a self,
+        key: &DiffHunkKey,
+        snapshot: &MultiBufferSnapshot,
+    ) -> &'a [StoredReviewComment] {
+        let key_point = key.hunk_start_anchor.to_point(snapshot);
+        self.stored_review_comments
+            .iter()
+            .find(|(k, _)| {
+                k.file_path == key.file_path && k.hunk_start_anchor.to_point(snapshot) == key_point
+            })
+            .map(|(_, comments)| comments.as_slice())
+            .unwrap_or(&[])
+    }
+
+    /// Returns the count of comments for a specific hunk.
+    pub(super) fn hunk_comment_count(
+        &self,
+        key: &DiffHunkKey,
+        snapshot: &MultiBufferSnapshot,
+    ) -> usize {
+        let key_point = key.hunk_start_anchor.to_point(snapshot);
+        self.stored_review_comments
+            .iter()
+            .find(|(k, _)| {
+                k.file_path == key.file_path && k.hunk_start_anchor.to_point(snapshot) == key_point
+            })
+            .map(|(_, v)| v.len())
+            .unwrap_or(0)
+    }
+
+    /// Removes a review comment by ID from any hunk.
+    pub(super) fn remove_review_comment(&mut self, id: usize, cx: &mut Context<Self>) -> bool {
+        for (_, comments) in self.stored_review_comments.iter_mut() {
+            if let Some(index) = comments.iter().position(|c| c.id == id) {
+                comments.remove(index);
+                cx.emit(EditorEvent::ReviewCommentsChanged {
+                    total_count: self.total_review_comment_count(),
+                });
+                cx.notify();
+                return true;
+            }
+        }
+        false
+    }
+
+    /// Updates a review comment's text by ID.
+    pub(super) fn update_review_comment(
+        &mut self,
+        id: usize,
+        new_comment: String,
+        cx: &mut Context<Self>,
+    ) -> bool {
+        for (_, comments) in self.stored_review_comments.iter_mut() {
+            if let Some(comment) = comments.iter_mut().find(|c| c.id == id) {
+                comment.comment = new_comment;
+                comment.is_editing = false;
+                cx.emit(EditorEvent::ReviewCommentsChanged {
+                    total_count: self.total_review_comment_count(),
+                });
+                cx.notify();
+                return true;
+            }
+        }
+        false
+    }
+
+    /// Sets a comment's editing state.
+    pub(super) fn set_comment_editing(
+        &mut self,
+        id: usize,
+        is_editing: bool,
+        cx: &mut Context<Self>,
+    ) {
+        for (_, comments) in self.stored_review_comments.iter_mut() {
+            if let Some(comment) = comments.iter_mut().find(|c| c.id == id) {
+                comment.is_editing = is_editing;
+                cx.notify();
+                return;
+            }
+        }
+    }
+
+    /// Removes review comments whose anchors are no longer valid or whose
+    /// associated diff hunks no longer exist.
+    ///
+    /// This should be called when the buffer changes to prevent orphaned comments
+    /// from accumulating.
+    pub(super) fn cleanup_orphaned_review_comments(&mut self, cx: &mut Context<Self>) {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let original_count = self.total_review_comment_count();
+
+        // Remove comments with invalid hunk anchors
+        self.stored_review_comments
+            .retain(|(hunk_key, _)| hunk_key.hunk_start_anchor.is_valid(&snapshot));
+
+        // Also clean up individual comments with invalid anchor ranges
+        for (_, comments) in &mut self.stored_review_comments {
+            comments.retain(|comment| {
+                comment.range.start.is_valid(&snapshot) && comment.range.end.is_valid(&snapshot)
+            });
+        }
+
+        // Remove empty hunk entries
+        self.stored_review_comments
+            .retain(|(_, comments)| !comments.is_empty());
+
+        let new_count = self.total_review_comment_count();
+        if new_count != original_count {
+            cx.emit(EditorEvent::ReviewCommentsChanged {
+                total_count: new_count,
+            });
+            cx.notify();
+        }
+    }
+
+    /// Toggles the expanded state of the comments section in the overlay.
+    pub(super) fn toggle_review_comments_expanded(
         &mut self,
-        render_diff_hunk_controls: RenderDiffHunkControlsFn,
+        _: &ToggleReviewCommentsExpanded,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.render_diff_hunk_controls = render_diff_hunk_controls;
-        cx.notify();
+        // Find the overlay that currently has focus, or use the first one
+        let overlay_info = self.diff_review_overlays.iter_mut().find_map(|overlay| {
+            if overlay.prompt_editor.focus_handle(cx).is_focused(window) {
+                overlay.comments_expanded = !overlay.comments_expanded;
+                Some(overlay.hunk_key.clone())
+            } else {
+                None
+            }
+        });
+
+        // If no focused overlay found, toggle the first one
+        let hunk_key = overlay_info.or_else(|| {
+            self.diff_review_overlays.first_mut().map(|overlay| {
+                overlay.comments_expanded = !overlay.comments_expanded;
+                overlay.hunk_key.clone()
+            })
+        });
+
+        if let Some(hunk_key) = hunk_key {
+            self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
+            cx.notify();
+        }
     }
 
-    pub fn working_directory(&self, cx: &App) -> Option<PathBuf> {
-        if let Some(buffer) = self.buffer().read(cx).as_singleton() {
-            if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local())
-                && let Some(dir) = file.abs_path(cx).parent()
+    /// Handles the EditReviewComment action - sets a comment into editing mode.
+    pub(super) fn edit_review_comment(
+        &mut self,
+        action: &EditReviewComment,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let comment_id = action.id;
+
+        // Set the comment to editing mode
+        self.set_comment_editing(comment_id, true, cx);
+
+        // Find the overlay that contains this comment and create an inline editor if needed
+        // First, find which hunk this comment belongs to
+        let hunk_key = self
+            .stored_review_comments
+            .iter()
+            .find_map(|(key, comments)| {
+                if comments.iter().any(|c| c.id == comment_id) {
+                    Some(key.clone())
+                } else {
+                    None
+                }
+            });
+
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        if let Some(hunk_key) = hunk_key {
+            if let Some(overlay) = self
+                .diff_review_overlays
+                .iter_mut()
+                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
             {
-                return Some(dir.to_owned());
+                if let std::collections::hash_map::Entry::Vacant(entry) =
+                    overlay.inline_edit_editors.entry(comment_id)
+                {
+                    // Find the comment text
+                    let comment_text = self
+                        .stored_review_comments
+                        .iter()
+                        .flat_map(|(_, comments)| comments)
+                        .find(|c| c.id == comment_id)
+                        .map(|c| c.comment.clone())
+                        .unwrap_or_default();
+
+                    // Create inline editor
+                    let parent_editor = cx.entity().downgrade();
+                    let inline_editor = cx.new(|cx| {
+                        let mut editor = Editor::single_line(window, cx);
+                        editor.set_text(&*comment_text, window, cx);
+                        // Select all text for easy replacement
+                        editor.select_all(&crate::actions::SelectAll, window, cx);
+                        editor
+                    });
+
+                    // Register the Newline action to confirm the edit
+                    let subscription = inline_editor.update(cx, |inline_editor, _cx| {
+                        inline_editor.register_action({
+                            let parent_editor = parent_editor.clone();
+                            move |_: &crate::actions::Newline, window, cx| {
+                                if let Some(editor) = parent_editor.upgrade() {
+                                    editor.update(cx, |editor, cx| {
+                                        editor.confirm_edit_review_comment(comment_id, window, cx);
+                                    });
+                                }
+                            }
+                        })
+                    });
+
+                    // Store the subscription to keep the action handler alive
+                    overlay
+                        .inline_edit_subscriptions
+                        .insert(comment_id, subscription);
+
+                    // Focus the inline editor
+                    let focus_handle = inline_editor.focus_handle(cx);
+                    window.focus(&focus_handle, cx);
+
+                    entry.insert(inline_editor);
+                }
             }
         }
 
-        None
+        cx.notify();
     }
 
-    pub fn target_file_abs_path(&self, cx: &mut Context<Self>) -> Option<PathBuf> {
-        self.active_buffer(cx).and_then(|buffer| {
-            let buffer = buffer.read(cx);
-            if let Some(project_path) = buffer.project_path(cx) {
-                let project = self.project()?.read(cx);
-                project.absolute_path(&project_path, cx)
-            } else {
-                buffer
-                    .file()
-                    .and_then(|file| file.as_local().map(|file| file.abs_path(cx)))
-            }
-        })
-    }
+    /// Confirms an inline edit of a review comment.
+    pub(super) fn confirm_edit_review_comment(
+        &mut self,
+        comment_id: usize,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        // Get the new text from the inline editor
+        // Find the overlay containing this comment's inline editor
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let hunk_key = self
+            .stored_review_comments
+            .iter()
+            .find_map(|(key, comments)| {
+                if comments.iter().any(|c| c.id == comment_id) {
+                    Some(key.clone())
+                } else {
+                    None
+                }
+            });
 
-    /// Returns the project path for the editor's buffer, if any buffer is
-    /// opened in the editor.
-    pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
-        if let Some(buffer) = self.buffer.read(cx).as_singleton() {
-            buffer.read(cx).project_path(cx)
-        } else {
-            None
+        let new_text = hunk_key
+            .as_ref()
+            .and_then(|hunk_key| {
+                self.diff_review_overlays
+                    .iter()
+                    .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot))
+            })
+            .as_ref()
+            .and_then(|overlay| overlay.inline_edit_editors.get(&comment_id))
+            .map(|editor| editor.read(cx).text(cx).trim().to_string());
+
+        if let Some(new_text) = new_text {
+            if !new_text.is_empty() {
+                self.update_review_comment(comment_id, new_text, cx);
+            }
         }
-    }
 
-    pub fn git_blame_inline_enabled(&self) -> bool {
-        self.git_blame_inline_enabled
-    }
+        // Remove the inline editor and its subscription
+        if let Some(hunk_key) = hunk_key {
+            if let Some(overlay) = self
+                .diff_review_overlays
+                .iter_mut()
+                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
+            {
+                overlay.inline_edit_editors.remove(&comment_id);
+                overlay.inline_edit_subscriptions.remove(&comment_id);
+            }
+        }
 
-    pub fn selection_menu_enabled(&self, cx: &App) -> bool {
-        self.show_selection_menu
-            .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu)
+        // Clear editing state
+        self.set_comment_editing(comment_id, false, cx);
     }
 
-    pub fn toggle_selection_menu(
+    /// Cancels an inline edit of a review comment.
+    pub(super) fn cancel_edit_review_comment(
         &mut self,
-        _: &ToggleSelectionMenu,
-        _: &mut Window,
+        comment_id: usize,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.show_selection_menu = self
-            .show_selection_menu
-            .map(|show_selections_menu| !show_selections_menu)
-            .or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu));
+        // Find which hunk this comment belongs to
+        let hunk_key = self
+            .stored_review_comments
+            .iter()
+            .find_map(|(key, comments)| {
+                if comments.iter().any(|c| c.id == comment_id) {
+                    Some(key.clone())
+                } else {
+                    None
+                }
+            });
 
-        cx.notify();
-    }
+        // Remove the inline editor and its subscription
+        if let Some(hunk_key) = hunk_key {
+            let snapshot = self.buffer.read(cx).snapshot(cx);
+            if let Some(overlay) = self
+                .diff_review_overlays
+                .iter_mut()
+                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
+            {
+                overlay.inline_edit_editors.remove(&comment_id);
+                overlay.inline_edit_subscriptions.remove(&comment_id);
+            }
+        }
 
-    pub fn blame(&self) -> Option<&Entity<GitBlame>> {
-        self.blame.as_ref()
+        // Clear editing state
+        self.set_comment_editing(comment_id, false, cx);
     }
 
-    pub fn show_git_blame_gutter(&self) -> bool {
-        self.show_git_blame_gutter
+    /// Action handler for ConfirmEditReviewComment.
+    pub(super) fn confirm_edit_review_comment_action(
+        &mut self,
+        action: &ConfirmEditReviewComment,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.confirm_edit_review_comment(action.id, window, cx);
     }
 
-    pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
-        let ranges: Vec<_> = self
-            .selections
-            .disjoint_anchors()
-            .iter()
-            .map(|s| s.range())
-            .collect();
-        self.buffer
-            .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx))
+    /// Action handler for CancelEditReviewComment.
+    pub(super) fn cancel_edit_review_comment_action(
+        &mut self,
+        action: &CancelEditReviewComment,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.cancel_edit_review_comment(action.id, window, cx);
     }
 
-    pub fn copy_file_name_without_extension(
+    /// Handles the DeleteReviewComment action - removes a comment.
+    pub(super) fn delete_review_comment(
         &mut self,
-        _: &CopyFileNameWithoutExtension,
-        _: &mut Window,
+        action: &DeleteReviewComment,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if let Some(file_stem) = self.active_buffer(cx).and_then(|buffer| {
-            let file = buffer.read(cx).file()?;
-            file.path().file_stem()
-        }) {
-            cx.write_to_clipboard(ClipboardItem::new_string(file_stem.to_string()));
-        }
-    }
+        // Get the hunk key before removing the comment
+        // Find the hunk key from the comment itself
+        let comment_id = action.id;
+        let hunk_key = self
+            .stored_review_comments
+            .iter()
+            .find_map(|(key, comments)| {
+                if comments.iter().any(|c| c.id == comment_id) {
+                    Some(key.clone())
+                } else {
+                    None
+                }
+            });
 
-    pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) {
-        if let Some(file_name) = self.active_buffer(cx).and_then(|buffer| {
-            let file = buffer.read(cx).file()?;
-            Some(file.file_name(cx))
-        }) {
-            cx.write_to_clipboard(ClipboardItem::new_string(file_name.to_string()));
+        // Also get it from the overlay for refresh purposes
+        let overlay_hunk_key = self
+            .diff_review_overlays
+            .first()
+            .map(|o| o.hunk_key.clone());
+
+        self.remove_review_comment(action.id, cx);
+
+        // Refresh the overlay height after removing a comment
+        if let Some(hunk_key) = hunk_key.or(overlay_hunk_key) {
+            self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
         }
     }
 
-    pub fn toggle_git_blame(
+    pub(super) fn copy_permalink_to_line(
         &mut self,
-        _: &::git::Blame,
+        _: &CopyPermalinkToLine,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.show_git_blame_gutter = !self.show_git_blame_gutter;
-
-        if self.show_git_blame_gutter && !self.has_blame_entries(cx) {
-            self.start_git_blame(true, window, cx);
-        }
+        let permalink_task = self.get_permalink_to_line(cx);
+        let workspace = self.workspace();
 
-        cx.notify();
+        cx.spawn_in(window, async move |_, cx| match permalink_task.await {
+            Ok(permalink) => {
+                cx.update(|_, cx| {
+                    cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
+                })
+                .ok();
+            }
+            Err(err) => {
+                let message = format!("Failed to copy permalink: {err}");
+
+                anyhow::Result::<()>::Err(err).log_err();
+
+                if let Some(workspace) = workspace {
+                    workspace
+                        .update_in(cx, |workspace, _, cx| {
+                            struct CopyPermalinkToLine;
+
+                            workspace.show_toast(
+                                Toast::new(
+                                    NotificationId::unique::<CopyPermalinkToLine>(),
+                                    message,
+                                ),
+                                cx,
+                            )
+                        })
+                        .ok();
+                }
+            }
+        })
+        .detach();
     }
 
-    pub fn toggle_git_blame_inline(
+    pub(super) fn open_permalink_to_line(
         &mut self,
-        _: &ToggleGitBlameInline,
+        _: &OpenPermalinkToLine,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.toggle_git_blame_inline_internal(true, window, cx);
-        cx.notify();
+        let permalink_task = self.get_permalink_to_line(cx);
+        let workspace = self.workspace();
+
+        cx.spawn_in(window, async move |_, cx| match permalink_task.await {
+            Ok(permalink) => {
+                cx.update(|_, cx| {
+                    cx.open_url(permalink.as_ref());
+                })
+                .ok();
+            }
+            Err(err) => {
+                let message = format!("Failed to open permalink: {err}");
+
+                anyhow::Result::<()>::Err(err).log_err();
+
+                if let Some(workspace) = workspace {
+                    workspace.update(cx, |workspace, cx| {
+                        struct OpenPermalinkToLine;
+
+                        workspace.show_toast(
+                            Toast::new(NotificationId::unique::<OpenPermalinkToLine>(), message),
+                            cx,
+                        )
+                    });
+                }
+            }
+        })
+        .detach();
     }
 
     pub(super) fn toggle_staged_selected_diff_hunks(