2815 lines · 100.7 KB · Rust
RawDownload
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

Lines 1–500 of 2815.