Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:31:34.878Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

git.rs

3149 lines · 112.4 KB · rust
1pub(super) mod blame;
2
3use super::*;
4use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
5use buffer_diff::{BufferDiff, DiffHunkStatus, DiffHunkStatusKind};
6
7#[derive(Clone)]
8pub struct ResolvedDiffHunk {
9    pub buffer_range: Range<text::Anchor>,
10    pub diff_base_byte_range: Range<usize>,
11    pub status: DiffHunkStatus,
12}
13
14#[derive(Clone)]
15pub struct ResolvedDiffHunks {
16    pub diff: Entity<BufferDiff>,
17    pub buffer_id: BufferId,
18    pub buffer: Option<Entity<Buffer>>,
19    pub hunks: Vec<ResolvedDiffHunk>,
20}
21
22pub trait DiffHunkDelegate {
23    fn toggle(
24        &self,
25        hunks: Vec<ResolvedDiffHunks>,
26        editor: &mut Editor,
27        window: &mut Window,
28        cx: &mut Context<Editor>,
29    );
30
31    fn stage_or_unstage(
32        &self,
33        stage: bool,
34        hunks: Vec<ResolvedDiffHunks>,
35        editor: &mut Editor,
36        window: &mut Window,
37        cx: &mut Context<Editor>,
38    );
39
40    fn restore(
41        &self,
42        hunks: Vec<ResolvedDiffHunks>,
43        editor: &mut Editor,
44        window: &mut Window,
45        cx: &mut Context<Editor>,
46    ) {
47        if hunks.is_empty() || editor.read_only(cx) {
48            return;
49        }
50        self.stage_or_unstage(false, hunks.clone(), editor, window, cx);
51        editor.transact(window, cx, |editor, window, cx| {
52            editor.restore_diff_hunks(hunks, cx);
53            let selections = editor
54                .selections
55                .all::<MultiBufferOffset>(&editor.display_snapshot(cx));
56            editor.change_selections(
57                SelectionEffects::no_scroll(),
58                window,
59                cx,
60                |selections_state| {
61                    selections_state.select(selections);
62                },
63            );
64        });
65    }
66
67    fn render_hunk_controls(
68        &self,
69        row: u32,
70        status: &DiffHunkStatus,
71        hunk_range: Range<Anchor>,
72        is_created_file: bool,
73        line_height: Pixels,
74        editor: &Entity<Editor>,
75        window: &mut Window,
76        cx: &mut App,
77    ) -> AnyElement;
78
79    fn render_hunk_as_staged(&self, status: &DiffHunkStatus, _cx: &App) -> bool {
80        !status.has_secondary_hunk()
81    }
82}
83
84pub struct UncommittedDiffHunkDelegate;
85
86impl DiffHunkDelegate for UncommittedDiffHunkDelegate {
87    fn toggle(
88        &self,
89        hunks: Vec<ResolvedDiffHunks>,
90        editor: &mut Editor,
91        window: &mut Window,
92        cx: &mut Context<Editor>,
93    ) {
94        let stage = hunks
95            .iter()
96            .flat_map(|hunks| hunks.hunks.iter())
97            .any(|hunk| hunk.status.has_secondary_hunk());
98        self.stage_or_unstage(stage, hunks, editor, window, cx);
99    }
100
101    fn stage_or_unstage(
102        &self,
103        stage: bool,
104        hunks: Vec<ResolvedDiffHunks>,
105        editor: &mut Editor,
106        _window: &mut Window,
107        cx: &mut Context<Editor>,
108    ) {
109        let Some(project) = editor.project() else {
110            return;
111        };
112        for hunks in hunks {
113            let Some(buffer) = hunks.buffer else {
114                continue;
115            };
116            let ranges = hunks
117                .hunks
118                .into_iter()
119                .map(|hunk| hunk.buffer_range)
120                .collect::<Vec<_>>();
121            if ranges.is_empty() {
122                continue;
123            }
124            let secondary_diff = hunks.diff.read(cx).secondary_diff();
125            project
126                .update(cx, |project, cx| {
127                    if stage {
128                        let Some(secondary_diff) = secondary_diff else {
129                            return Err(anyhow::anyhow!("diff has no unstaged secondary"));
130                        };
131                        project.stage_hunks(buffer, secondary_diff, ranges, cx)
132                    } else {
133                        project.unstage_uncommitted_hunks(buffer, hunks.diff, ranges, cx)
134                    }
135                })
136                .log_err();
137        }
138    }
139
140    fn render_hunk_controls(
141        &self,
142        row: u32,
143        status: &DiffHunkStatus,
144        hunk_range: Range<Anchor>,
145        is_created_file: bool,
146        line_height: Pixels,
147        editor: &Entity<Editor>,
148        window: &mut Window,
149        cx: &mut App,
150    ) -> AnyElement {
151        render_diff_hunk_controls(
152            row,
153            status,
154            hunk_range,
155            is_created_file,
156            line_height,
157            editor,
158            window,
159            cx,
160        )
161    }
162}
163
164pub struct RestoreOnlyDiffHunkDelegate;
165
166impl DiffHunkDelegate for RestoreOnlyDiffHunkDelegate {
167    fn toggle(
168        &self,
169        _hunks: Vec<ResolvedDiffHunks>,
170        _editor: &mut Editor,
171        _window: &mut Window,
172        _cx: &mut Context<Editor>,
173    ) {
174    }
175
176    fn stage_or_unstage(
177        &self,
178        _stage: bool,
179        _hunks: Vec<ResolvedDiffHunks>,
180        _editor: &mut Editor,
181        _window: &mut Window,
182        _cx: &mut Context<Editor>,
183    ) {
184    }
185
186    fn render_hunk_controls(
187        &self,
188        _row: u32,
189        _status: &DiffHunkStatus,
190        _hunk_range: Range<Anchor>,
191        _is_created_file: bool,
192        _line_height: Pixels,
193        _editor: &Entity<Editor>,
194        _window: &mut Window,
195        _cx: &mut App,
196    ) -> AnyElement {
197        gpui::Empty.into_any_element()
198    }
199}
200
201pub struct RestoreOnlyUnstagedDiffHunkDelegate;
202
203impl DiffHunkDelegate for RestoreOnlyUnstagedDiffHunkDelegate {
204    fn toggle(
205        &self,
206        _hunks: Vec<ResolvedDiffHunks>,
207        _editor: &mut Editor,
208        _window: &mut Window,
209        _cx: &mut Context<Editor>,
210    ) {
211    }
212
213    fn stage_or_unstage(
214        &self,
215        _stage: bool,
216        _hunks: Vec<ResolvedDiffHunks>,
217        _editor: &mut Editor,
218        _window: &mut Window,
219        _cx: &mut Context<Editor>,
220    ) {
221    }
222
223    fn render_hunk_controls(
224        &self,
225        _row: u32,
226        _status: &DiffHunkStatus,
227        _hunk_range: Range<Anchor>,
228        _is_created_file: bool,
229        _line_height: Pixels,
230        _editor: &Entity<Editor>,
231        _window: &mut Window,
232        _cx: &mut App,
233    ) -> AnyElement {
234        gpui::Empty.into_any_element()
235    }
236
237    fn render_hunk_as_staged(&self, _status: &DiffHunkStatus, _cx: &App) -> bool {
238        false
239    }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub(super) enum DisplayDiffHunk {
244    Folded {
245        display_row: DisplayRow,
246    },
247    Unfolded {
248        is_created_file: bool,
249        diff_base_byte_range: Range<usize>,
250        display_row_range: Range<DisplayRow>,
251        multi_buffer_range: Range<Anchor>,
252        status: DiffHunkStatus,
253        word_diffs: Vec<Range<MultiBufferOffset>>,
254    },
255}
256
257#[derive(Clone)]
258pub(super) struct InlineBlamePopoverState {
259    pub(super) scroll_handle: ScrollHandle,
260    pub(super) commit_message: Option<ParsedCommitMessage>,
261    pub(super) markdown: Entity<Markdown>,
262}
263
264pub(super) struct InlineBlamePopover {
265    pub(super) position: gpui::Point<Pixels>,
266    pub(super) hide_task: Option<Task<()>>,
267    pub(super) popover_bounds: Option<Bounds<Pixels>>,
268    pub(super) popover_state: InlineBlamePopoverState,
269    pub(super) keyboard_grace: bool,
270}
271
272/// Represents a diff review button indicator that shows up when hovering over lines in the gutter
273/// in diff view mode.
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub(super) struct PhantomDiffReviewIndicator {
276    /// The starting anchor of the selection (or the only row if not dragging).
277    pub(super) start: Anchor,
278    /// The ending anchor of the selection. Equal to start_anchor for single-line selection.
279    pub(super) end: Anchor,
280    /// There's a small debounce between hovering over the line and showing the indicator.
281    /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel.
282    pub(super) is_active: bool,
283}
284
285#[derive(Clone, Debug)]
286pub(super) struct DiffReviewDragState {
287    start_anchor: Anchor,
288    current_anchor: Anchor,
289}
290
291/// Identifies a specific hunk in the diff buffer.
292/// Used as a key to group comments by their location.
293#[derive(Clone, Debug)]
294pub(super) struct DiffHunkKey {
295    /// The file path (relative to worktree) this hunk belongs to.
296    pub(super) file_path: Arc<util::rel_path::RelPath>,
297    /// An anchor at the start of the hunk. This tracks position as the buffer changes.
298    pub(super) hunk_start_anchor: Anchor,
299}
300
301/// A review comment stored locally before being sent to the Agent panel.
302#[derive(Clone)]
303pub(super) struct StoredReviewComment {
304    /// Unique identifier for this comment (for edit/delete operations).
305    pub(super) id: usize,
306    /// The comment text entered by the user.
307    pub(super) comment: String,
308    /// Anchors for the code range being reviewed.
309    pub(super) range: Range<Anchor>,
310    /// Whether this comment is currently being edited inline.
311    pub(super) is_editing: bool,
312}
313
314/// Represents an active diff review overlay that appears when clicking the "Add Review" button.
315pub(super) struct DiffReviewOverlay {
316    pub(super) anchor_range: Range<Anchor>,
317    /// The block ID for the overlay.
318    pub(super) block_id: CustomBlockId,
319    /// The editor entity for the review input.
320    pub(super) prompt_editor: Entity<Editor>,
321    /// The hunk key this overlay belongs to.
322    pub(super) hunk_key: DiffHunkKey,
323    /// Whether the comments section is expanded.
324    pub(super) comments_expanded: bool,
325    /// Editors for comments currently being edited inline.
326    /// Key: comment ID, Value: Editor entity for inline editing.
327    pub(super) inline_edit_editors: HashMap<usize, Entity<Editor>>,
328    /// Subscriptions for inline edit editors' action handlers.
329    /// Key: comment ID, Value: Subscription keeping the Newline action handler alive.
330    pub(super) inline_edit_subscriptions: HashMap<usize, Subscription>,
331    /// The current user's avatar URI for display in comment rows.
332    pub(super) user_avatar_uri: Option<SharedUri>,
333    /// Subscription to keep the action handler alive.
334    _subscription: Subscription,
335}
336
337impl DiffReviewDragState {
338    pub(super) fn row_range(
339        &self,
340        snapshot: &DisplaySnapshot,
341    ) -> std::ops::RangeInclusive<DisplayRow> {
342        let start = self.start_anchor.to_display_point(snapshot).row();
343        let current = self.current_anchor.to_display_point(snapshot).row();
344
345        (start..=current).sorted()
346    }
347}
348
349impl StoredReviewComment {
350    fn new(id: usize, comment: String, anchor_range: Range<Anchor>) -> Self {
351        Self {
352            id,
353            comment,
354            range: anchor_range,
355            is_editing: false,
356        }
357    }
358}
359
360impl Editor {
361    pub fn diff_hunks_in_ranges<'a>(
362        &'a self,
363        ranges: &'a [Range<Anchor>],
364        buffer: &'a MultiBufferSnapshot,
365    ) -> impl 'a + Iterator<Item = MultiBufferDiffHunk> {
366        ranges.iter().flat_map(move |range| {
367            let end_excerpt = buffer.excerpt_containing(range.end..range.end);
368            let range = range.to_point(buffer);
369            let mut peek_end = range.end;
370            if range.end.row < buffer.max_row().0 {
371                peek_end = Point::new(range.end.row + 1, 0);
372            }
373            buffer
374                .diff_hunks_in_range(range.start..peek_end)
375                .filter(move |hunk| {
376                    if let Some((_, excerpt_range)) = &end_excerpt
377                        && let Some(end_anchor) =
378                            buffer.anchor_in_excerpt(excerpt_range.context.end)
379                        && let Some(hunk_end_anchor) =
380                            buffer.anchor_in_excerpt(hunk.excerpt_range.context.end)
381                        && hunk_end_anchor.cmp(&end_anchor, buffer).is_gt()
382                    {
383                        false
384                    } else {
385                        true
386                    }
387                })
388        })
389    }
390
391    fn resolve_diff_hunks(
392        &self,
393        hunks: Vec<MultiBufferDiffHunk>,
394        cx: &App,
395    ) -> Vec<ResolvedDiffHunks> {
396        let multibuffer = self.buffer().read(cx);
397        let chunk_by = hunks.into_iter().chunk_by(|hunk| hunk.buffer_id);
398        let mut resolved = Vec::new();
399
400        for (source_buffer_id, hunks) in &chunk_by {
401            let Some(diff) = multibuffer.diff_for(source_buffer_id) else {
402                continue;
403            };
404            let diff_snapshot = diff.read(cx).snapshot(cx);
405            let main_buffer_id = diff_snapshot.buffer_id();
406            let buffer = multibuffer.buffer(main_buffer_id).or_else(|| {
407                self.project
408                    .as_ref()
409                    .and_then(|project| project.read(cx).buffer_for_id(main_buffer_id, cx))
410            });
411            let mut resolved_hunks = Vec::new();
412
413            for hunk in hunks {
414                if hunk.buffer_id == main_buffer_id {
415                    resolved_hunks.push(ResolvedDiffHunk {
416                        buffer_range: hunk.buffer_range,
417                        diff_base_byte_range: hunk.diff_base_byte_range.start.0
418                            ..hunk.diff_base_byte_range.end.0,
419                        status: hunk.status,
420                    });
421                } else {
422                    let diff_base_byte_range =
423                        hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0;
424                    let Some(hunk) = diff_snapshot
425                        .hunks_intersecting_base_text_range(
426                            diff_base_byte_range.clone(),
427                            diff_snapshot.buffer_snapshot(),
428                        )
429                        .find(|hunk| hunk.diff_base_byte_range == diff_base_byte_range)
430                    else {
431                        continue;
432                    };
433                    let kind = if hunk.buffer_range.start == hunk.buffer_range.end {
434                        DiffHunkStatusKind::Deleted
435                    } else if hunk.diff_base_byte_range.is_empty() {
436                        DiffHunkStatusKind::Added
437                    } else {
438                        DiffHunkStatusKind::Modified
439                    };
440                    resolved_hunks.push(ResolvedDiffHunk {
441                        buffer_range: hunk.buffer_range,
442                        diff_base_byte_range: hunk.diff_base_byte_range,
443                        status: DiffHunkStatus {
444                            kind,
445                            secondary: hunk.secondary_status,
446                        },
447                    });
448                }
449            }
450
451            if !resolved_hunks.is_empty() {
452                resolved.push(ResolvedDiffHunks {
453                    diff,
454                    buffer_id: main_buffer_id,
455                    buffer,
456                    hunks: resolved_hunks,
457                });
458            }
459        }
460
461        resolved
462    }
463
464    pub fn diff_hunk_delegate(&self) -> Arc<dyn DiffHunkDelegate> {
465        self.diff_hunk_delegate
466            .clone()
467            .unwrap_or_else(|| Arc::new(UncommittedDiffHunkDelegate))
468    }
469
470    pub fn set_diff_hunk_delegate(
471        &mut self,
472        delegate: Option<Arc<dyn DiffHunkDelegate>>,
473        cx: &mut Context<Self>,
474    ) {
475        let had_delegate = self.diff_hunk_delegate.is_some();
476        let has_delegate = delegate.is_some();
477        self.diff_hunk_delegate = delegate;
478
479        if !had_delegate && has_delegate {
480            self.load_diff_task.take();
481        } else if had_delegate && !has_delegate {
482            self.buffer.update(cx, |buffer, cx| {
483                buffer.set_all_diff_hunks_collapsed(cx);
484            });
485
486            if let Some(project) = self.project.clone() {
487                self.load_diff_task = Some(
488                    update_uncommitted_diff_for_buffer(
489                        cx.entity(),
490                        &project,
491                        self.buffer.read(cx).all_buffers(),
492                        self.buffer.clone(),
493                        cx,
494                    )
495                    .shared(),
496                );
497            }
498        }
499
500        cx.notify();
501    }
502
503    pub fn git_blame_inline_enabled(&self) -> bool {
504        self.git_blame_inline_enabled
505    }
506
507    pub fn blame(&self) -> Option<&Entity<GitBlame>> {
508        self.blame.as_ref()
509    }
510
511    pub fn active_git_blame_entry(&self, cx: &mut App) -> Option<BlameEntry> {
512        if !self.show_git_blame_inline
513            || self.newest_selection_head_on_empty_line(cx)
514            || !self.has_blame_entries(cx)
515        {
516            return None;
517        }
518
519        let blame = self.blame.as_ref()?;
520        let snapshot = self.display_snapshot(cx);
521        let cursor = self.selections.newest::<Point>(&snapshot).head();
522        let (buffer, point) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)?;
523
524        blame
525            .update(cx, |blame, cx| {
526                blame
527                    .blame_for_rows(
528                        &[RowInfo {
529                            buffer_id: Some(buffer.remote_id()),
530                            buffer_row: Some(point.row),
531                            ..Default::default()
532                        }],
533                        cx,
534                    )
535                    .next()
536            })
537            .flatten()
538            .map(|(_, entry)| entry)
539    }
540
541    pub fn show_git_blame_gutter(&self) -> bool {
542        self.show_git_blame_gutter
543    }
544
545    pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
546        let ranges: Vec<_> = self
547            .selections
548            .disjoint_anchors()
549            .iter()
550            .map(|s| s.range())
551            .collect();
552        self.buffer
553            .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx))
554    }
555
556    pub fn toggle_git_blame(
557        &mut self,
558        _: &::git::Blame,
559        window: &mut Window,
560        cx: &mut Context<Self>,
561    ) {
562        self.show_git_blame_gutter = !self.show_git_blame_gutter;
563
564        if self.show_git_blame_gutter && !self.has_blame_entries(cx) {
565            self.start_git_blame(true, window, cx);
566        }
567
568        cx.notify();
569    }
570
571    pub fn toggle_git_blame_inline(
572        &mut self,
573        _: &ToggleGitBlameInline,
574        window: &mut Window,
575        cx: &mut Context<Self>,
576    ) {
577        self.toggle_git_blame_inline_internal(true, window, cx);
578        cx.notify();
579    }
580
581    /// Hides the inline blame popover element, in case it's already visible, or
582    /// interrupts the task meant to show it, in case the task is running.
583    ///
584    /// When `ignore_timeout` is set to `true`, the popover is hidden
585    /// immediately, otherwise it'll be hidden after a short delay.
586    ///
587    /// Returns `true` if the popover was visible and was hidden, `false`
588    /// otherwise.
589    pub fn hide_blame_popover(&mut self, ignore_timeout: bool, cx: &mut Context<Self>) -> bool {
590        self.inline_blame_popover_show_task.take();
591
592        if let Some(state) = &mut self.inline_blame_popover {
593            if ignore_timeout {
594                self.inline_blame_popover.take();
595                cx.notify();
596            } else {
597                state.hide_task = Some(cx.spawn(async move |editor, cx| {
598                    cx.background_executor()
599                        .timer(std::time::Duration::from_millis(100))
600                        .await;
601
602                    editor
603                        .update(cx, |editor, cx| {
604                            editor.inline_blame_popover.take();
605                            cx.notify();
606                        })
607                        .ok();
608                }));
609            }
610
611            true
612        } else {
613            false
614        }
615    }
616
617    pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context<Self>) {
618        if self.read_only(cx) {
619            return;
620        }
621        let selections = self
622            .selections
623            .all(&self.display_snapshot(cx))
624            .into_iter()
625            .map(|s| s.range())
626            .collect();
627        self.restore_hunks_in_ranges(selections, window, cx);
628    }
629
630    pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
631        if let Some(status) = self
632            .addons
633            .iter()
634            .find_map(|(_, addon)| addon.override_status_for_buffer_id(buffer_id, cx))
635        {
636            return Some(status);
637        }
638        self.project
639            .as_ref()?
640            .read(cx)
641            .status_for_buffer_id(buffer_id, cx)
642    }
643
644    pub fn go_to_hunk_before_or_after_position(
645        &mut self,
646        snapshot: &EditorSnapshot,
647        position: Point,
648        direction: Direction,
649        wrap_around: bool,
650        window: &mut Window,
651        cx: &mut Context<Editor>,
652    ) {
653        let row = if direction == Direction::Next {
654            self.hunk_after_position(snapshot, position, wrap_around)
655                .map(|hunk| hunk.row_range.start)
656        } else {
657            self.hunk_before_position(snapshot, position, wrap_around)
658        };
659
660        if let Some(row) = row {
661            let destination = Point::new(row.0, 0);
662            let autoscroll = Autoscroll::center();
663
664            self.unfold_ranges(&[destination..destination], false, false, cx);
665            self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
666                s.select_ranges([destination..destination]);
667            });
668        }
669    }
670
671    pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) {
672        self.buffer.update(cx, |buffer, cx| {
673            buffer.set_all_diff_hunks_expanded(cx);
674        });
675    }
676
677    pub fn expand_all_diff_hunks(
678        &mut self,
679        _: &ExpandAllDiffHunks,
680        _window: &mut Window,
681        cx: &mut Context<Self>,
682    ) {
683        self.buffer.update(cx, |buffer, cx| {
684            buffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx)
685        });
686    }
687
688    pub fn show_diff_review_overlay(
689        &mut self,
690        display_range: Range<DisplayRow>,
691        window: &mut Window,
692        cx: &mut Context<Self>,
693    ) {
694        let Range { start, end } = display_range.sorted();
695
696        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
697        let editor_snapshot = self.snapshot(window, cx);
698
699        // Convert display rows to multibuffer points
700        let start_point = editor_snapshot
701            .display_snapshot
702            .display_point_to_point(start.as_display_point(), Bias::Left);
703        let end_point = editor_snapshot
704            .display_snapshot
705            .display_point_to_point(end.as_display_point(), Bias::Left);
706        let end_multi_buffer_row = MultiBufferRow(end_point.row);
707
708        // Create anchor range for the selected lines (start of first line to end of last line)
709        let line_end = Point::new(
710            end_point.row,
711            buffer_snapshot.line_len(end_multi_buffer_row),
712        );
713        let anchor_range =
714            buffer_snapshot.anchor_after(start_point)..buffer_snapshot.anchor_before(line_end);
715
716        // Compute the hunk key for this display row
717        let file_path = buffer_snapshot
718            .file_at(start_point)
719            .map(|file: &Arc<dyn language::File>| file.path().clone())
720            .unwrap_or_else(|| Arc::from(util::rel_path::RelPath::empty()));
721        let hunk_start_anchor = buffer_snapshot.anchor_before(start_point);
722        let new_hunk_key = DiffHunkKey {
723            file_path,
724            hunk_start_anchor,
725        };
726
727        // Check if we already have an overlay for this hunk
728        if let Some(existing_overlay) = self.diff_review_overlays.iter().find(|overlay| {
729            Self::hunk_keys_match(&overlay.hunk_key, &new_hunk_key, &buffer_snapshot)
730        }) {
731            // Just focus the existing overlay's prompt editor
732            let focus_handle = existing_overlay.prompt_editor.focus_handle(cx);
733            window.focus(&focus_handle, cx);
734            return;
735        }
736
737        // Dismiss overlays that have no comments for their hunks
738        self.dismiss_overlays_without_comments(cx);
739
740        // Get the current user's avatar URI from the project's user_store
741        let user_avatar_uri = self.project.as_ref().and_then(|project| {
742            let user_store = project.read(cx).user_store();
743            user_store
744                .read(cx)
745                .current_user()
746                .map(|user| user.avatar_uri.clone())
747        });
748
749        // Create anchor at the end of the last row so the block appears immediately below it
750        // Use multibuffer coordinates for anchor creation
751        let line_len = buffer_snapshot.line_len(end_multi_buffer_row);
752        let anchor = buffer_snapshot.anchor_after(Point::new(end_multi_buffer_row.0, line_len));
753
754        // Use the hunk key we already computed
755        let hunk_key = new_hunk_key;
756
757        // Create the prompt editor for the review input
758        let prompt_editor = cx.new(|cx| {
759            let mut editor = Editor::single_line(window, cx);
760            editor.set_placeholder_text("Add a review comment...", window, cx);
761            editor
762        });
763
764        // Register the Newline action on the prompt editor to submit the review
765        let parent_editor = cx.entity().downgrade();
766        let subscription = prompt_editor.update(cx, |prompt_editor, _cx| {
767            prompt_editor.register_action({
768                let parent_editor = parent_editor.clone();
769                move |_: &crate::actions::Newline, window, cx| {
770                    if let Some(editor) = parent_editor.upgrade() {
771                        editor.update(cx, |editor, cx| {
772                            editor.submit_diff_review_comment(window, cx);
773                        });
774                    }
775                }
776            })
777        });
778
779        // Calculate initial height based on existing comments for this hunk
780        let initial_height = self.calculate_overlay_height(&hunk_key, true, &buffer_snapshot);
781
782        // Create the overlay block
783        let prompt_editor_for_render = prompt_editor.clone();
784        let hunk_key_for_render = hunk_key.clone();
785        let editor_handle = cx.entity().downgrade();
786        let block = BlockProperties {
787            style: BlockStyle::Sticky,
788            placement: BlockPlacement::Below(anchor),
789            height: Some(initial_height),
790            render: Arc::new(move |cx| {
791                Self::render_diff_review_overlay(
792                    &prompt_editor_for_render,
793                    &hunk_key_for_render,
794                    &editor_handle,
795                    cx,
796                )
797            }),
798            priority: 0,
799        };
800
801        let block_ids = self.insert_blocks([block], None, cx);
802        let Some(block_id) = block_ids.into_iter().next() else {
803            log::error!("Failed to insert diff review overlay block");
804            return;
805        };
806
807        self.diff_review_overlays.push(DiffReviewOverlay {
808            anchor_range,
809            block_id,
810            prompt_editor: prompt_editor.clone(),
811            hunk_key,
812            comments_expanded: true,
813            inline_edit_editors: HashMap::default(),
814            inline_edit_subscriptions: HashMap::default(),
815            user_avatar_uri,
816            _subscription: subscription,
817        });
818
819        // Focus the prompt editor
820        let focus_handle = prompt_editor.focus_handle(cx);
821        window.focus(&focus_handle, cx);
822
823        cx.notify();
824    }
825
826    /// Stores the diff review comment locally.
827    /// Comments are stored per-hunk and can later be batch-submitted to the Agent panel.
828    pub fn submit_diff_review_comment(&mut self, window: &mut Window, cx: &mut Context<Self>) {
829        // Find the overlay that currently has focus
830        let overlay_index = self
831            .diff_review_overlays
832            .iter()
833            .position(|overlay| overlay.prompt_editor.focus_handle(cx).is_focused(window));
834        let Some(overlay_index) = overlay_index else {
835            return;
836        };
837        let overlay = &self.diff_review_overlays[overlay_index];
838
839        let comment_text = overlay.prompt_editor.read(cx).text(cx).trim().to_string();
840        if comment_text.is_empty() {
841            return;
842        }
843
844        let anchor_range = overlay.anchor_range.clone();
845        let hunk_key = overlay.hunk_key.clone();
846
847        self.add_review_comment(hunk_key.clone(), comment_text, anchor_range, cx);
848
849        // Clear the prompt editor but keep the overlay open
850        if let Some(overlay) = self.diff_review_overlays.get(overlay_index) {
851            overlay.prompt_editor.update(cx, |editor, cx| {
852                editor.clear(window, cx);
853            });
854        }
855
856        // Refresh the overlay to update the block height for the new comment
857        self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
858
859        cx.notify();
860    }
861
862    /// Returns the prompt editor for the diff review overlay, if one is active.
863    /// This is primarily used for testing.
864    pub fn diff_review_prompt_editor(&self) -> Option<&Entity<Editor>> {
865        self.diff_review_overlays
866            .first()
867            .map(|overlay| &overlay.prompt_editor)
868    }
869
870    /// Sets whether the comments section is expanded in the diff review overlay.
871    /// This is primarily used for testing.
872    pub fn set_diff_review_comments_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) {
873        for overlay in &mut self.diff_review_overlays {
874            overlay.comments_expanded = expanded;
875        }
876        cx.notify();
877    }
878
879    /// Returns the total count of stored review comments across all hunks.
880    pub(super) fn total_review_comment_count(&self) -> usize {
881        self.stored_review_comments
882            .iter()
883            .map(|(_, v)| v.len())
884            .sum()
885    }
886
887    /// Adds a new review comment to a specific hunk.
888    pub(super) fn add_review_comment(
889        &mut self,
890        hunk_key: DiffHunkKey,
891        comment: String,
892        anchor_range: Range<Anchor>,
893        cx: &mut Context<Self>,
894    ) -> usize {
895        let id = self.next_review_comment_id;
896        self.next_review_comment_id += 1;
897
898        let stored_comment = StoredReviewComment::new(id, comment, anchor_range);
899
900        let snapshot = self.buffer.read(cx).snapshot(cx);
901        let key_point = hunk_key.hunk_start_anchor.to_point(&snapshot);
902
903        // Find existing entry for this hunk or add a new one
904        if let Some((_, comments)) = self.stored_review_comments.iter_mut().find(|(k, _)| {
905            k.file_path == hunk_key.file_path
906                && k.hunk_start_anchor.to_point(&snapshot) == key_point
907        }) {
908            comments.push(stored_comment);
909        } else {
910            self.stored_review_comments
911                .push((hunk_key, vec![stored_comment]));
912        }
913
914        cx.emit(EditorEvent::ReviewCommentsChanged {
915            total_count: self.total_review_comment_count(),
916        });
917        cx.notify();
918        id
919    }
920
921    pub(super) fn blame_hover(
922        &mut self,
923        _: &BlameHover,
924        window: &mut Window,
925        cx: &mut Context<Self>,
926    ) {
927        let just_started = self.blame.is_none();
928        if just_started {
929            self.start_git_blame(true, window, cx);
930        }
931        let Some(blame) = self.blame.as_ref() else {
932            return;
933        };
934
935        if just_started && !blame.read(cx).has_generated_entries() {
936            let subscription = cx.observe_in(blame, window, |editor, blame, window, cx| {
937                if blame.read(cx).has_generated_entries() {
938                    editor.pending_blame_hover_observation.take();
939                    editor.show_blame_hover_popover(window, cx);
940                }
941            });
942            self.pending_blame_hover_observation = Some(subscription);
943            return;
944        }
945
946        self.show_blame_hover_popover(window, cx);
947    }
948
949    fn show_blame_hover_popover(&mut self, window: &mut Window, cx: &mut Context<Self>) {
950        let snapshot = self.snapshot(window, cx);
951        let cursor = self
952            .selections
953            .newest::<Point>(&snapshot.display_snapshot)
954            .head();
955        let Some((buffer, point)) = snapshot.buffer_snapshot().point_to_buffer_point(cursor) else {
956            return;
957        };
958
959        let Some(blame) = self.blame.as_ref() else {
960            return;
961        };
962
963        let row_info = RowInfo {
964            buffer_id: Some(buffer.remote_id()),
965            buffer_row: Some(point.row),
966            ..Default::default()
967        };
968        let Some((buffer, blame_entry)) = blame
969            .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
970            .flatten()
971        else {
972            return;
973        };
974
975        let anchor = self.selections.newest_anchor().head();
976        let position = self.to_pixel_point(anchor, &snapshot, window, cx);
977        if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) {
978            self.show_blame_popover(
979                buffer,
980                &blame_entry,
981                position + last_bounds.origin,
982                true,
983                cx,
984            );
985        };
986    }
987
988    pub(super) fn restore_file(
989        &mut self,
990        _: &::git::RestoreFile,
991        window: &mut Window,
992        cx: &mut Context<Self>,
993    ) {
994        if self.read_only(cx) {
995            return;
996        }
997        let mut buffer_ids = HashSet::default();
998        let snapshot = self.buffer().read(cx).snapshot(cx);
999        for selection in self
1000            .selections
1001            .all::<MultiBufferOffset>(&self.display_snapshot(cx))
1002        {
1003            buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range()))
1004        }
1005
1006        let ranges = buffer_ids
1007            .into_iter()
1008            .flat_map(|buffer_id| snapshot.range_for_buffer(buffer_id))
1009            .collect::<Vec<_>>();
1010
1011        self.restore_hunks_in_ranges(ranges, window, cx);
1012    }
1013
1014    /// Restores the diff hunks in the editor's selections and moves the cursor
1015    /// to the next diff hunk. Wraps around to the beginning of the buffer if
1016    /// not all diff hunks are expanded.
1017    pub(super) fn restore_and_next(
1018        &mut self,
1019        _: &::git::RestoreAndNext,
1020        window: &mut Window,
1021        cx: &mut Context<Self>,
1022    ) {
1023        if self.read_only(cx) {
1024            return;
1025        }
1026        let selections = self
1027            .selections
1028            .all(&self.display_snapshot(cx))
1029            .into_iter()
1030            .map(|selection| selection.range())
1031            .collect();
1032
1033        self.restore_hunks_in_ranges(selections, window, cx);
1034
1035        let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
1036        let wrap_around = !all_diff_hunks_expanded;
1037        let snapshot = self.snapshot(window, cx);
1038        let position = self
1039            .selections
1040            .newest::<Point>(&snapshot.display_snapshot)
1041            .head();
1042
1043        self.go_to_hunk_before_or_after_position(
1044            &snapshot,
1045            position,
1046            Direction::Next,
1047            wrap_around,
1048            window,
1049            cx,
1050        );
1051    }
1052
1053    pub fn restore_diff_hunks(&mut self, hunks: Vec<ResolvedDiffHunks>, cx: &mut Context<Self>) {
1054        let mut revert_changes = Vec::new();
1055        for hunks in hunks {
1056            let Some(buffer) = hunks.buffer else {
1057                continue;
1058            };
1059            let diff_snapshot = hunks.diff.read(cx).snapshot(cx);
1060            let changes = hunks
1061                .hunks
1062                .into_iter()
1063                .filter_map(|hunk| {
1064                    if hunk.diff_base_byte_range == (0..0)
1065                        && hunk.buffer_range.start.is_min()
1066                        && hunk.buffer_range.end.is_max()
1067                    {
1068                        return None;
1069                    }
1070                    let original_text = diff_snapshot
1071                        .base_text()
1072                        .as_rope()
1073                        .slice(hunk.diff_base_byte_range.start..hunk.diff_base_byte_range.end);
1074                    Some((hunk.buffer_range, original_text))
1075                })
1076                .collect::<Vec<_>>();
1077            if !changes.is_empty() {
1078                revert_changes.push((buffer, changes));
1079            }
1080        }
1081
1082        for (buffer, changes) in revert_changes {
1083            buffer.update(cx, |buffer, cx| {
1084                buffer.edit(
1085                    changes
1086                        .into_iter()
1087                        .map(|(range, text)| (range, text.to_string())),
1088                    None,
1089                    cx,
1090                );
1091            });
1092        }
1093    }
1094
1095    pub(super) fn go_to_next_hunk(
1096        &mut self,
1097        _: &GoToHunk,
1098        window: &mut Window,
1099        cx: &mut Context<Self>,
1100    ) {
1101        let snapshot = self.snapshot(window, cx);
1102        let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
1103        self.go_to_hunk_before_or_after_position(
1104            &snapshot,
1105            selection.head(),
1106            Direction::Next,
1107            true,
1108            window,
1109            cx,
1110        );
1111    }
1112
1113    pub(super) fn collapse_all_diff_hunks(
1114        &mut self,
1115        _: &CollapseAllDiffHunks,
1116        _window: &mut Window,
1117        cx: &mut Context<Self>,
1118    ) {
1119        self.buffer.update(cx, |buffer, cx| {
1120            buffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], cx)
1121        });
1122    }
1123
1124    pub fn toggle_all_diff_hunks(
1125        &mut self,
1126        _: &ToggleAllDiffHunks,
1127        window: &mut Window,
1128        cx: &mut Context<Self>,
1129    ) {
1130        if self.has_any_expanded_diff_hunks(cx) {
1131            self.collapse_all_diff_hunks(&CollapseAllDiffHunks, window, cx);
1132        } else {
1133            self.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
1134        }
1135    }
1136
1137    pub(super) fn toggle_selected_diff_hunks(
1138        &mut self,
1139        _: &ToggleSelectedDiffHunks,
1140        _window: &mut Window,
1141        cx: &mut Context<Self>,
1142    ) {
1143        let ranges: Vec<_> = self
1144            .selections
1145            .disjoint_anchors()
1146            .iter()
1147            .map(|s| s.range())
1148            .collect();
1149        self.toggle_diff_hunks_in_ranges(ranges, cx);
1150    }
1151
1152    pub(super) fn show_diff_review_button(&self) -> bool {
1153        self.show_diff_review_button
1154    }
1155
1156    pub(super) fn render_diff_review_button(
1157        &self,
1158        display_row: DisplayRow,
1159        width: Pixels,
1160        cx: &mut Context<Self>,
1161    ) -> impl IntoElement {
1162        let text_color = cx.theme().colors().text;
1163        let icon_color = cx.theme().colors().icon_accent;
1164
1165        h_flex()
1166            .id("diff_review_button")
1167            .cursor_pointer()
1168            .w(width - px(1.))
1169            .h(relative(0.9))
1170            .justify_center()
1171            .rounded_sm()
1172            .border_1()
1173            .border_color(text_color.opacity(0.1))
1174            .bg(text_color.opacity(0.15))
1175            .hover(|s| {
1176                s.bg(icon_color.opacity(0.4))
1177                    .border_color(icon_color.opacity(0.5))
1178            })
1179            .child(Icon::new(IconName::Plus).size(IconSize::Small))
1180            .tooltip(Tooltip::text("Add Review (drag to select multiple lines)"))
1181            .on_mouse_down(
1182                gpui::MouseButton::Left,
1183                cx.listener(move |editor, _event: &gpui::MouseDownEvent, window, cx| {
1184                    editor.start_diff_review_drag(display_row, window, cx);
1185                }),
1186            )
1187    }
1188
1189    pub(super) fn start_diff_review_drag(
1190        &mut self,
1191        display_row: DisplayRow,
1192        window: &mut Window,
1193        cx: &mut Context<Self>,
1194    ) {
1195        let snapshot = self.snapshot(window, cx);
1196        let point = snapshot
1197            .display_snapshot
1198            .display_point_to_point(DisplayPoint::new(display_row, 0), Bias::Left);
1199        let anchor = snapshot.buffer_snapshot().anchor_before(point);
1200        self.diff_review_drag_state = Some(DiffReviewDragState {
1201            start_anchor: anchor,
1202            current_anchor: anchor,
1203        });
1204        cx.notify();
1205    }
1206
1207    pub(super) fn update_diff_review_drag(
1208        &mut self,
1209        display_row: DisplayRow,
1210        window: &mut Window,
1211        cx: &mut Context<Self>,
1212    ) {
1213        if self.diff_review_drag_state.is_none() {
1214            return;
1215        }
1216        let snapshot = self.snapshot(window, cx);
1217        let point = snapshot
1218            .display_snapshot
1219            .display_point_to_point(display_row.as_display_point(), Bias::Left);
1220        let anchor = snapshot.buffer_snapshot().anchor_before(point);
1221        if let Some(drag_state) = &mut self.diff_review_drag_state {
1222            drag_state.current_anchor = anchor;
1223            cx.notify();
1224        }
1225    }
1226
1227    pub(super) fn end_diff_review_drag(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1228        if let Some(drag_state) = self.diff_review_drag_state.take() {
1229            let snapshot = self.snapshot(window, cx);
1230            let range = drag_state.row_range(&snapshot.display_snapshot);
1231            self.show_diff_review_overlay(*range.start()..*range.end(), window, cx);
1232        }
1233        cx.notify();
1234    }
1235
1236    pub(super) fn cancel_diff_review_drag(&mut self, cx: &mut Context<Self>) {
1237        self.diff_review_drag_state = None;
1238        cx.notify();
1239    }
1240
1241    /// Dismisses all diff review overlays.
1242    pub(super) fn dismiss_all_diff_review_overlays(&mut self, cx: &mut Context<Self>) {
1243        if self.diff_review_overlays.is_empty() {
1244            return;
1245        }
1246        let block_ids: HashSet<_> = self
1247            .diff_review_overlays
1248            .drain(..)
1249            .map(|overlay| overlay.block_id)
1250            .collect();
1251        self.remove_blocks(block_ids, None, cx);
1252        cx.notify();
1253    }
1254
1255    /// Action handler for SubmitDiffReviewComment.
1256    pub(super) fn submit_diff_review_comment_action(
1257        &mut self,
1258        _: &SubmitDiffReviewComment,
1259        window: &mut Window,
1260        cx: &mut Context<Self>,
1261    ) {
1262        self.submit_diff_review_comment(window, cx);
1263    }
1264
1265    /// Returns comments for a specific hunk, ordered by creation time.
1266    pub(super) fn comments_for_hunk<'a>(
1267        &'a self,
1268        key: &DiffHunkKey,
1269        snapshot: &MultiBufferSnapshot,
1270    ) -> &'a [StoredReviewComment] {
1271        let key_point = key.hunk_start_anchor.to_point(snapshot);
1272        self.stored_review_comments
1273            .iter()
1274            .find(|(k, _)| {
1275                k.file_path == key.file_path && k.hunk_start_anchor.to_point(snapshot) == key_point
1276            })
1277            .map(|(_, comments)| comments.as_slice())
1278            .unwrap_or(&[])
1279    }
1280
1281    /// Returns the count of comments for a specific hunk.
1282    pub(super) fn hunk_comment_count(
1283        &self,
1284        key: &DiffHunkKey,
1285        snapshot: &MultiBufferSnapshot,
1286    ) -> usize {
1287        let key_point = key.hunk_start_anchor.to_point(snapshot);
1288        self.stored_review_comments
1289            .iter()
1290            .find(|(k, _)| {
1291                k.file_path == key.file_path && k.hunk_start_anchor.to_point(snapshot) == key_point
1292            })
1293            .map(|(_, v)| v.len())
1294            .unwrap_or(0)
1295    }
1296
1297    /// Removes a review comment by ID from any hunk.
1298    pub(super) fn remove_review_comment(&mut self, id: usize, cx: &mut Context<Self>) -> bool {
1299        for (_, comments) in self.stored_review_comments.iter_mut() {
1300            if let Some(index) = comments.iter().position(|c| c.id == id) {
1301                comments.remove(index);
1302                cx.emit(EditorEvent::ReviewCommentsChanged {
1303                    total_count: self.total_review_comment_count(),
1304                });
1305                cx.notify();
1306                return true;
1307            }
1308        }
1309        false
1310    }
1311
1312    /// Updates a review comment's text by ID.
1313    pub(super) fn update_review_comment(
1314        &mut self,
1315        id: usize,
1316        new_comment: String,
1317        cx: &mut Context<Self>,
1318    ) -> bool {
1319        for (_, comments) in self.stored_review_comments.iter_mut() {
1320            if let Some(comment) = comments.iter_mut().find(|c| c.id == id) {
1321                comment.comment = new_comment;
1322                comment.is_editing = false;
1323                cx.emit(EditorEvent::ReviewCommentsChanged {
1324                    total_count: self.total_review_comment_count(),
1325                });
1326                cx.notify();
1327                return true;
1328            }
1329        }
1330        false
1331    }
1332
1333    /// Sets a comment's editing state.
1334    pub(super) fn set_comment_editing(
1335        &mut self,
1336        id: usize,
1337        is_editing: bool,
1338        cx: &mut Context<Self>,
1339    ) {
1340        for (_, comments) in self.stored_review_comments.iter_mut() {
1341            if let Some(comment) = comments.iter_mut().find(|c| c.id == id) {
1342                comment.is_editing = is_editing;
1343                cx.notify();
1344                return;
1345            }
1346        }
1347    }
1348
1349    /// Removes review comments whose anchors are no longer valid or whose
1350    /// associated diff hunks no longer exist.
1351    ///
1352    /// This should be called when the buffer changes to prevent orphaned comments
1353    /// from accumulating.
1354    pub(super) fn cleanup_orphaned_review_comments(&mut self, cx: &mut Context<Self>) {
1355        let snapshot = self.buffer.read(cx).snapshot(cx);
1356        let original_count = self.total_review_comment_count();
1357
1358        // Remove comments with invalid hunk anchors
1359        self.stored_review_comments
1360            .retain(|(hunk_key, _)| hunk_key.hunk_start_anchor.is_valid(&snapshot));
1361
1362        // Also clean up individual comments with invalid anchor ranges
1363        for (_, comments) in &mut self.stored_review_comments {
1364            comments.retain(|comment| {
1365                comment.range.start.is_valid(&snapshot) && comment.range.end.is_valid(&snapshot)
1366            });
1367        }
1368
1369        // Remove empty hunk entries
1370        self.stored_review_comments
1371            .retain(|(_, comments)| !comments.is_empty());
1372
1373        let new_count = self.total_review_comment_count();
1374        if new_count != original_count {
1375            cx.emit(EditorEvent::ReviewCommentsChanged {
1376                total_count: new_count,
1377            });
1378            cx.notify();
1379        }
1380    }
1381
1382    /// Toggles the expanded state of the comments section in the overlay.
1383    pub(super) fn toggle_review_comments_expanded(
1384        &mut self,
1385        _: &ToggleReviewCommentsExpanded,
1386        window: &mut Window,
1387        cx: &mut Context<Self>,
1388    ) {
1389        // Find the overlay that currently has focus, or use the first one
1390        let overlay_info = self.diff_review_overlays.iter_mut().find_map(|overlay| {
1391            if overlay.prompt_editor.focus_handle(cx).is_focused(window) {
1392                overlay.comments_expanded = !overlay.comments_expanded;
1393                Some(overlay.hunk_key.clone())
1394            } else {
1395                None
1396            }
1397        });
1398
1399        // If no focused overlay found, toggle the first one
1400        let hunk_key = overlay_info.or_else(|| {
1401            self.diff_review_overlays.first_mut().map(|overlay| {
1402                overlay.comments_expanded = !overlay.comments_expanded;
1403                overlay.hunk_key.clone()
1404            })
1405        });
1406
1407        if let Some(hunk_key) = hunk_key {
1408            self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
1409            cx.notify();
1410        }
1411    }
1412
1413    /// Handles the EditReviewComment action - sets a comment into editing mode.
1414    pub(super) fn edit_review_comment(
1415        &mut self,
1416        action: &EditReviewComment,
1417        window: &mut Window,
1418        cx: &mut Context<Self>,
1419    ) {
1420        let comment_id = action.id;
1421
1422        // Set the comment to editing mode
1423        self.set_comment_editing(comment_id, true, cx);
1424
1425        // Find the overlay that contains this comment and create an inline editor if needed
1426        // First, find which hunk this comment belongs to
1427        let hunk_key = self
1428            .stored_review_comments
1429            .iter()
1430            .find_map(|(key, comments)| {
1431                if comments.iter().any(|c| c.id == comment_id) {
1432                    Some(key.clone())
1433                } else {
1434                    None
1435                }
1436            });
1437
1438        let snapshot = self.buffer.read(cx).snapshot(cx);
1439        if let Some(hunk_key) = hunk_key {
1440            if let Some(overlay) = self
1441                .diff_review_overlays
1442                .iter_mut()
1443                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
1444            {
1445                if let std::collections::hash_map::Entry::Vacant(entry) =
1446                    overlay.inline_edit_editors.entry(comment_id)
1447                {
1448                    // Find the comment text
1449                    let comment_text = self
1450                        .stored_review_comments
1451                        .iter()
1452                        .flat_map(|(_, comments)| comments)
1453                        .find(|c| c.id == comment_id)
1454                        .map(|c| c.comment.clone())
1455                        .unwrap_or_default();
1456
1457                    // Create inline editor
1458                    let parent_editor = cx.entity().downgrade();
1459                    let inline_editor = cx.new(|cx| {
1460                        let mut editor = Editor::single_line(window, cx);
1461                        editor.set_text(&*comment_text, window, cx);
1462                        // Select all text for easy replacement
1463                        editor.select_all(&crate::actions::SelectAll, window, cx);
1464                        editor
1465                    });
1466
1467                    // Register the Newline action to confirm the edit
1468                    let subscription = inline_editor.update(cx, |inline_editor, _cx| {
1469                        inline_editor.register_action({
1470                            let parent_editor = parent_editor.clone();
1471                            move |_: &crate::actions::Newline, window, cx| {
1472                                if let Some(editor) = parent_editor.upgrade() {
1473                                    editor.update(cx, |editor, cx| {
1474                                        editor.confirm_edit_review_comment(comment_id, window, cx);
1475                                    });
1476                                }
1477                            }
1478                        })
1479                    });
1480
1481                    // Store the subscription to keep the action handler alive
1482                    overlay
1483                        .inline_edit_subscriptions
1484                        .insert(comment_id, subscription);
1485
1486                    // Focus the inline editor
1487                    let focus_handle = inline_editor.focus_handle(cx);
1488                    window.focus(&focus_handle, cx);
1489
1490                    entry.insert(inline_editor);
1491                }
1492            }
1493        }
1494
1495        cx.notify();
1496    }
1497
1498    /// Confirms an inline edit of a review comment.
1499    pub(super) fn confirm_edit_review_comment(
1500        &mut self,
1501        comment_id: usize,
1502        _window: &mut Window,
1503        cx: &mut Context<Self>,
1504    ) {
1505        // Get the new text from the inline editor
1506        // Find the overlay containing this comment's inline editor
1507        let snapshot = self.buffer.read(cx).snapshot(cx);
1508        let hunk_key = self
1509            .stored_review_comments
1510            .iter()
1511            .find_map(|(key, comments)| {
1512                if comments.iter().any(|c| c.id == comment_id) {
1513                    Some(key.clone())
1514                } else {
1515                    None
1516                }
1517            });
1518
1519        let new_text = hunk_key
1520            .as_ref()
1521            .and_then(|hunk_key| {
1522                self.diff_review_overlays
1523                    .iter()
1524                    .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot))
1525            })
1526            .as_ref()
1527            .and_then(|overlay| overlay.inline_edit_editors.get(&comment_id))
1528            .map(|editor| editor.read(cx).text(cx).trim().to_string());
1529
1530        if let Some(new_text) = new_text {
1531            if !new_text.is_empty() {
1532                self.update_review_comment(comment_id, new_text, cx);
1533            }
1534        }
1535
1536        // Remove the inline editor and its subscription
1537        if let Some(hunk_key) = hunk_key {
1538            if let Some(overlay) = self
1539                .diff_review_overlays
1540                .iter_mut()
1541                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
1542            {
1543                overlay.inline_edit_editors.remove(&comment_id);
1544                overlay.inline_edit_subscriptions.remove(&comment_id);
1545            }
1546        }
1547
1548        // Clear editing state
1549        self.set_comment_editing(comment_id, false, cx);
1550    }
1551
1552    /// Cancels an inline edit of a review comment.
1553    pub(super) fn cancel_edit_review_comment(
1554        &mut self,
1555        comment_id: usize,
1556        _window: &mut Window,
1557        cx: &mut Context<Self>,
1558    ) {
1559        // Find which hunk this comment belongs to
1560        let hunk_key = self
1561            .stored_review_comments
1562            .iter()
1563            .find_map(|(key, comments)| {
1564                if comments.iter().any(|c| c.id == comment_id) {
1565                    Some(key.clone())
1566                } else {
1567                    None
1568                }
1569            });
1570
1571        // Remove the inline editor and its subscription
1572        if let Some(hunk_key) = hunk_key {
1573            let snapshot = self.buffer.read(cx).snapshot(cx);
1574            if let Some(overlay) = self
1575                .diff_review_overlays
1576                .iter_mut()
1577                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, &hunk_key, &snapshot))
1578            {
1579                overlay.inline_edit_editors.remove(&comment_id);
1580                overlay.inline_edit_subscriptions.remove(&comment_id);
1581            }
1582        }
1583
1584        // Clear editing state
1585        self.set_comment_editing(comment_id, false, cx);
1586    }
1587
1588    /// Action handler for ConfirmEditReviewComment.
1589    pub(super) fn confirm_edit_review_comment_action(
1590        &mut self,
1591        action: &ConfirmEditReviewComment,
1592        window: &mut Window,
1593        cx: &mut Context<Self>,
1594    ) {
1595        self.confirm_edit_review_comment(action.id, window, cx);
1596    }
1597
1598    /// Action handler for CancelEditReviewComment.
1599    pub(super) fn cancel_edit_review_comment_action(
1600        &mut self,
1601        action: &CancelEditReviewComment,
1602        window: &mut Window,
1603        cx: &mut Context<Self>,
1604    ) {
1605        self.cancel_edit_review_comment(action.id, window, cx);
1606    }
1607
1608    /// Handles the DeleteReviewComment action - removes a comment.
1609    pub(super) fn delete_review_comment(
1610        &mut self,
1611        action: &DeleteReviewComment,
1612        window: &mut Window,
1613        cx: &mut Context<Self>,
1614    ) {
1615        // Get the hunk key before removing the comment
1616        // Find the hunk key from the comment itself
1617        let comment_id = action.id;
1618        let hunk_key = self
1619            .stored_review_comments
1620            .iter()
1621            .find_map(|(key, comments)| {
1622                if comments.iter().any(|c| c.id == comment_id) {
1623                    Some(key.clone())
1624                } else {
1625                    None
1626                }
1627            });
1628
1629        // Also get it from the overlay for refresh purposes
1630        let overlay_hunk_key = self
1631            .diff_review_overlays
1632            .first()
1633            .map(|o| o.hunk_key.clone());
1634
1635        self.remove_review_comment(action.id, cx);
1636
1637        // Refresh the overlay height after removing a comment
1638        if let Some(hunk_key) = hunk_key.or(overlay_hunk_key) {
1639            self.refresh_diff_review_overlay_height(&hunk_key, window, cx);
1640        }
1641    }
1642
1643    pub(super) fn copy_permalink_to_line(
1644        &mut self,
1645        _: &CopyPermalinkToLine,
1646        window: &mut Window,
1647        cx: &mut Context<Self>,
1648    ) {
1649        let permalink_task = self.get_permalink_to_line(cx);
1650        let workspace = self.workspace();
1651
1652        cx.spawn_in(window, async move |_, cx| match permalink_task.await {
1653            Ok(permalink) => {
1654                cx.update(|_, cx| {
1655                    cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
1656                })
1657                .ok();
1658            }
1659            Err(err) => {
1660                let message = format!("Failed to copy permalink: {err}");
1661
1662                anyhow::Result::<()>::Err(err).log_err();
1663
1664                if let Some(workspace) = workspace {
1665                    workspace
1666                        .update_in(cx, |workspace, _, cx| {
1667                            struct CopyPermalinkToLine;
1668
1669                            workspace.show_toast(
1670                                Toast::new(
1671                                    NotificationId::unique::<CopyPermalinkToLine>(),
1672                                    message,
1673                                ),
1674                                cx,
1675                            )
1676                        })
1677                        .ok();
1678                }
1679            }
1680        })
1681        .detach();
1682    }
1683
1684    pub(super) fn open_permalink_to_line(
1685        &mut self,
1686        _: &OpenPermalinkToLine,
1687        window: &mut Window,
1688        cx: &mut Context<Self>,
1689    ) {
1690        let permalink_task = self.get_permalink_to_line(cx);
1691        let workspace = self.workspace();
1692
1693        cx.spawn_in(window, async move |_, cx| match permalink_task.await {
1694            Ok(permalink) => {
1695                cx.update(|_, cx| {
1696                    cx.open_url(permalink.as_ref());
1697                })
1698                .ok();
1699            }
1700            Err(err) => {
1701                let message = format!("Failed to open permalink: {err}");
1702
1703                anyhow::Result::<()>::Err(err).log_err();
1704
1705                if let Some(workspace) = workspace {
1706                    workspace.update(cx, |workspace, cx| {
1707                        struct OpenPermalinkToLine;
1708
1709                        workspace.show_toast(
1710                            Toast::new(NotificationId::unique::<OpenPermalinkToLine>(), message),
1711                            cx,
1712                        )
1713                    });
1714                }
1715            }
1716        })
1717        .detach();
1718    }
1719
1720    pub(super) fn toggle_staged_selected_diff_hunks(
1721        &mut self,
1722        _: &::git::ToggleStaged,
1723        window: &mut Window,
1724        cx: &mut Context<Self>,
1725    ) {
1726        let ranges: Vec<_> = self
1727            .selections
1728            .disjoint_anchors()
1729            .iter()
1730            .map(|s| s.range())
1731            .collect();
1732        let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
1733        cx.spawn_in(window, async move |this, cx| {
1734            task.await?;
1735            this.update_in(cx, |this, window, cx| {
1736                let snapshot = this.buffer.read(cx).snapshot(cx);
1737                let hunks = this.diff_hunks_in_ranges(&ranges, &snapshot).collect();
1738                this.apply_toggle(hunks, window, cx);
1739            })
1740        })
1741        .detach_and_log_err(cx);
1742    }
1743
1744    pub(super) fn stage_and_next(
1745        &mut self,
1746        _: &::git::StageAndNext,
1747        window: &mut Window,
1748        cx: &mut Context<Self>,
1749    ) {
1750        self.do_stage_or_unstage_and_next(true, window, cx);
1751    }
1752
1753    pub(super) fn unstage_and_next(
1754        &mut self,
1755        _: &::git::UnstageAndNext,
1756        window: &mut Window,
1757        cx: &mut Context<Self>,
1758    ) {
1759        self.do_stage_or_unstage_and_next(false, window, cx);
1760    }
1761
1762    pub fn apply_toggle(
1763        &mut self,
1764        hunks: Vec<MultiBufferDiffHunk>,
1765        window: &mut Window,
1766        cx: &mut Context<Self>,
1767    ) {
1768        let hunks = self.resolve_diff_hunks(hunks, cx);
1769        if hunks.is_empty() {
1770            return;
1771        }
1772        let delegate = self.diff_hunk_delegate();
1773        delegate.toggle(hunks, self, window, cx);
1774    }
1775
1776    pub fn apply_stage_or_unstage(
1777        &mut self,
1778        stage: bool,
1779        hunks: Vec<MultiBufferDiffHunk>,
1780        window: &mut Window,
1781        cx: &mut Context<Self>,
1782    ) {
1783        let hunks = self.resolve_diff_hunks(hunks, cx);
1784        if hunks.is_empty() {
1785            return;
1786        }
1787        let delegate = self.diff_hunk_delegate();
1788        delegate.stage_or_unstage(stage, hunks, self, window, cx);
1789    }
1790
1791    pub fn apply_restore(
1792        &mut self,
1793        hunks: Vec<MultiBufferDiffHunk>,
1794        window: &mut Window,
1795        cx: &mut Context<Self>,
1796    ) {
1797        let hunks = self.resolve_diff_hunks(hunks, cx);
1798        if hunks.is_empty() {
1799            return;
1800        }
1801        let delegate = self.diff_hunk_delegate();
1802        delegate.restore(hunks, self, window, cx);
1803    }
1804
1805    pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut Context<Self>) -> bool {
1806        self.buffer.update(cx, |buffer, cx| {
1807            let ranges = vec![Anchor::Min..Anchor::Max];
1808            if !buffer.all_diff_hunks_expanded()
1809                && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx)
1810            {
1811                buffer.collapse_diff_hunks(ranges, cx);
1812                true
1813            } else {
1814                false
1815            }
1816        })
1817    }
1818
1819    pub(super) fn has_any_expanded_diff_hunks(&self, cx: &App) -> bool {
1820        if self.buffer.read(cx).all_diff_hunks_expanded() {
1821            return true;
1822        }
1823        let ranges = vec![Anchor::Min..Anchor::Max];
1824        self.buffer
1825            .read(cx)
1826            .has_expanded_diff_hunks_in_ranges(&ranges, cx)
1827    }
1828
1829    pub(super) fn toggle_single_diff_hunk(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
1830        self.buffer.update(cx, |buffer, cx| {
1831            buffer.toggle_single_diff_hunk(range, cx);
1832        })
1833    }
1834
1835    pub(super) fn apply_all_diff_hunks(
1836        &mut self,
1837        _: &ApplyAllDiffHunks,
1838        window: &mut Window,
1839        cx: &mut Context<Self>,
1840    ) {
1841        if self.read_only(cx) {
1842            return;
1843        }
1844
1845        let buffers = self.buffer.read(cx).all_buffers();
1846        for branch_buffer in buffers {
1847            branch_buffer.update(cx, |branch_buffer, cx| {
1848                branch_buffer.merge_into_base(Vec::new(), cx);
1849            });
1850        }
1851
1852        if let Some(project) = self.project.clone() {
1853            self.save(
1854                SaveOptions {
1855                    format: true,
1856                    force_format: false,
1857                    autosave: false,
1858                },
1859                project,
1860                window,
1861                cx,
1862            )
1863            .detach_and_log_err(cx);
1864        }
1865    }
1866
1867    pub(super) fn apply_selected_diff_hunks(
1868        &mut self,
1869        _: &ApplyDiffHunk,
1870        window: &mut Window,
1871        cx: &mut Context<Self>,
1872    ) {
1873        if self.read_only(cx) {
1874            return;
1875        }
1876        let snapshot = self.snapshot(window, cx);
1877        let hunks = snapshot.hunks_for_ranges(
1878            self.selections
1879                .all(&snapshot.display_snapshot)
1880                .into_iter()
1881                .map(|selection| selection.range()),
1882        );
1883        let mut ranges_by_buffer = HashMap::default();
1884        self.transact(window, cx, |editor, _window, cx| {
1885            for hunk in hunks {
1886                if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
1887                    ranges_by_buffer
1888                        .entry(buffer.clone())
1889                        .or_insert_with(Vec::new)
1890                        .push(hunk.buffer_range.to_offset(buffer.read(cx)));
1891                }
1892            }
1893
1894            for (buffer, ranges) in ranges_by_buffer {
1895                buffer.update(cx, |buffer, cx| {
1896                    buffer.merge_into_base(ranges, cx);
1897                });
1898            }
1899        });
1900
1901        if let Some(project) = self.project.clone() {
1902            self.save(
1903                SaveOptions {
1904                    format: true,
1905                    force_format: false,
1906                    autosave: false,
1907                },
1908                project,
1909                window,
1910                cx,
1911            )
1912            .detach_and_log_err(cx);
1913        }
1914    }
1915
1916    pub(super) fn open_git_blame_commit(
1917        &mut self,
1918        _: &OpenGitBlameCommit,
1919        window: &mut Window,
1920        cx: &mut Context<Self>,
1921    ) {
1922        self.open_git_blame_commit_internal(window, cx);
1923    }
1924
1925    pub(super) fn toggle_git_blame_inline_internal(
1926        &mut self,
1927        user_triggered: bool,
1928        window: &mut Window,
1929        cx: &mut Context<Self>,
1930    ) {
1931        if self.git_blame_inline_enabled {
1932            self.git_blame_inline_enabled = false;
1933            self.show_git_blame_inline = false;
1934            self.show_git_blame_inline_delay_task.take();
1935        } else {
1936            self.git_blame_inline_enabled = true;
1937            self.start_git_blame_inline(user_triggered, window, cx);
1938        }
1939
1940        cx.notify();
1941    }
1942
1943    pub(super) fn start_git_blame_inline(
1944        &mut self,
1945        user_triggered: bool,
1946        window: &mut Window,
1947        cx: &mut Context<Self>,
1948    ) {
1949        self.start_git_blame(user_triggered, window, cx);
1950
1951        if ProjectSettings::get_global(cx)
1952            .git
1953            .inline_blame_delay()
1954            .is_some()
1955        {
1956            self.start_inline_blame_timer(window, cx);
1957        } else {
1958            self.show_git_blame_inline = true
1959        }
1960    }
1961
1962    pub(super) fn render_git_blame_gutter(&self, cx: &App) -> bool {
1963        !self.mode().is_minimap() && self.show_git_blame_gutter && self.has_blame_entries(cx)
1964    }
1965
1966    pub(super) fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool {
1967        ProjectSettings::get_global(cx).git.inline_blame.location
1968            == project::project_settings::InlineBlameLocation::Inline
1969            && self.show_git_blame_inline
1970            && (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some())
1971            && !self.newest_selection_head_on_empty_line(cx)
1972            && self.has_blame_entries(cx)
1973    }
1974
1975    pub(super) fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1976        if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() {
1977            self.show_git_blame_inline = false;
1978
1979            self.show_git_blame_inline_delay_task =
1980                Some(cx.spawn_in(window, async move |this, cx| {
1981                    cx.background_executor().timer(delay).await;
1982
1983                    this.update(cx, |this, cx| {
1984                        this.show_git_blame_inline = true;
1985                        cx.notify();
1986                    })
1987                    .log_err();
1988                }));
1989        }
1990    }
1991
1992    pub(super) fn show_blame_popover(
1993        &mut self,
1994        buffer: BufferId,
1995        blame_entry: &BlameEntry,
1996        position: gpui::Point<Pixels>,
1997        ignore_timeout: bool,
1998        cx: &mut Context<Self>,
1999    ) {
2000        if let Some(state) = &mut self.inline_blame_popover {
2001            state.hide_task.take();
2002        } else {
2003            let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0;
2004            let blame_entry = blame_entry.clone();
2005            let show_task = cx.spawn(async move |editor, cx| {
2006                if !ignore_timeout {
2007                    cx.background_executor()
2008                        .timer(std::time::Duration::from_millis(blame_popover_delay))
2009                        .await;
2010                }
2011                editor
2012                    .update(cx, |editor, cx| {
2013                        editor.inline_blame_popover_show_task.take();
2014                        let Some(blame) = editor.blame.as_ref() else {
2015                            return;
2016                        };
2017                        let blame = blame.read(cx);
2018                        let details = blame.details_for_entry(buffer, &blame_entry);
2019                        let markdown = cx.new(|cx| {
2020                            Markdown::new(
2021                                details
2022                                    .as_ref()
2023                                    .map(|message| message.message.clone())
2024                                    .unwrap_or_default(),
2025                                None,
2026                                None,
2027                                cx,
2028                            )
2029                        });
2030                        editor.inline_blame_popover = Some(InlineBlamePopover {
2031                            position,
2032                            hide_task: None,
2033                            popover_bounds: None,
2034                            popover_state: InlineBlamePopoverState {
2035                                scroll_handle: ScrollHandle::new(),
2036                                commit_message: details,
2037                                markdown,
2038                            },
2039                            keyboard_grace: ignore_timeout,
2040                        });
2041                        cx.notify();
2042                    })
2043                    .ok();
2044            });
2045            self.inline_blame_popover_show_task = Some(show_task);
2046        }
2047    }
2048
2049    pub(super) fn go_to_prev_hunk(
2050        &mut self,
2051        _: &GoToPreviousHunk,
2052        window: &mut Window,
2053        cx: &mut Context<Self>,
2054    ) {
2055        let snapshot = self.snapshot(window, cx);
2056        let selection = self.selections.newest::<Point>(&snapshot.display_snapshot);
2057        self.go_to_hunk_before_or_after_position(
2058            &snapshot,
2059            selection.head(),
2060            Direction::Prev,
2061            true,
2062            window,
2063            cx,
2064        );
2065    }
2066
2067    /// Calculates the appropriate block height for the diff review overlay.
2068    /// Height is in lines: 2 for input row, 1 for header when comments exist,
2069    /// and 2 lines per comment when expanded.
2070    pub(super) fn calculate_overlay_height(
2071        &self,
2072        hunk_key: &DiffHunkKey,
2073        comments_expanded: bool,
2074        snapshot: &MultiBufferSnapshot,
2075    ) -> u32 {
2076        let comment_count = self.hunk_comment_count(hunk_key, snapshot);
2077        let base_height: u32 = 2; // Input row with avatar and buttons
2078
2079        if comment_count == 0 {
2080            base_height
2081        } else if comments_expanded {
2082            // Header (1 line) + 2 lines per comment
2083            base_height + 1 + (comment_count as u32 * 2)
2084        } else {
2085            // Just header when collapsed
2086            base_height + 1
2087        }
2088    }
2089
2090    pub fn stage_or_unstage_diff_hunks(
2091        &mut self,
2092        stage: bool,
2093        ranges: Vec<Range<Anchor>>,
2094        window: &mut Window,
2095        cx: &mut Context<Self>,
2096    ) {
2097        let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
2098        cx.spawn_in(window, async move |this, cx| {
2099            task.await?;
2100            this.update_in(cx, |this, window, cx| {
2101                let snapshot = this.buffer.read(cx).snapshot(cx);
2102                let hunks = this.diff_hunks_in_ranges(&ranges, &snapshot).collect();
2103                this.apply_stage_or_unstage(stage, hunks, window, cx);
2104            })
2105        })
2106        .detach_and_log_err(cx);
2107    }
2108
2109    pub fn restore_diff_hunks_in_ranges(
2110        &mut self,
2111        ranges: Vec<Range<Anchor>>,
2112        window: &mut Window,
2113        cx: &mut Context<Self>,
2114    ) {
2115        let snapshot = self.buffer.read(cx).snapshot(cx);
2116        let hunks = self.diff_hunks_in_ranges(&ranges, &snapshot).collect();
2117        self.apply_restore(hunks, window, cx);
2118    }
2119
2120    fn toggle_diff_hunks_in_ranges(
2121        &mut self,
2122        ranges: Vec<Range<Anchor>>,
2123        cx: &mut Context<Editor>,
2124    ) {
2125        self.buffer.update(cx, |buffer, cx| {
2126            let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx);
2127            buffer.expand_or_collapse_diff_hunks(ranges, expand, cx);
2128        })
2129    }
2130
2131    fn start_git_blame(
2132        &mut self,
2133        user_triggered: bool,
2134        window: &mut Window,
2135        cx: &mut Context<Self>,
2136    ) {
2137        if let Some(project) = self.project() {
2138            if let Some(buffer) = self.buffer().read(cx).as_singleton()
2139                && buffer.read(cx).file().is_none()
2140            {
2141                return;
2142            }
2143
2144            let focused = self.focus_handle(cx).contains_focused(window, cx);
2145
2146            let project = project.clone();
2147            let blame = cx
2148                .new(|cx| GitBlame::new(self.buffer.clone(), project, user_triggered, focused, cx));
2149            self.blame_subscription =
2150                Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify()));
2151            self.blame = Some(blame);
2152        }
2153    }
2154
2155    fn restore_hunks_in_ranges(
2156        &mut self,
2157        ranges: Vec<Range<Point>>,
2158        window: &mut Window,
2159        cx: &mut Context<Editor>,
2160    ) {
2161        let hunks = self.snapshot(window, cx).hunks_for_ranges(ranges);
2162        self.apply_restore(hunks, window, cx);
2163    }
2164
2165    fn save_buffers_for_ranges_if_needed(
2166        &mut self,
2167        ranges: &[Range<Anchor>],
2168        cx: &mut Context<Editor>,
2169    ) -> Task<Result<()>> {
2170        let multibuffer = self.buffer.read(cx);
2171        let snapshot = multibuffer.read(cx);
2172        let buffer_ids: HashSet<_> = ranges
2173            .iter()
2174            .flat_map(|range| snapshot.buffer_ids_for_range(range.clone()))
2175            .collect();
2176        drop(snapshot);
2177
2178        let mut buffers = HashSet::default();
2179        for buffer_id in buffer_ids {
2180            if let Some(buffer_entity) = multibuffer.buffer(buffer_id) {
2181                let buffer = buffer_entity.read(cx);
2182                if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty()
2183                {
2184                    buffers.insert(buffer_entity);
2185                }
2186            }
2187        }
2188
2189        if let Some(project) = &self.project {
2190            project.update(cx, |project, cx| project.save_buffers(buffers, cx))
2191        } else {
2192            Task::ready(Ok(()))
2193        }
2194    }
2195
2196    fn do_stage_or_unstage_and_next(
2197        &mut self,
2198        stage: bool,
2199        window: &mut Window,
2200        cx: &mut Context<Self>,
2201    ) {
2202        let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
2203
2204        if ranges.iter().any(|range| range.start != range.end) {
2205            self.stage_or_unstage_diff_hunks(stage, ranges, window, cx);
2206            return;
2207        }
2208
2209        self.stage_or_unstage_diff_hunks(stage, ranges, window, cx);
2210
2211        let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
2212        let wrap_around = !all_diff_hunks_expanded;
2213        let snapshot = self.snapshot(window, cx);
2214        let position = self
2215            .selections
2216            .newest::<Point>(&snapshot.display_snapshot)
2217            .head();
2218
2219        self.go_to_hunk_before_or_after_position(
2220            &snapshot,
2221            position,
2222            Direction::Next,
2223            wrap_around,
2224            window,
2225            cx,
2226        );
2227    }
2228
2229    fn open_git_blame_commit_internal(
2230        &mut self,
2231        window: &mut Window,
2232        cx: &mut Context<Self>,
2233    ) -> Option<()> {
2234        let blame = self.blame.as_ref()?;
2235        let snapshot = self.snapshot(window, cx);
2236        let cursor = self
2237            .selections
2238            .newest::<Point>(&snapshot.display_snapshot)
2239            .head();
2240        let (buffer, point) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)?;
2241        let (_, blame_entry) = blame
2242            .update(cx, |blame, cx| {
2243                blame
2244                    .blame_for_rows(
2245                        &[RowInfo {
2246                            buffer_id: Some(buffer.remote_id()),
2247                            buffer_row: Some(point.row),
2248                            ..Default::default()
2249                        }],
2250                        cx,
2251                    )
2252                    .next()
2253            })
2254            .flatten()?;
2255        let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
2256        let repo = blame.read(cx).repository(cx, buffer.remote_id())?;
2257        let workspace = self.workspace()?.downgrade();
2258        renderer.open_blame_commit(blame_entry, repo, workspace, window, cx);
2259        None
2260    }
2261
2262    fn has_blame_entries(&self, cx: &App) -> bool {
2263        self.blame()
2264            .is_some_and(|blame| blame.read(cx).has_generated_entries())
2265    }
2266
2267    fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool {
2268        let cursor_anchor = self.selections.newest_anchor().head();
2269
2270        let snapshot = self.buffer.read(cx).snapshot(cx);
2271        let buffer_row = MultiBufferRow(cursor_anchor.to_point(&snapshot).row);
2272
2273        snapshot.line_len(buffer_row) == 0
2274    }
2275    fn hunk_after_position(
2276        &mut self,
2277        snapshot: &EditorSnapshot,
2278        position: Point,
2279        wrap_around: bool,
2280    ) -> Option<MultiBufferDiffHunk> {
2281        let result = snapshot
2282            .buffer_snapshot()
2283            .diff_hunks_in_range(position..snapshot.buffer_snapshot().max_point())
2284            .find(|hunk| hunk.row_range.start.0 > position.row);
2285
2286        if wrap_around {
2287            result.or_else(|| {
2288                snapshot
2289                    .buffer_snapshot()
2290                    .diff_hunks_in_range(Point::zero()..position)
2291                    .find(|hunk| hunk.row_range.end.0 < position.row)
2292            })
2293        } else {
2294            result
2295        }
2296    }
2297
2298    fn hunk_before_position(
2299        &mut self,
2300        snapshot: &EditorSnapshot,
2301        position: Point,
2302        wrap_around: bool,
2303    ) -> Option<MultiBufferRow> {
2304        let result = snapshot.buffer_snapshot().diff_hunk_before(position);
2305
2306        if wrap_around {
2307            result.or_else(|| snapshot.buffer_snapshot().diff_hunk_before(Point::MAX))
2308        } else {
2309            result
2310        }
2311    }
2312
2313    /// Dismisses overlays that have no comments stored for their hunks.
2314    /// Keeps overlays that have at least one comment.
2315    fn dismiss_overlays_without_comments(&mut self, cx: &mut Context<Self>) {
2316        let snapshot = self.buffer.read(cx).snapshot(cx);
2317
2318        // First, compute which overlays have comments (to avoid borrow issues with retain)
2319        let overlays_with_comments: Vec<bool> = self
2320            .diff_review_overlays
2321            .iter()
2322            .map(|overlay| self.hunk_comment_count(&overlay.hunk_key, &snapshot) > 0)
2323            .collect();
2324
2325        // Now collect block IDs to remove and retain overlays
2326        let mut block_ids_to_remove = HashSet::default();
2327        let mut index = 0;
2328        self.diff_review_overlays.retain(|overlay| {
2329            let has_comments = overlays_with_comments[index];
2330            index += 1;
2331            if !has_comments {
2332                block_ids_to_remove.insert(overlay.block_id);
2333            }
2334            has_comments
2335        });
2336
2337        if !block_ids_to_remove.is_empty() {
2338            self.remove_blocks(block_ids_to_remove, None, cx);
2339            cx.notify();
2340        }
2341    }
2342
2343    /// Refreshes the diff review overlay block to update its height and render function.
2344    /// Uses resize_blocks and replace_blocks to avoid visual flicker from remove+insert.
2345    fn refresh_diff_review_overlay_height(
2346        &mut self,
2347        hunk_key: &DiffHunkKey,
2348        _window: &mut Window,
2349        cx: &mut Context<Self>,
2350    ) {
2351        // Extract all needed data from overlay first to avoid borrow conflicts
2352        let snapshot = self.buffer.read(cx).snapshot(cx);
2353        let (comments_expanded, block_id, prompt_editor) = {
2354            let Some(overlay) = self
2355                .diff_review_overlays
2356                .iter()
2357                .find(|overlay| Self::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot))
2358            else {
2359                return;
2360            };
2361
2362            (
2363                overlay.comments_expanded,
2364                overlay.block_id,
2365                overlay.prompt_editor.clone(),
2366            )
2367        };
2368
2369        // Calculate new height
2370        let snapshot = self.buffer.read(cx).snapshot(cx);
2371        let new_height = self.calculate_overlay_height(hunk_key, comments_expanded, &snapshot);
2372
2373        // Update the block height using resize_blocks (avoids flicker)
2374        let mut heights = HashMap::default();
2375        heights.insert(block_id, new_height);
2376        self.resize_blocks(heights, None, cx);
2377
2378        // Update the render function using replace_blocks (avoids flicker)
2379        let hunk_key_for_render = hunk_key.clone();
2380        let editor_handle = cx.entity().downgrade();
2381        let render: Arc<dyn Fn(&mut BlockContext) -> AnyElement + Send + Sync> =
2382            Arc::new(move |cx| {
2383                Self::render_diff_review_overlay(
2384                    &prompt_editor,
2385                    &hunk_key_for_render,
2386                    &editor_handle,
2387                    cx,
2388                )
2389            });
2390
2391        let mut renderers = HashMap::default();
2392        renderers.insert(block_id, render);
2393        self.replace_blocks(renderers, None, cx);
2394    }
2395
2396    /// Compares two DiffHunkKeys for equality by resolving their anchors.
2397    fn hunk_keys_match(a: &DiffHunkKey, b: &DiffHunkKey, snapshot: &MultiBufferSnapshot) -> bool {
2398        a.file_path == b.file_path
2399            && a.hunk_start_anchor.to_point(snapshot) == b.hunk_start_anchor.to_point(snapshot)
2400    }
2401
2402    fn render_diff_review_overlay(
2403        prompt_editor: &Entity<Editor>,
2404        hunk_key: &DiffHunkKey,
2405        editor_handle: &WeakEntity<Editor>,
2406        cx: &mut BlockContext,
2407    ) -> AnyElement {
2408        fn format_line_ranges(ranges: &[(u32, u32)]) -> Option<String> {
2409            if ranges.is_empty() {
2410                return None;
2411            }
2412            let formatted: Vec<String> = ranges
2413                .iter()
2414                .map(|(start, end)| {
2415                    let start_line = start + 1;
2416                    let end_line = end + 1;
2417                    if start_line == end_line {
2418                        format!("Line {start_line}")
2419                    } else {
2420                        format!("Lines {start_line}-{end_line}")
2421                    }
2422                })
2423                .collect();
2424            // Don't show label for single line in single excerpt
2425            if ranges.len() == 1 && ranges[0].0 == ranges[0].1 {
2426                return None;
2427            }
2428            Some(formatted.join(" ⋯ "))
2429        }
2430
2431        let theme = cx.theme();
2432        let colors = theme.colors();
2433
2434        let (comments, comments_expanded, inline_editors, user_avatar_uri, line_ranges) =
2435            editor_handle
2436                .upgrade()
2437                .map(|editor| {
2438                    let editor = editor.read(cx);
2439                    let snapshot = editor.buffer().read(cx).snapshot(cx);
2440                    let comments = editor.comments_for_hunk(hunk_key, &snapshot).to_vec();
2441                    let (expanded, editors, avatar_uri, line_ranges) = editor
2442                        .diff_review_overlays
2443                        .iter()
2444                        .find(|overlay| {
2445                            Editor::hunk_keys_match(&overlay.hunk_key, hunk_key, &snapshot)
2446                        })
2447                        .map(|o| {
2448                            let start_point = o.anchor_range.start.to_point(&snapshot);
2449                            let end_point = o.anchor_range.end.to_point(&snapshot);
2450                            // Get line ranges per excerpt to detect discontinuities
2451                            let buffer_ranges =
2452                                snapshot.range_to_buffer_ranges(start_point..end_point);
2453                            let ranges: Vec<(u32, u32)> = buffer_ranges
2454                                .iter()
2455                                .map(|(buffer_snapshot, range, _)| {
2456                                    let start = buffer_snapshot.offset_to_point(range.start.0).row;
2457                                    let end = buffer_snapshot.offset_to_point(range.end.0).row;
2458                                    (start, end)
2459                                })
2460                                .collect();
2461                            (
2462                                o.comments_expanded,
2463                                o.inline_edit_editors.clone(),
2464                                o.user_avatar_uri.clone(),
2465                                if ranges.is_empty() {
2466                                    None
2467                                } else {
2468                                    Some(ranges)
2469                                },
2470                            )
2471                        })
2472                        .unwrap_or((true, HashMap::default(), None, None));
2473                    (comments, expanded, editors, avatar_uri, line_ranges)
2474                })
2475                .unwrap_or((Vec::new(), true, HashMap::default(), None, None));
2476
2477        let comment_count = comments.len();
2478        let avatar_size = px(20.);
2479        let action_icon_size = IconSize::XSmall;
2480
2481        v_flex()
2482            .w_full()
2483            .bg(colors.editor_background)
2484            .border_b_1()
2485            .border_color(colors.border)
2486            .px_2()
2487            .pb_2()
2488            .gap_2()
2489            // Line range indicator (only shown for multi-line selections or multiple excerpts)
2490            .when_some(line_ranges, |el, ranges| {
2491                let label = format_line_ranges(&ranges);
2492                if let Some(label) = label {
2493                    el.child(
2494                        h_flex()
2495                            .w_full()
2496                            .px_2()
2497                            .child(Label::new(label).size(LabelSize::Small).color(Color::Muted)),
2498                    )
2499                } else {
2500                    el
2501                }
2502            })
2503            // Top row: editable input with user's avatar
2504            .child(
2505                h_flex()
2506                    .w_full()
2507                    .items_center()
2508                    .gap_2()
2509                    .px_2()
2510                    .py_1p5()
2511                    .rounded_md()
2512                    .bg(colors.surface_background)
2513                    .child(
2514                        div()
2515                            .size(avatar_size)
2516                            .flex_shrink_0()
2517                            .rounded_full()
2518                            .overflow_hidden()
2519                            .child(if let Some(ref avatar_uri) = user_avatar_uri {
2520                                Avatar::new(avatar_uri.clone())
2521                                    .size(avatar_size)
2522                                    .into_any_element()
2523                            } else {
2524                                Icon::new(IconName::Person)
2525                                    .size(IconSize::Small)
2526                                    .color(ui::Color::Muted)
2527                                    .into_any_element()
2528                            }),
2529                    )
2530                    .child(
2531                        div()
2532                            .flex_1()
2533                            .border_1()
2534                            .border_color(colors.border)
2535                            .rounded_md()
2536                            .bg(colors.editor_background)
2537                            .px_2()
2538                            .py_1()
2539                            .child(prompt_editor.clone()),
2540                    )
2541                    .child(
2542                        h_flex()
2543                            .flex_shrink_0()
2544                            .gap_1()
2545                            .child(
2546                                IconButton::new("diff-review-close", IconName::Close)
2547                                    .icon_color(ui::Color::Muted)
2548                                    .icon_size(action_icon_size)
2549                                    .tooltip(Tooltip::text("Close"))
2550                                    .on_click(|_, window, cx| {
2551                                        window
2552                                            .dispatch_action(Box::new(crate::actions::Cancel), cx);
2553                                    }),
2554                            )
2555                            .child(
2556                                IconButton::new("diff-review-add", IconName::Return)
2557                                    .icon_color(ui::Color::Muted)
2558                                    .icon_size(action_icon_size)
2559                                    .tooltip(Tooltip::text("Add comment"))
2560                                    .on_click(|_, window, cx| {
2561                                        window.dispatch_action(
2562                                            Box::new(crate::actions::SubmitDiffReviewComment),
2563                                            cx,
2564                                        );
2565                                    }),
2566                            ),
2567                    ),
2568            )
2569            // Expandable comments section (only shown when there are comments)
2570            .when(comment_count > 0, |el| {
2571                el.child(Self::render_comments_section(
2572                    comments,
2573                    comments_expanded,
2574                    inline_editors,
2575                    user_avatar_uri,
2576                    avatar_size,
2577                    action_icon_size,
2578                    colors,
2579                ))
2580            })
2581            .into_any_element()
2582    }
2583
2584    fn render_comments_section(
2585        comments: Vec<StoredReviewComment>,
2586        expanded: bool,
2587        inline_editors: HashMap<usize, Entity<Editor>>,
2588        user_avatar_uri: Option<SharedUri>,
2589        avatar_size: Pixels,
2590        action_icon_size: IconSize,
2591        colors: &theme::ThemeColors,
2592    ) -> impl IntoElement {
2593        let comment_count = comments.len();
2594
2595        v_flex()
2596            .w_full()
2597            .gap_1()
2598            // Header with expand/collapse toggle
2599            .child(
2600                h_flex()
2601                    .id("review-comments-header")
2602                    .w_full()
2603                    .items_center()
2604                    .gap_1()
2605                    .px_2()
2606                    .py_1()
2607                    .cursor_pointer()
2608                    .rounded_md()
2609                    .hover(|style| style.bg(colors.ghost_element_hover))
2610                    .on_click(|_, window: &mut Window, cx| {
2611                        window.dispatch_action(
2612                            Box::new(crate::actions::ToggleReviewCommentsExpanded),
2613                            cx,
2614                        );
2615                    })
2616                    .child(
2617                        Icon::new(if expanded {
2618                            IconName::ChevronDown
2619                        } else {
2620                            IconName::ChevronRight
2621                        })
2622                        .size(IconSize::Small)
2623                        .color(ui::Color::Muted),
2624                    )
2625                    .child(
2626                        Label::new(format!(
2627                            "{} Comment{}",
2628                            comment_count,
2629                            if comment_count == 1 { "" } else { "s" }
2630                        ))
2631                        .size(LabelSize::Small)
2632                        .color(Color::Muted),
2633                    ),
2634            )
2635            // Comments list (when expanded)
2636            .when(expanded, |el| {
2637                el.children(comments.into_iter().map(|comment| {
2638                    let inline_editor = inline_editors.get(&comment.id).cloned();
2639                    Self::render_comment_row(
2640                        comment,
2641                        inline_editor,
2642                        user_avatar_uri.clone(),
2643                        avatar_size,
2644                        action_icon_size,
2645                        colors,
2646                    )
2647                }))
2648            })
2649    }
2650
2651    fn render_comment_row(
2652        comment: StoredReviewComment,
2653        inline_editor: Option<Entity<Editor>>,
2654        user_avatar_uri: Option<SharedUri>,
2655        avatar_size: Pixels,
2656        action_icon_size: IconSize,
2657        colors: &theme::ThemeColors,
2658    ) -> impl IntoElement {
2659        let comment_id = comment.id;
2660        let is_editing = inline_editor.is_some();
2661
2662        h_flex()
2663            .w_full()
2664            .items_center()
2665            .gap_2()
2666            .px_2()
2667            .py_1p5()
2668            .rounded_md()
2669            .bg(colors.surface_background)
2670            .child(
2671                div()
2672                    .size(avatar_size)
2673                    .flex_shrink_0()
2674                    .rounded_full()
2675                    .overflow_hidden()
2676                    .child(if let Some(ref avatar_uri) = user_avatar_uri {
2677                        Avatar::new(avatar_uri.clone())
2678                            .size(avatar_size)
2679                            .into_any_element()
2680                    } else {
2681                        Icon::new(IconName::Person)
2682                            .size(IconSize::Small)
2683                            .color(ui::Color::Muted)
2684                            .into_any_element()
2685                    }),
2686            )
2687            .child(if let Some(editor) = inline_editor {
2688                // Inline edit mode: show an editable text field
2689                div()
2690                    .flex_1()
2691                    .border_1()
2692                    .border_color(colors.border)
2693                    .rounded_md()
2694                    .bg(colors.editor_background)
2695                    .px_2()
2696                    .py_1()
2697                    .child(editor)
2698                    .into_any_element()
2699            } else {
2700                // Display mode: show the comment text
2701                div()
2702                    .flex_1()
2703                    .text_sm()
2704                    .text_color(colors.text)
2705                    .child(comment.comment)
2706                    .into_any_element()
2707            })
2708            .child(if is_editing {
2709                // Editing mode: show close and confirm buttons
2710                h_flex()
2711                    .gap_1()
2712                    .child(
2713                        IconButton::new(
2714                            format!("diff-review-cancel-edit-{comment_id}"),
2715                            IconName::Close,
2716                        )
2717                        .icon_color(ui::Color::Muted)
2718                        .icon_size(action_icon_size)
2719                        .tooltip(Tooltip::text("Cancel"))
2720                        .on_click(move |_, window, cx| {
2721                            window.dispatch_action(
2722                                Box::new(crate::actions::CancelEditReviewComment {
2723                                    id: comment_id,
2724                                }),
2725                                cx,
2726                            );
2727                        }),
2728                    )
2729                    .child(
2730                        IconButton::new(
2731                            format!("diff-review-confirm-edit-{comment_id}"),
2732                            IconName::Return,
2733                        )
2734                        .icon_color(ui::Color::Muted)
2735                        .icon_size(action_icon_size)
2736                        .tooltip(Tooltip::text("Confirm"))
2737                        .on_click(move |_, window, cx| {
2738                            window.dispatch_action(
2739                                Box::new(crate::actions::ConfirmEditReviewComment {
2740                                    id: comment_id,
2741                                }),
2742                                cx,
2743                            );
2744                        }),
2745                    )
2746                    .into_any_element()
2747            } else {
2748                // Display mode: no action buttons for now (edit/delete not yet implemented)
2749                gpui::Empty.into_any_element()
2750            })
2751    }
2752
2753    fn get_permalink_to_line(&self, cx: &mut Context<Self>) -> Task<Result<url::Url>> {
2754        let buffer_and_selection = maybe!({
2755            let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
2756            let selection_range = selection.range();
2757
2758            let multi_buffer = self.buffer().read(cx);
2759            let multi_buffer_snapshot = multi_buffer.snapshot(cx);
2760            let buffer_ranges = multi_buffer_snapshot
2761                .range_to_buffer_ranges(selection_range.start..selection_range.end);
2762
2763            let (buffer_snapshot, range, _) = if selection.reversed {
2764                buffer_ranges.first()
2765            } else {
2766                buffer_ranges.last()
2767            }?;
2768
2769            let buffer_range = range.to_point(buffer_snapshot);
2770            let buffer = multi_buffer.buffer(buffer_snapshot.remote_id())?;
2771
2772            let Some(buffer_diff) = multi_buffer.diff_for(buffer_snapshot.remote_id()) else {
2773                return Some((buffer, buffer_range.start.row..buffer_range.end.row));
2774            };
2775
2776            let buffer_diff_snapshot = buffer_diff.read(cx).snapshot(cx);
2777            let start = buffer_diff_snapshot
2778                .buffer_point_to_base_text_point(buffer_range.start, &buffer_snapshot);
2779            let end = buffer_diff_snapshot
2780                .buffer_point_to_base_text_point(buffer_range.end, &buffer_snapshot);
2781
2782            Some((buffer, start.row..end.row))
2783        });
2784
2785        let Some((buffer, selection)) = buffer_and_selection else {
2786            return Task::ready(Err(anyhow!("failed to determine buffer and selection")));
2787        };
2788
2789        let Some(project) = self.project() else {
2790            return Task::ready(Err(anyhow!("editor does not have project")));
2791        };
2792
2793        project.update(cx, |project, cx| {
2794            project.get_permalink_to_line(&buffer, selection, cx)
2795        })
2796    }
2797}
2798
2799#[cfg(test)]
2800impl Editor {
2801    /// Returns the line range for the first diff review overlay, if one is active.
2802    /// Returns (start_row, end_row) as physical line numbers in the underlying file.
2803    pub(super) fn diff_review_line_range(&self, cx: &App) -> Option<(u32, u32)> {
2804        let overlay = self.diff_review_overlays.first()?;
2805        let snapshot = self.buffer.read(cx).snapshot(cx);
2806        let start_point = overlay.anchor_range.start.to_point(&snapshot);
2807        let end_point = overlay.anchor_range.end.to_point(&snapshot);
2808        let start_row = snapshot
2809            .point_to_buffer_point(start_point)
2810            .map(|(_, p)| p.row)
2811            .unwrap_or(start_point.row);
2812        let end_row = snapshot
2813            .point_to_buffer_point(end_point)
2814            .map(|(_, p)| p.row)
2815            .unwrap_or(end_point.row);
2816        Some((start_row, end_row))
2817    }
2818
2819    /// Takes all stored comments from all hunks, clearing the storage.
2820    /// Returns a Vec of (hunk_key, comments) pairs.
2821    pub(super) fn take_all_review_comments(
2822        &mut self,
2823        cx: &mut Context<Self>,
2824    ) -> Vec<(DiffHunkKey, Vec<StoredReviewComment>)> {
2825        // Dismiss all overlays when taking comments (e.g., when sending to agent)
2826        self.dismiss_all_diff_review_overlays(cx);
2827        let comments = std::mem::take(&mut self.stored_review_comments);
2828        // Reset the ID counter since all comments have been taken
2829        self.next_review_comment_id = 0;
2830        cx.emit(EditorEvent::ReviewCommentsChanged { total_count: 0 });
2831        cx.notify();
2832        comments
2833    }
2834}
2835
2836impl EditorSnapshot {
2837    pub(super) fn display_diff_hunks_for_rows<'a>(
2838        &'a self,
2839        display_rows: Range<DisplayRow>,
2840        folded_buffers: &'a HashSet<BufferId>,
2841    ) -> impl 'a + Iterator<Item = DisplayDiffHunk> {
2842        let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(self);
2843        let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(self);
2844
2845        self.buffer_snapshot()
2846            .diff_hunks_in_range(buffer_start..buffer_end)
2847            .filter_map(|hunk| {
2848                if folded_buffers.contains(&hunk.buffer_id)
2849                    || (hunk.row_range.is_empty() && self.buffer.all_diff_hunks_expanded())
2850                {
2851                    return None;
2852                }
2853
2854                let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
2855                let hunk_end_point = if hunk.row_range.end > hunk.row_range.start {
2856                    let last_row = MultiBufferRow(hunk.row_range.end.0 - 1);
2857                    let line_len = self.buffer_snapshot().line_len(last_row);
2858                    Point::new(last_row.0, line_len)
2859                } else {
2860                    Point::new(hunk.row_range.end.0, 0)
2861                };
2862
2863                let hunk_display_start = self.point_to_display_point(hunk_start_point, Bias::Left);
2864                let hunk_display_end = self.point_to_display_point(hunk_end_point, Bias::Right);
2865
2866                let display_hunk = if hunk_display_start.column() != 0 {
2867                    DisplayDiffHunk::Folded {
2868                        display_row: hunk_display_start.row(),
2869                    }
2870                } else {
2871                    let mut end_row = hunk_display_end.row();
2872                    if hunk.row_range.end > hunk.row_range.start || hunk_display_end.column() > 0 {
2873                        end_row.0 += 1;
2874                    }
2875                    let is_created_file = hunk.is_created_file();
2876                    let multi_buffer_range = hunk.multi_buffer_range.clone();
2877
2878                    DisplayDiffHunk::Unfolded {
2879                        status: hunk.status(),
2880                        diff_base_byte_range: hunk.diff_base_byte_range.start.0
2881                            ..hunk.diff_base_byte_range.end.0,
2882                        word_diffs: hunk.word_diffs,
2883                        display_row_range: hunk_display_start.row()..end_row,
2884                        multi_buffer_range,
2885                        is_created_file,
2886                    }
2887                };
2888
2889                Some(display_hunk)
2890            })
2891    }
2892
2893    fn hunks_for_ranges(
2894        &self,
2895        ranges: impl IntoIterator<Item = Range<Point>>,
2896    ) -> Vec<MultiBufferDiffHunk> {
2897        let mut hunks = Vec::new();
2898        let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
2899            HashMap::default();
2900        for query_range in ranges {
2901            let query_rows =
2902                MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1);
2903            for hunk in self.buffer_snapshot().diff_hunks_in_range(
2904                Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0),
2905            ) {
2906                // Include deleted hunks that are adjacent to the query range, because
2907                // otherwise they would be missed.
2908                let mut intersects_range = hunk.row_range.overlaps(&query_rows);
2909                if hunk.status().is_deleted() {
2910                    intersects_range |= hunk.row_range.start == query_rows.end;
2911                    intersects_range |= hunk.row_range.end == query_rows.start;
2912                }
2913                if intersects_range {
2914                    if !processed_buffer_rows
2915                        .entry(hunk.buffer_id)
2916                        .or_default()
2917                        .insert(hunk.buffer_range.start..hunk.buffer_range.end)
2918                    {
2919                        continue;
2920                    }
2921                    hunks.push(hunk);
2922                }
2923            }
2924        }
2925
2926        hunks
2927    }
2928}
2929
2930pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) {
2931    cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));
2932}
2933
2934pub fn render_diff_hunk_controls(
2935    row: u32,
2936    status: &DiffHunkStatus,
2937    hunk_range: Range<Anchor>,
2938    is_created_file: bool,
2939    line_height: Pixels,
2940    editor: &Entity<Editor>,
2941    _window: &mut Window,
2942    cx: &mut App,
2943) -> AnyElement {
2944    let show_stage_restore = ProjectSettings::get_global(cx)
2945        .git
2946        .show_stage_restore_buttons;
2947
2948    h_flex()
2949        .h(line_height)
2950        .mr_1()
2951        .gap_1()
2952        .px_0p5()
2953        .pb_1()
2954        .border_x_1()
2955        .border_b_1()
2956        .border_color(cx.theme().colors().border_variant)
2957        .rounded_b_lg()
2958        .bg(cx.theme().colors().editor_background)
2959        .gap_1()
2960        .block_mouse_except_scroll()
2961        .shadow_md()
2962        .when(show_stage_restore, |el| {
2963            el.child(if status.has_secondary_hunk() {
2964                Button::new(("stage", row as u64), "Stage")
2965                    .alpha(if status.is_pending() { 0.66 } else { 1.0 })
2966                    .tooltip({
2967                        let focus_handle = editor.focus_handle(cx);
2968                        move |_window, cx| {
2969                            Tooltip::for_action_in(
2970                                "Stage Hunk",
2971                                &::git::ToggleStaged,
2972                                &focus_handle,
2973                                cx,
2974                            )
2975                        }
2976                    })
2977                    .on_click({
2978                        let editor = editor.clone();
2979                        move |_event, window, cx| {
2980                            editor.update(cx, |editor, cx| {
2981                                editor.stage_or_unstage_diff_hunks(
2982                                    true,
2983                                    vec![hunk_range.start..hunk_range.start],
2984                                    window,
2985                                    cx,
2986                                );
2987                            });
2988                        }
2989                    })
2990            } else {
2991                Button::new(("unstage", row as u64), "Unstage")
2992                    .alpha(if status.is_pending() { 0.66 } else { 1.0 })
2993                    .tooltip({
2994                        let focus_handle = editor.focus_handle(cx);
2995                        move |_window, cx| {
2996                            Tooltip::for_action_in(
2997                                "Unstage Hunk",
2998                                &::git::ToggleStaged,
2999                                &focus_handle,
3000                                cx,
3001                            )
3002                        }
3003                    })
3004                    .on_click({
3005                        let editor = editor.clone();
3006                        move |_event, window, cx| {
3007                            editor.update(cx, |editor, cx| {
3008                                editor.stage_or_unstage_diff_hunks(
3009                                    false,
3010                                    vec![hunk_range.start..hunk_range.start],
3011                                    window,
3012                                    cx,
3013                                );
3014                            });
3015                        }
3016                    })
3017            })
3018        })
3019        .when(show_stage_restore, |el| {
3020            el.child(
3021                Button::new(("restore", row as u64), "Restore")
3022                    .tooltip({
3023                        let focus_handle = editor.focus_handle(cx);
3024                        move |_window, cx| {
3025                            Tooltip::for_action_in(
3026                                "Restore Hunk",
3027                                &::git::Restore,
3028                                &focus_handle,
3029                                cx,
3030                            )
3031                        }
3032                    })
3033                    .on_click({
3034                        let editor = editor.clone();
3035                        move |_event, window, cx| {
3036                            editor.update(cx, |editor, cx| {
3037                                let snapshot = editor.snapshot(window, cx);
3038                                let point = hunk_range.start.to_point(&snapshot.buffer_snapshot());
3039                                editor.restore_hunks_in_ranges(vec![point..point], window, cx);
3040                            });
3041                        }
3042                    })
3043                    .disabled(is_created_file),
3044            )
3045        })
3046        .when(
3047            !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
3048            |el| {
3049                el.child(
3050                    IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
3051                        .shape(IconButtonShape::Square)
3052                        .icon_size(IconSize::Small)
3053                        // .disabled(!has_multiple_hunks)
3054                        .tooltip({
3055                            let focus_handle = editor.focus_handle(cx);
3056                            move |_window, cx| {
3057                                Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, cx)
3058                            }
3059                        })
3060                        .on_click({
3061                            let editor = editor.clone();
3062                            move |_event, window, cx| {
3063                                editor.update(cx, |editor, cx| {
3064                                    let snapshot = editor.snapshot(window, cx);
3065                                    let position =
3066                                        hunk_range.end.to_point(&snapshot.buffer_snapshot());
3067                                    editor.go_to_hunk_before_or_after_position(
3068                                        &snapshot,
3069                                        position,
3070                                        Direction::Next,
3071                                        true,
3072                                        window,
3073                                        cx,
3074                                    );
3075                                    editor.expand_selected_diff_hunks(cx);
3076                                });
3077                            }
3078                        }),
3079                )
3080                .child(
3081                    IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
3082                        .shape(IconButtonShape::Square)
3083                        .icon_size(IconSize::Small)
3084                        // .disabled(!has_multiple_hunks)
3085                        .tooltip({
3086                            let focus_handle = editor.focus_handle(cx);
3087                            move |_window, cx| {
3088                                Tooltip::for_action_in(
3089                                    "Previous Hunk",
3090                                    &GoToPreviousHunk,
3091                                    &focus_handle,
3092                                    cx,
3093                                )
3094                            }
3095                        })
3096                        .on_click({
3097                            let editor = editor.clone();
3098                            move |_event, window, cx| {
3099                                editor.update(cx, |editor, cx| {
3100                                    let snapshot = editor.snapshot(window, cx);
3101                                    let point =
3102                                        hunk_range.start.to_point(&snapshot.buffer_snapshot());
3103                                    editor.go_to_hunk_before_or_after_position(
3104                                        &snapshot,
3105                                        point,
3106                                        Direction::Prev,
3107                                        true,
3108                                        window,
3109                                        cx,
3110                                    );
3111                                    editor.expand_selected_diff_hunks(cx);
3112                                });
3113                            }
3114                        }),
3115                )
3116            },
3117        )
3118        .into_any_element()
3119}
3120
3121pub(super) fn update_uncommitted_diff_for_buffer(
3122    editor: Entity<Editor>,
3123    project: &Entity<Project>,
3124    buffers: impl IntoIterator<Item = Entity<Buffer>>,
3125    buffer: Entity<MultiBuffer>,
3126    cx: &mut App,
3127) -> Task<()> {
3128    let mut tasks = Vec::new();
3129    project.update(cx, |project, cx| {
3130        for buffer in buffers {
3131            if project::File::from_dyn(buffer.read(cx).file()).is_some() {
3132                tasks.push(project.open_uncommitted_diff(buffer.clone(), cx))
3133            }
3134        }
3135    });
3136    cx.spawn(async move |cx| {
3137        let diffs = future::join_all(tasks).await;
3138        if editor.read_with(cx, |editor, _cx| editor.diff_hunk_delegate.is_some()) {
3139            return;
3140        }
3141
3142        buffer.update(cx, |buffer, cx| {
3143            for diff in diffs.into_iter().flatten() {
3144                buffer.add_diff(diff, cx);
3145            }
3146        });
3147    })
3148}
3149
Served at tenant.openagents/omega Member data and write actions are omitted.