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>( &'a self, ranges: &'a [Range<Anchor>], buffer: &'a MultiBufferSnapshot, ) -> impl 'a + Iterator<Item = MultiBufferDiffHunk> { ranges.iter().flat_map(move |range| { let end_excerpt = buffer.excerpt_containing(range.end..range.end); let range = range.to_point(buffer); let mut peek_end = range.end; if range.end.row < buffer.max_row().0 { peek_end = Point::new(range.end.row + 1, 0); } buffer .diff_hunks_in_range(range.start..peek_end) .filter(move |hunk| { if let Some((_, excerpt_range)) = &end_excerpt && let Some(end_anchor) = buffer.anchor_in_excerpt(excerpt_range.context.end) && let Some(hunk_end_anchor) = buffer.anchor_in_excerpt(hunk.excerpt_range.context.end) && hunk_end_anchor.cmp(&end_anchor, buffer).is_gt() { false } else { true } }) }) } 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 focusLines 1–500 of 2828. Load more
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>( &'a self, ranges: &'a [Range<Anchor>], buffer: &'a MultiBufferSnapshot, ) -> impl 'a + Iterator<Item = MultiBufferDiffHunk> { ranges.iter().flat_map(move |range| { let end_excerpt = buffer.excerpt_containing(range.end..range.end); let range = range.to_point(buffer); let mut peek_end = range.end; if range.end.row < buffer.max_row().0 { peek_end = Point::new(range.end.row + 1, 0); } buffer .diff_hunks_in_range(range.start..peek_end) .filter(move |hunk| { if let Some((_, excerpt_range)) = &end_excerpt && let Some(end_anchor) = buffer.anchor_in_excerpt(excerpt_range.context.end) && let Some(hunk_end_anchor) = buffer.anchor_in_excerpt(hunk.excerpt_range.context.end) && hunk_end_anchor.cmp(&end_anchor, buffer).is_gt() { false } else { true } }) }) } 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