1use collections::{hash_map, HashMap, HashSet};
2use git::diff::DiffHunkStatus;
3use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Task, View};
4use language::{Buffer, BufferId, Point};
5use multi_buffer::{
6 Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
7 MultiBufferSnapshot, ToPoint,
8};
9use std::{
10 ops::{Range, RangeInclusive},
11 sync::Arc,
12};
13use ui::{
14 prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement,
15 ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext,
16};
17use util::RangeExt;
18
19use crate::{
20 editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, BlockDisposition,
21 BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot,
22 Editor, EditorElement, EditorSnapshot, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
23 RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
24};
25
26#[derive(Debug, Clone)]
27pub(super) struct HoveredHunk {
28 pub multi_buffer_range: Range<Anchor>,
29 pub status: DiffHunkStatus,
30 pub diff_base_byte_range: Range<usize>,
31}
32
33#[derive(Debug, Default)]
34pub(super) struct ExpandedHunks {
35 pub(crate) hunks: Vec<ExpandedHunk>,
36 diff_base: HashMap<BufferId, DiffBaseBuffer>,
37 hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
38}
39
40#[derive(Debug, Clone)]
41pub(super) struct ExpandedHunk {
42 pub blocks: Vec<CustomBlockId>,
43 pub hunk_range: Range<Anchor>,
44 pub diff_base_byte_range: Range<usize>,
45 pub status: DiffHunkStatus,
46 pub folded: bool,
47}
48
49#[derive(Debug)]
50struct DiffBaseBuffer {
51 buffer: Model<Buffer>,
52 diff_base_version: usize,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum DisplayDiffHunk {
57 Folded {
58 display_row: DisplayRow,
59 },
60
61 Unfolded {
62 diff_base_byte_range: Range<usize>,
63 display_row_range: Range<DisplayRow>,
64 multi_buffer_range: Range<Anchor>,
65 status: DiffHunkStatus,
66 },
67}
68
69impl ExpandedHunks {
70 pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {
71 self.hunks
72 .iter()
73 .filter(move |hunk| include_folded || !hunk.folded)
74 }
75}
76
77impl Editor {
78 pub(super) fn toggle_hovered_hunk(
79 &mut self,
80 hovered_hunk: &HoveredHunk,
81 cx: &mut ViewContext<Editor>,
82 ) {
83 let editor_snapshot = self.snapshot(cx);
84 if let Some(diff_hunk) = to_diff_hunk(hovered_hunk, &editor_snapshot.buffer_snapshot) {
85 self.toggle_hunks_expanded(vec![diff_hunk], cx);
86 self.change_selections(None, cx, |selections| selections.refresh());
87 }
88 }
89
90 pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
91 let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
92 let selections = self.selections.disjoint_anchors();
93 self.toggle_hunks_expanded(
94 hunks_for_selections(&multi_buffer_snapshot, &selections),
95 cx,
96 );
97 }
98
99 pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext<Self>) {
100 let snapshot = self.snapshot(cx);
101 let display_rows_with_expanded_hunks = self
102 .expanded_hunks
103 .hunks(false)
104 .map(|hunk| &hunk.hunk_range)
105 .map(|anchor_range| {
106 (
107 anchor_range
108 .start
109 .to_display_point(&snapshot.display_snapshot)
110 .row(),
111 anchor_range
112 .end
113 .to_display_point(&snapshot.display_snapshot)
114 .row(),
115 )
116 })
117 .collect::<HashMap<_, _>>();
118 let hunks = snapshot
119 .display_snapshot
120 .buffer_snapshot
121 .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
122 .filter(|hunk| {
123 let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0)
124 .to_display_point(&snapshot.display_snapshot)
125 ..Point::new(hunk.row_range.end.0, 0)
126 .to_display_point(&snapshot.display_snapshot);
127 let row_range_end =
128 display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row());
129 row_range_end.is_none() || row_range_end != Some(&hunk_display_row_range.end.row())
130 });
131 self.toggle_hunks_expanded(hunks.collect(), cx);
132 }
133
134 fn toggle_hunks_expanded(
135 &mut self,
136 hunks_to_toggle: Vec<MultiBufferDiffHunk>,
137 cx: &mut ViewContext<Self>,
138 ) {
139 let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None);
140 let new_toggle_task = cx.spawn(move |editor, mut cx| async move {
141 if let Some(task) = previous_toggle_task {
142 task.await;
143 }
144
145 editor
146 .update(&mut cx, |editor, cx| {
147 let snapshot = editor.snapshot(cx);
148 let mut hunks_to_toggle = hunks_to_toggle.into_iter().fuse().peekable();
149 let mut highlights_to_remove =
150 Vec::with_capacity(editor.expanded_hunks.hunks.len());
151 let mut blocks_to_remove = HashSet::default();
152 let mut hunks_to_expand = Vec::new();
153 editor.expanded_hunks.hunks.retain(|expanded_hunk| {
154 if expanded_hunk.folded {
155 return true;
156 }
157 let expanded_hunk_row_range = expanded_hunk
158 .hunk_range
159 .start
160 .to_display_point(&snapshot)
161 .row()
162 ..expanded_hunk
163 .hunk_range
164 .end
165 .to_display_point(&snapshot)
166 .row();
167 let mut retain = true;
168 while let Some(hunk_to_toggle) = hunks_to_toggle.peek() {
169 match diff_hunk_to_display(hunk_to_toggle, &snapshot) {
170 DisplayDiffHunk::Folded { .. } => {
171 hunks_to_toggle.next();
172 continue;
173 }
174 DisplayDiffHunk::Unfolded {
175 diff_base_byte_range,
176 display_row_range,
177 multi_buffer_range,
178 status,
179 } => {
180 let hunk_to_toggle_row_range = display_row_range;
181 if hunk_to_toggle_row_range.start > expanded_hunk_row_range.end
182 {
183 break;
184 } else if expanded_hunk_row_range == hunk_to_toggle_row_range {
185 highlights_to_remove.push(expanded_hunk.hunk_range.clone());
186 blocks_to_remove
187 .extend(expanded_hunk.blocks.iter().copied());
188 hunks_to_toggle.next();
189 retain = false;
190 break;
191 } else {
192 hunks_to_expand.push(HoveredHunk {
193 status,
194 multi_buffer_range,
195 diff_base_byte_range,
196 });
197 hunks_to_toggle.next();
198 continue;
199 }
200 }
201 }
202 }
203
204 retain
205 });
206 for remaining_hunk in hunks_to_toggle {
207 let remaining_hunk_point_range =
208 Point::new(remaining_hunk.row_range.start.0, 0)
209 ..Point::new(remaining_hunk.row_range.end.0, 0);
210 hunks_to_expand.push(HoveredHunk {
211 status: hunk_status(&remaining_hunk),
212 multi_buffer_range: snapshot
213 .buffer_snapshot
214 .anchor_before(remaining_hunk_point_range.start)
215 ..snapshot
216 .buffer_snapshot
217 .anchor_after(remaining_hunk_point_range.end),
218 diff_base_byte_range: remaining_hunk.diff_base_byte_range.clone(),
219 });
220 }
221
222 editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
223 editor.remove_blocks(blocks_to_remove, None, cx);
224 for hunk in hunks_to_expand {
225 editor.expand_diff_hunk(None, &hunk, cx);
226 }
227 cx.notify();
228 })
229 .ok();
230 });
231
232 self.expanded_hunks
233 .hunk_update_tasks
234 .insert(None, cx.background_executor().spawn(new_toggle_task));
235 }
236
237 pub(super) fn expand_diff_hunk(
238 &mut self,
239 diff_base_buffer: Option<Model<Buffer>>,
240 hunk: &HoveredHunk,
241 cx: &mut ViewContext<'_, Editor>,
242 ) -> Option<()> {
243 let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
244 let multi_buffer_row_range = hunk
245 .multi_buffer_range
246 .start
247 .to_point(&multi_buffer_snapshot)
248 ..hunk.multi_buffer_range.end.to_point(&multi_buffer_snapshot);
249 let hunk_start = hunk.multi_buffer_range.start;
250 let hunk_end = hunk.multi_buffer_range.end;
251
252 let buffer = self.buffer().clone();
253 let snapshot = self.snapshot(cx);
254 let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| {
255 let hunk = buffer_diff_hunk(&snapshot.buffer_snapshot, multi_buffer_row_range.clone())?;
256 let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx);
257 if buffer_ranges.len() == 1 {
258 let (buffer, _, _) = buffer_ranges.pop()?;
259 let diff_base_buffer = diff_base_buffer
260 .or_else(|| self.current_diff_base_buffer(&buffer, cx))
261 .or_else(|| create_diff_base_buffer(&buffer, cx))?;
262 let buffer = buffer.read(cx);
263 let deleted_text_lines = buffer.diff_base().map(|diff_base| {
264 let diff_start_row = diff_base
265 .offset_to_point(hunk.diff_base_byte_range.start)
266 .row;
267 let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
268
269 diff_end_row - diff_start_row
270 })?;
271 Some((diff_base_buffer, deleted_text_lines))
272 } else {
273 None
274 }
275 })?;
276
277 let block_insert_index = match self.expanded_hunks.hunks.binary_search_by(|probe| {
278 probe
279 .hunk_range
280 .start
281 .cmp(&hunk_start, &multi_buffer_snapshot)
282 }) {
283 Ok(_already_present) => return None,
284 Err(ix) => ix,
285 };
286
287 let blocks;
288 match hunk.status {
289 DiffHunkStatus::Removed => {
290 blocks = self.insert_blocks(
291 [
292 self.hunk_header_block(&hunk, cx),
293 Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx),
294 ],
295 None,
296 cx,
297 );
298 }
299 DiffHunkStatus::Added => {
300 self.highlight_rows::<DiffRowHighlight>(
301 to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
302 added_hunk_color(cx),
303 false,
304 cx,
305 );
306 blocks = self.insert_blocks([self.hunk_header_block(&hunk, cx)], None, cx);
307 }
308 DiffHunkStatus::Modified => {
309 self.highlight_rows::<DiffRowHighlight>(
310 to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
311 added_hunk_color(cx),
312 false,
313 cx,
314 );
315 blocks = self.insert_blocks(
316 [
317 self.hunk_header_block(&hunk, cx),
318 Self::deleted_text_block(hunk, diff_base_buffer, deleted_text_lines, cx),
319 ],
320 None,
321 cx,
322 );
323 }
324 };
325 self.expanded_hunks.hunks.insert(
326 block_insert_index,
327 ExpandedHunk {
328 blocks,
329 hunk_range: hunk_start..hunk_end,
330 status: hunk.status,
331 folded: false,
332 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
333 },
334 );
335
336 Some(())
337 }
338
339 fn hunk_header_block(
340 &self,
341 hunk: &HoveredHunk,
342 cx: &mut ViewContext<'_, Editor>,
343 ) -> BlockProperties<Anchor> {
344 let border_color = cx.theme().colors().border_disabled;
345 let gutter_color = match hunk.status {
346 DiffHunkStatus::Added => cx.theme().status().created,
347 DiffHunkStatus::Modified => cx.theme().status().modified,
348 DiffHunkStatus::Removed => cx.theme().status().deleted,
349 };
350
351 BlockProperties {
352 position: hunk.multi_buffer_range.start,
353 height: 1,
354 style: BlockStyle::Sticky,
355 disposition: BlockDisposition::Above,
356 priority: 0,
357 render: Box::new({
358 let editor = cx.view().clone();
359 let hunk = hunk.clone();
360 move |cx| {
361 let hunk_controls_menu_handle =
362 editor.read(cx).hunk_controls_menu_handle.clone();
363
364 h_flex()
365 .id(cx.block_id)
366 .w_full()
367 .h(cx.line_height())
368 .child(
369 div()
370 .id("gutter-strip")
371 .w(EditorElement::diff_hunk_strip_width(cx.line_height()))
372 .h_full()
373 .bg(gutter_color)
374 .cursor(CursorStyle::PointingHand)
375 .on_click({
376 let editor = editor.clone();
377 let hunk = hunk.clone();
378 move |_event, cx| {
379 editor.update(cx, |editor, cx| {
380 editor.toggle_hovered_hunk(&hunk, cx);
381 });
382 }
383 }),
384 )
385 .child(
386 h_flex()
387 .size_full()
388 .justify_between()
389 .border_t_1()
390 .border_color(border_color)
391 .child(
392 h_flex()
393 .gap_2()
394 .pl_6()
395 .child(
396 IconButton::new("next-hunk", IconName::ArrowDown)
397 .shape(IconButtonShape::Square)
398 .icon_size(IconSize::Small)
399 .tooltip({
400 let focus_handle = editor.focus_handle(cx);
401 move |cx| {
402 Tooltip::for_action_in(
403 "Next Hunk",
404 &GoToHunk,
405 &focus_handle,
406 cx,
407 )
408 }
409 })
410 .on_click({
411 let editor = editor.clone();
412 let hunk = hunk.clone();
413 move |_event, cx| {
414 editor.update(cx, |editor, cx| {
415 let snapshot = editor.snapshot(cx);
416 let position = hunk
417 .multi_buffer_range
418 .end
419 .to_point(
420 &snapshot.buffer_snapshot,
421 );
422 if let Some(hunk) = editor
423 .go_to_hunk_after_position(
424 &snapshot, position, cx,
425 )
426 {
427 let multi_buffer_start = snapshot
428 .buffer_snapshot
429 .anchor_before(Point::new(
430 hunk.row_range.start.0,
431 0,
432 ));
433 let multi_buffer_end = snapshot
434 .buffer_snapshot
435 .anchor_after(Point::new(
436 hunk.row_range.end.0,
437 0,
438 ));
439 editor.expand_diff_hunk(
440 None,
441 &HoveredHunk {
442 multi_buffer_range:
443 multi_buffer_start
444 ..multi_buffer_end,
445 status: hunk_status(&hunk),
446 diff_base_byte_range: hunk
447 .diff_base_byte_range,
448 },
449 cx,
450 );
451 }
452 });
453 }
454 }),
455 )
456 .child(
457 IconButton::new("prev-hunk", IconName::ArrowUp)
458 .shape(IconButtonShape::Square)
459 .icon_size(IconSize::Small)
460 .tooltip({
461 let focus_handle = editor.focus_handle(cx);
462 move |cx| {
463 Tooltip::for_action_in(
464 "Previous Hunk",
465 &GoToPrevHunk,
466 &focus_handle,
467 cx,
468 )
469 }
470 })
471 .on_click({
472 let editor = editor.clone();
473 let hunk = hunk.clone();
474 move |_event, cx| {
475 editor.update(cx, |editor, cx| {
476 let snapshot = editor.snapshot(cx);
477 let position = hunk
478 .multi_buffer_range
479 .start
480 .to_point(
481 &snapshot.buffer_snapshot,
482 );
483 let hunk = editor
484 .go_to_hunk_before_position(
485 &snapshot, position, cx,
486 );
487 if let Some(hunk) = hunk {
488 let multi_buffer_start = snapshot
489 .buffer_snapshot
490 .anchor_before(Point::new(
491 hunk.row_range.start.0,
492 0,
493 ));
494 let multi_buffer_end = snapshot
495 .buffer_snapshot
496 .anchor_after(Point::new(
497 hunk.row_range.end.0,
498 0,
499 ));
500 editor.expand_diff_hunk(
501 None,
502 &HoveredHunk {
503 multi_buffer_range:
504 multi_buffer_start
505 ..multi_buffer_end,
506 status: hunk_status(&hunk),
507 diff_base_byte_range: hunk
508 .diff_base_byte_range,
509 },
510 cx,
511 );
512 }
513 });
514 }
515 }),
516 ),
517 )
518 .child(
519 h_flex()
520 .gap_2()
521 .pr_6()
522 .child({
523 let focus = editor.focus_handle(cx);
524 PopoverMenu::new("hunk-controls-dropdown")
525 .trigger(
526 IconButton::new(
527 "toggle_editor_selections_icon",
528 IconName::EllipsisVertical,
529 )
530 .shape(IconButtonShape::Square)
531 .icon_size(IconSize::Small)
532 .style(ButtonStyle::Subtle)
533 .selected(
534 hunk_controls_menu_handle.is_deployed(),
535 )
536 .when(
537 !hunk_controls_menu_handle.is_deployed(),
538 |this| {
539 this.tooltip(|cx| {
540 Tooltip::text("Hunk Controls", cx)
541 })
542 },
543 ),
544 )
545 .anchor(AnchorCorner::TopRight)
546 .with_handle(hunk_controls_menu_handle)
547 .menu(move |cx| {
548 let focus = focus.clone();
549 let menu =
550 ContextMenu::build(cx, move |menu, _| {
551 menu.context(focus.clone()).action(
552 "Discard All",
553 RevertFile.boxed_clone(),
554 )
555 });
556 Some(menu)
557 })
558 })
559 .child(
560 IconButton::new("discard", IconName::RotateCcw)
561 .shape(IconButtonShape::Square)
562 .icon_size(IconSize::Small)
563 .tooltip({
564 let focus_handle = editor.focus_handle(cx);
565 move |cx| {
566 Tooltip::for_action_in(
567 "Discard Hunk",
568 &RevertSelectedHunks,
569 &focus_handle,
570 cx,
571 )
572 }
573 })
574 .on_click({
575 let editor = editor.clone();
576 let hunk = hunk.clone();
577 move |_event, cx| {
578 let multi_buffer =
579 editor.read(cx).buffer().clone();
580 let multi_buffer_snapshot =
581 multi_buffer.read(cx).snapshot(cx);
582 let mut revert_changes = HashMap::default();
583 if let Some(hunk) =
584 crate::hunk_diff::to_diff_hunk(
585 &hunk,
586 &multi_buffer_snapshot,
587 )
588 {
589 Editor::prepare_revert_change(
590 &mut revert_changes,
591 &multi_buffer,
592 &hunk,
593 cx,
594 );
595 }
596 if !revert_changes.is_empty() {
597 editor.update(cx, |editor, cx| {
598 editor.revert(revert_changes, cx)
599 });
600 }
601 }
602 }),
603 )
604 .child(
605 IconButton::new("collapse", IconName::Close)
606 .shape(IconButtonShape::Square)
607 .icon_size(IconSize::Small)
608 .tooltip({
609 let focus_handle = editor.focus_handle(cx);
610 move |cx| {
611 Tooltip::for_action_in(
612 "Collapse Hunk",
613 &ToggleHunkDiff,
614 &focus_handle,
615 cx,
616 )
617 }
618 })
619 .on_click({
620 let editor = editor.clone();
621 let hunk = hunk.clone();
622 move |_event, cx| {
623 editor.update(cx, |editor, cx| {
624 editor.toggle_hovered_hunk(&hunk, cx);
625 });
626 }
627 }),
628 ),
629 ),
630 )
631 .into_any_element()
632 }
633 }),
634 }
635 }
636
637 fn deleted_text_block(
638 hunk: &HoveredHunk,
639 diff_base_buffer: Model<Buffer>,
640 deleted_text_height: u32,
641 cx: &mut ViewContext<'_, Editor>,
642 ) -> BlockProperties<Anchor> {
643 let gutter_color = match hunk.status {
644 DiffHunkStatus::Added => unreachable!(),
645 DiffHunkStatus::Modified => cx.theme().status().modified,
646 DiffHunkStatus::Removed => cx.theme().status().deleted,
647 };
648 let deleted_hunk_color = deleted_hunk_color(cx);
649 let (editor_height, editor_with_deleted_text) =
650 editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
651 let editor = cx.view().clone();
652 let hunk = hunk.clone();
653 let height = editor_height.max(deleted_text_height);
654 BlockProperties {
655 position: hunk.multi_buffer_range.start,
656 height,
657 style: BlockStyle::Flex,
658 disposition: BlockDisposition::Above,
659 priority: 0,
660 render: Box::new(move |cx| {
661 let width = EditorElement::diff_hunk_strip_width(cx.line_height());
662 let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
663
664 h_flex()
665 .id(cx.block_id)
666 .bg(deleted_hunk_color)
667 .h(height as f32 * cx.line_height())
668 .w_full()
669 .child(
670 h_flex()
671 .id("gutter")
672 .max_w(gutter_dimensions.full_width())
673 .min_w(gutter_dimensions.full_width())
674 .size_full()
675 .child(
676 h_flex()
677 .id("gutter hunk")
678 .bg(gutter_color)
679 .pl(gutter_dimensions.margin
680 + gutter_dimensions
681 .git_blame_entries_width
682 .unwrap_or_default())
683 .max_w(width)
684 .min_w(width)
685 .size_full()
686 .cursor(CursorStyle::PointingHand)
687 .on_mouse_down(MouseButton::Left, {
688 let editor = editor.clone();
689 let hunk = hunk.clone();
690 move |_event, cx| {
691 editor.update(cx, |editor, cx| {
692 editor.toggle_hovered_hunk(&hunk, cx);
693 });
694 }
695 }),
696 ),
697 )
698 .child(editor_with_deleted_text.clone())
699 .into_any_element()
700 }),
701 }
702 }
703
704 pub(super) fn clear_clicked_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
705 self.expanded_hunks.hunk_update_tasks.clear();
706 self.clear_row_highlights::<DiffRowHighlight>();
707 let to_remove = self
708 .expanded_hunks
709 .hunks
710 .drain(..)
711 .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
712 .collect::<HashSet<_>>();
713 if to_remove.is_empty() {
714 false
715 } else {
716 self.remove_blocks(to_remove, None, cx);
717 true
718 }
719 }
720
721 pub(super) fn sync_expanded_diff_hunks(
722 &mut self,
723 buffer: Model<Buffer>,
724 cx: &mut ViewContext<'_, Self>,
725 ) {
726 let buffer_id = buffer.read(cx).remote_id();
727 let buffer_diff_base_version = buffer.read(cx).diff_base_version();
728 self.expanded_hunks
729 .hunk_update_tasks
730 .remove(&Some(buffer_id));
731 let diff_base_buffer = self.current_diff_base_buffer(&buffer, cx);
732 let new_sync_task = cx.spawn(move |editor, mut cx| async move {
733 let diff_base_buffer_unchanged = diff_base_buffer.is_some();
734 let Ok(diff_base_buffer) =
735 cx.update(|cx| diff_base_buffer.or_else(|| create_diff_base_buffer(&buffer, cx)))
736 else {
737 return;
738 };
739 editor
740 .update(&mut cx, |editor, cx| {
741 if let Some(diff_base_buffer) = &diff_base_buffer {
742 editor.expanded_hunks.diff_base.insert(
743 buffer_id,
744 DiffBaseBuffer {
745 buffer: diff_base_buffer.clone(),
746 diff_base_version: buffer_diff_base_version,
747 },
748 );
749 }
750
751 let snapshot = editor.snapshot(cx);
752 let mut recalculated_hunks = snapshot
753 .buffer_snapshot
754 .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
755 .filter(|hunk| hunk.buffer_id == buffer_id)
756 .fuse()
757 .peekable();
758 let mut highlights_to_remove =
759 Vec::with_capacity(editor.expanded_hunks.hunks.len());
760 let mut blocks_to_remove = HashSet::default();
761 let mut hunks_to_reexpand =
762 Vec::with_capacity(editor.expanded_hunks.hunks.len());
763 editor.expanded_hunks.hunks.retain_mut(|expanded_hunk| {
764 if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) {
765 return true;
766 };
767
768 let mut retain = false;
769 if diff_base_buffer_unchanged {
770 let expanded_hunk_display_range = expanded_hunk
771 .hunk_range
772 .start
773 .to_display_point(&snapshot)
774 .row()
775 ..expanded_hunk
776 .hunk_range
777 .end
778 .to_display_point(&snapshot)
779 .row();
780 while let Some(buffer_hunk) = recalculated_hunks.peek() {
781 match diff_hunk_to_display(buffer_hunk, &snapshot) {
782 DisplayDiffHunk::Folded { display_row } => {
783 recalculated_hunks.next();
784 if !expanded_hunk.folded
785 && expanded_hunk_display_range
786 .to_inclusive()
787 .contains(&display_row)
788 {
789 retain = true;
790 expanded_hunk.folded = true;
791 highlights_to_remove
792 .push(expanded_hunk.hunk_range.clone());
793 for block in expanded_hunk.blocks.drain(..) {
794 blocks_to_remove.insert(block);
795 }
796 break;
797 } else {
798 continue;
799 }
800 }
801 DisplayDiffHunk::Unfolded {
802 diff_base_byte_range,
803 display_row_range,
804 multi_buffer_range,
805 status,
806 } => {
807 let hunk_display_range = display_row_range;
808 if expanded_hunk_display_range.start
809 > hunk_display_range.end
810 {
811 recalculated_hunks.next();
812 continue;
813 } else if expanded_hunk_display_range.end
814 < hunk_display_range.start
815 {
816 break;
817 } else {
818 if !expanded_hunk.folded
819 && expanded_hunk_display_range == hunk_display_range
820 && expanded_hunk.status == hunk_status(buffer_hunk)
821 && expanded_hunk.diff_base_byte_range
822 == buffer_hunk.diff_base_byte_range
823 {
824 recalculated_hunks.next();
825 retain = true;
826 } else {
827 hunks_to_reexpand.push(HoveredHunk {
828 status,
829 multi_buffer_range,
830 diff_base_byte_range,
831 });
832 }
833 break;
834 }
835 }
836 }
837 }
838 }
839 if !retain {
840 blocks_to_remove.extend(expanded_hunk.blocks.drain(..));
841 highlights_to_remove.push(expanded_hunk.hunk_range.clone());
842 }
843 retain
844 });
845
846 editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
847 editor.remove_blocks(blocks_to_remove, None, cx);
848
849 if let Some(diff_base_buffer) = &diff_base_buffer {
850 for hunk in hunks_to_reexpand {
851 editor.expand_diff_hunk(Some(diff_base_buffer.clone()), &hunk, cx);
852 }
853 }
854 })
855 .ok();
856 });
857
858 self.expanded_hunks.hunk_update_tasks.insert(
859 Some(buffer_id),
860 cx.background_executor().spawn(new_sync_task),
861 );
862 }
863
864 fn current_diff_base_buffer(
865 &mut self,
866 buffer: &Model<Buffer>,
867 cx: &mut AppContext,
868 ) -> Option<Model<Buffer>> {
869 buffer.update(cx, |buffer, _| {
870 match self.expanded_hunks.diff_base.entry(buffer.remote_id()) {
871 hash_map::Entry::Occupied(o) => {
872 if o.get().diff_base_version != buffer.diff_base_version() {
873 o.remove();
874 None
875 } else {
876 Some(o.get().buffer.clone())
877 }
878 }
879 hash_map::Entry::Vacant(_) => None,
880 }
881 })
882 }
883}
884
885fn to_diff_hunk(
886 hovered_hunk: &HoveredHunk,
887 multi_buffer_snapshot: &MultiBufferSnapshot,
888) -> Option<MultiBufferDiffHunk> {
889 let buffer_id = hovered_hunk
890 .multi_buffer_range
891 .start
892 .buffer_id
893 .or(hovered_hunk.multi_buffer_range.end.buffer_id)?;
894 let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor
895 ..hovered_hunk.multi_buffer_range.end.text_anchor;
896 let point_range = hovered_hunk
897 .multi_buffer_range
898 .to_point(multi_buffer_snapshot);
899 Some(MultiBufferDiffHunk {
900 row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
901 buffer_id,
902 buffer_range,
903 diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
904 })
905}
906
907fn create_diff_base_buffer(buffer: &Model<Buffer>, cx: &mut AppContext) -> Option<Model<Buffer>> {
908 buffer
909 .update(cx, |buffer, _| {
910 let language = buffer.language().cloned();
911 let diff_base = buffer.diff_base()?.clone();
912 Some((buffer.line_ending(), diff_base, language))
913 })
914 .map(|(line_ending, diff_base, language)| {
915 cx.new_model(|cx| {
916 let buffer = Buffer::local_normalized(diff_base, line_ending, cx);
917 match language {
918 Some(language) => buffer.with_language(language, cx),
919 None => buffer,
920 }
921 })
922 })
923}
924
925fn added_hunk_color(cx: &AppContext) -> Hsla {
926 let mut created_color = cx.theme().status().git().created;
927 created_color.fade_out(0.7);
928 created_color
929}
930
931fn deleted_hunk_color(cx: &AppContext) -> Hsla {
932 let mut deleted_color = cx.theme().status().deleted;
933 deleted_color.fade_out(0.7);
934 deleted_color
935}
936
937fn editor_with_deleted_text(
938 diff_base_buffer: Model<Buffer>,
939 deleted_color: Hsla,
940 hunk: &HoveredHunk,
941 cx: &mut ViewContext<'_, Editor>,
942) -> (u32, View<Editor>) {
943 let parent_editor = cx.view().downgrade();
944 let editor = cx.new_view(|cx| {
945 let multi_buffer =
946 cx.new_model(|_| MultiBuffer::without_headers(language::Capability::ReadOnly));
947 multi_buffer.update(cx, |multi_buffer, cx| {
948 multi_buffer.push_excerpts(
949 diff_base_buffer,
950 Some(ExcerptRange {
951 context: hunk.diff_base_byte_range.clone(),
952 primary: None,
953 }),
954 cx,
955 );
956 });
957
958 let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
959 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
960 editor.set_show_wrap_guides(false, cx);
961 editor.set_show_gutter(false, cx);
962 editor.scroll_manager.set_forbid_vertical_scroll(true);
963 editor.set_read_only(true);
964 editor.set_show_inline_completions(Some(false), cx);
965 editor.highlight_rows::<DiffRowHighlight>(
966 Anchor::min()..=Anchor::max(),
967 deleted_color,
968 false,
969 cx,
970 );
971 editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
972 editor
973 ._subscriptions
974 .extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
975 editor.change_selections(None, cx, |s| {
976 s.try_cancel();
977 });
978 })]);
979
980 let parent_editor_for_reverts = parent_editor.clone();
981 let original_multi_buffer_range = hunk.multi_buffer_range.clone();
982 let diff_base_range = hunk.diff_base_byte_range.clone();
983 editor
984 .register_action::<RevertSelectedHunks>(move |_, cx| {
985 parent_editor_for_reverts
986 .update(cx, |editor, cx| {
987 let Some((buffer, original_text)) =
988 editor.buffer().update(cx, |buffer, cx| {
989 let (_, buffer, _) = buffer
990 .excerpt_containing(original_multi_buffer_range.start, cx)?;
991 let original_text =
992 buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
993 Some((buffer, Arc::from(original_text.to_string())))
994 })
995 else {
996 return;
997 };
998 buffer.update(cx, |buffer, cx| {
999 buffer.edit(
1000 Some((
1001 original_multi_buffer_range.start.text_anchor
1002 ..original_multi_buffer_range.end.text_anchor,
1003 original_text,
1004 )),
1005 None,
1006 cx,
1007 )
1008 });
1009 })
1010 .ok();
1011 })
1012 .detach();
1013 let hunk = hunk.clone();
1014 editor
1015 .register_action::<ToggleHunkDiff>(move |_, cx| {
1016 parent_editor
1017 .update(cx, |editor, cx| {
1018 editor.toggle_hovered_hunk(&hunk, cx);
1019 })
1020 .ok();
1021 })
1022 .detach();
1023 editor
1024 });
1025
1026 let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0);
1027 (editor_height, editor)
1028}
1029
1030fn buffer_diff_hunk(
1031 buffer_snapshot: &MultiBufferSnapshot,
1032 row_range: Range<Point>,
1033) -> Option<MultiBufferDiffHunk> {
1034 let mut hunks = buffer_snapshot.git_diff_hunks_in_range(
1035 MultiBufferRow(row_range.start.row)..MultiBufferRow(row_range.end.row),
1036 );
1037 let hunk = hunks.next()?;
1038 let second_hunk = hunks.next();
1039 if second_hunk.is_none() {
1040 return Some(hunk);
1041 }
1042 None
1043}
1044
1045fn to_inclusive_row_range(
1046 row_range: Range<Anchor>,
1047 snapshot: &EditorSnapshot,
1048) -> RangeInclusive<Anchor> {
1049 let mut end = row_range.end.to_point(&snapshot.buffer_snapshot);
1050 if end.column == 0 && end.row > 0 {
1051 end = Point::new(
1052 end.row - 1,
1053 snapshot
1054 .buffer_snapshot
1055 .line_len(MultiBufferRow(end.row - 1)),
1056 );
1057 }
1058 row_range.start..=snapshot.buffer_snapshot.anchor_after(end)
1059}
1060
1061impl DisplayDiffHunk {
1062 pub fn start_display_row(&self) -> DisplayRow {
1063 match self {
1064 &DisplayDiffHunk::Folded { display_row } => display_row,
1065 DisplayDiffHunk::Unfolded {
1066 display_row_range, ..
1067 } => display_row_range.start,
1068 }
1069 }
1070
1071 pub fn contains_display_row(&self, display_row: DisplayRow) -> bool {
1072 let range = match self {
1073 &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
1074
1075 DisplayDiffHunk::Unfolded {
1076 display_row_range, ..
1077 } => display_row_range.start..=display_row_range.end,
1078 };
1079
1080 range.contains(&display_row)
1081 }
1082}
1083
1084pub fn diff_hunk_to_display(
1085 hunk: &MultiBufferDiffHunk,
1086 snapshot: &DisplaySnapshot,
1087) -> DisplayDiffHunk {
1088 let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
1089 let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0);
1090 let hunk_end_point_sub = Point::new(
1091 hunk.row_range
1092 .end
1093 .0
1094 .saturating_sub(1)
1095 .max(hunk.row_range.start.0),
1096 0,
1097 );
1098
1099 let status = hunk_status(hunk);
1100 let is_removal = status == DiffHunkStatus::Removed;
1101
1102 let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0);
1103 let folds_end = Point::new(hunk.row_range.end.0 + 2, 0);
1104 let folds_range = folds_start..folds_end;
1105
1106 let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
1107 let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
1108 let fold_point_range = fold_point_range.start..=fold_point_range.end;
1109
1110 let folded_start = fold_point_range.contains(&hunk_start_point);
1111 let folded_end = fold_point_range.contains(&hunk_end_point_sub);
1112 let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
1113
1114 (folded_start && folded_end) || (is_removal && folded_start_sub)
1115 });
1116
1117 if let Some(fold) = containing_fold {
1118 let row = fold.range.start.to_display_point(snapshot).row();
1119 DisplayDiffHunk::Folded { display_row: row }
1120 } else {
1121 let start = hunk_start_point.to_display_point(snapshot).row();
1122
1123 let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start);
1124 let hunk_end_point = Point::new(hunk_end_row.0, 0);
1125
1126 let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point);
1127 let multi_buffer_end = snapshot.buffer_snapshot.anchor_after(hunk_end_point);
1128 let end = hunk_end_point.to_display_point(snapshot).row();
1129
1130 DisplayDiffHunk::Unfolded {
1131 display_row_range: start..end,
1132 multi_buffer_range: multi_buffer_start..multi_buffer_end,
1133 status,
1134 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
1135 }
1136 }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142 use crate::{editor_tests::init_test, hunk_status};
1143 use gpui::{Context, TestAppContext};
1144 use language::Capability::ReadWrite;
1145 use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow};
1146 use project::{FakeFs, Project};
1147 use unindent::Unindent as _;
1148
1149 #[gpui::test]
1150 async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
1151 use git::diff::DiffHunkStatus;
1152 init_test(cx, |_| {});
1153
1154 let fs = FakeFs::new(cx.background_executor.clone());
1155 let project = Project::test(fs, [], cx).await;
1156
1157 // buffer has two modified hunks with two rows each
1158 let buffer_1 = project.update(cx, |project, cx| {
1159 project.create_local_buffer(
1160 "
1161 1.zero
1162 1.ONE
1163 1.TWO
1164 1.three
1165 1.FOUR
1166 1.FIVE
1167 1.six
1168 "
1169 .unindent()
1170 .as_str(),
1171 None,
1172 cx,
1173 )
1174 });
1175 buffer_1.update(cx, |buffer, cx| {
1176 buffer.set_diff_base(
1177 Some(
1178 "
1179 1.zero
1180 1.one
1181 1.two
1182 1.three
1183 1.four
1184 1.five
1185 1.six
1186 "
1187 .unindent(),
1188 ),
1189 cx,
1190 );
1191 });
1192
1193 // buffer has a deletion hunk and an insertion hunk
1194 let buffer_2 = project.update(cx, |project, cx| {
1195 project.create_local_buffer(
1196 "
1197 2.zero
1198 2.one
1199 2.two
1200 2.three
1201 2.four
1202 2.five
1203 2.six
1204 "
1205 .unindent()
1206 .as_str(),
1207 None,
1208 cx,
1209 )
1210 });
1211 buffer_2.update(cx, |buffer, cx| {
1212 buffer.set_diff_base(
1213 Some(
1214 "
1215 2.zero
1216 2.one
1217 2.one-and-a-half
1218 2.two
1219 2.three
1220 2.four
1221 2.six
1222 "
1223 .unindent(),
1224 ),
1225 cx,
1226 );
1227 });
1228
1229 cx.background_executor.run_until_parked();
1230
1231 let multibuffer = cx.new_model(|cx| {
1232 let mut multibuffer = MultiBuffer::new(ReadWrite);
1233 multibuffer.push_excerpts(
1234 buffer_1.clone(),
1235 [
1236 // excerpt ends in the middle of a modified hunk
1237 ExcerptRange {
1238 context: Point::new(0, 0)..Point::new(1, 5),
1239 primary: Default::default(),
1240 },
1241 // excerpt begins in the middle of a modified hunk
1242 ExcerptRange {
1243 context: Point::new(5, 0)..Point::new(6, 5),
1244 primary: Default::default(),
1245 },
1246 ],
1247 cx,
1248 );
1249 multibuffer.push_excerpts(
1250 buffer_2.clone(),
1251 [
1252 // excerpt ends at a deletion
1253 ExcerptRange {
1254 context: Point::new(0, 0)..Point::new(1, 5),
1255 primary: Default::default(),
1256 },
1257 // excerpt starts at a deletion
1258 ExcerptRange {
1259 context: Point::new(2, 0)..Point::new(2, 5),
1260 primary: Default::default(),
1261 },
1262 // excerpt fully contains a deletion hunk
1263 ExcerptRange {
1264 context: Point::new(1, 0)..Point::new(2, 5),
1265 primary: Default::default(),
1266 },
1267 // excerpt fully contains an insertion hunk
1268 ExcerptRange {
1269 context: Point::new(4, 0)..Point::new(6, 5),
1270 primary: Default::default(),
1271 },
1272 ],
1273 cx,
1274 );
1275 multibuffer
1276 });
1277
1278 let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
1279
1280 assert_eq!(
1281 snapshot.text(),
1282 "
1283 1.zero
1284 1.ONE
1285 1.FIVE
1286 1.six
1287 2.zero
1288 2.one
1289 2.two
1290 2.one
1291 2.two
1292 2.four
1293 2.five
1294 2.six"
1295 .unindent()
1296 );
1297
1298 let expected = [
1299 (
1300 DiffHunkStatus::Modified,
1301 MultiBufferRow(1)..MultiBufferRow(2),
1302 ),
1303 (
1304 DiffHunkStatus::Modified,
1305 MultiBufferRow(2)..MultiBufferRow(3),
1306 ),
1307 //TODO: Define better when and where removed hunks show up at range extremities
1308 (
1309 DiffHunkStatus::Removed,
1310 MultiBufferRow(6)..MultiBufferRow(6),
1311 ),
1312 (
1313 DiffHunkStatus::Removed,
1314 MultiBufferRow(8)..MultiBufferRow(8),
1315 ),
1316 (
1317 DiffHunkStatus::Added,
1318 MultiBufferRow(10)..MultiBufferRow(11),
1319 ),
1320 ];
1321
1322 assert_eq!(
1323 snapshot
1324 .git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12))
1325 .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1326 .collect::<Vec<_>>(),
1327 &expected,
1328 );
1329
1330 assert_eq!(
1331 snapshot
1332 .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12))
1333 .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1334 .collect::<Vec<_>>(),
1335 expected
1336 .iter()
1337 .rev()
1338 .cloned()
1339 .collect::<Vec<_>>()
1340 .as_slice(),
1341 );
1342 }
1343}