Skip to repository content3149 lines · 112.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:31:34.878Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
git.rs
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