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