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 .child(
518 IconButton::new("discard", IconName::RotateCcw)
519 .shape(IconButtonShape::Square)
520 .icon_size(IconSize::Small)
521 .tooltip({
522 let focus_handle = editor.focus_handle(cx);
523 move |cx| {
524 Tooltip::for_action_in(
525 "Discard Hunk",
526 &RevertSelectedHunks,
527 &focus_handle,
528 cx,
529 )
530 }
531 })
532 .on_click({
533 let editor = editor.clone();
534 let hunk = hunk.clone();
535 move |_event, cx| {
536 let multi_buffer =
537 editor.read(cx).buffer().clone();
538 let multi_buffer_snapshot =
539 multi_buffer.read(cx).snapshot(cx);
540 let mut revert_changes = HashMap::default();
541 if let Some(hunk) =
542 crate::hunk_diff::to_diff_hunk(
543 &hunk,
544 &multi_buffer_snapshot,
545 )
546 {
547 Editor::prepare_revert_change(
548 &mut revert_changes,
549 &multi_buffer,
550 &hunk,
551 cx,
552 );
553 }
554 if !revert_changes.is_empty() {
555 editor.update(cx, |editor, cx| {
556 editor.revert(revert_changes, cx)
557 });
558 }
559 }
560 }),
561 )
562 .child({
563 let focus = editor.focus_handle(cx);
564 PopoverMenu::new("hunk-controls-dropdown")
565 .trigger(
566 IconButton::new(
567 "toggle_editor_selections_icon",
568 IconName::EllipsisVertical,
569 )
570 .shape(IconButtonShape::Square)
571 .icon_size(IconSize::Small)
572 .style(ButtonStyle::Subtle)
573 .selected(
574 hunk_controls_menu_handle.is_deployed(),
575 )
576 .when(
577 !hunk_controls_menu_handle.is_deployed(),
578 |this| {
579 this.tooltip(|cx| {
580 Tooltip::text("Hunk Controls", cx)
581 })
582 },
583 ),
584 )
585 .anchor(AnchorCorner::TopRight)
586 .with_handle(hunk_controls_menu_handle)
587 .menu(move |cx| {
588 let focus = focus.clone();
589 let menu =
590 ContextMenu::build(cx, move |menu, _| {
591 menu.context(focus.clone()).action(
592 "Discard All",
593 RevertFile.boxed_clone(),
594 )
595 });
596 Some(menu)
597 })
598 }),
599 )
600 .child(
601 h_flex().gap_2().pr_6().child(
602 IconButton::new("collapse", IconName::Close)
603 .shape(IconButtonShape::Square)
604 .icon_size(IconSize::Small)
605 .tooltip({
606 let focus_handle = editor.focus_handle(cx);
607 move |cx| {
608 Tooltip::for_action_in(
609 "Collapse Hunk",
610 &ToggleHunkDiff,
611 &focus_handle,
612 cx,
613 )
614 }
615 })
616 .on_click({
617 let editor = editor.clone();
618 let hunk = hunk.clone();
619 move |_event, cx| {
620 editor.update(cx, |editor, cx| {
621 editor.toggle_hovered_hunk(&hunk, cx);
622 });
623 }
624 }),
625 ),
626 ),
627 )
628 .into_any_element()
629 }
630 }),
631 }
632 }
633
634 fn deleted_text_block(
635 hunk: &HoveredHunk,
636 diff_base_buffer: Model<Buffer>,
637 deleted_text_height: u32,
638 cx: &mut ViewContext<'_, Editor>,
639 ) -> BlockProperties<Anchor> {
640 let gutter_color = match hunk.status {
641 DiffHunkStatus::Added => unreachable!(),
642 DiffHunkStatus::Modified => cx.theme().status().modified,
643 DiffHunkStatus::Removed => cx.theme().status().deleted,
644 };
645 let deleted_hunk_color = deleted_hunk_color(cx);
646 let (editor_height, editor_with_deleted_text) =
647 editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
648 let editor = cx.view().clone();
649 let hunk = hunk.clone();
650 let height = editor_height.max(deleted_text_height);
651 BlockProperties {
652 position: hunk.multi_buffer_range.start,
653 height,
654 style: BlockStyle::Flex,
655 disposition: BlockDisposition::Above,
656 priority: 0,
657 render: Box::new(move |cx| {
658 let width = EditorElement::diff_hunk_strip_width(cx.line_height());
659 let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
660
661 h_flex()
662 .id(cx.block_id)
663 .bg(deleted_hunk_color)
664 .h(height as f32 * cx.line_height())
665 .w_full()
666 .child(
667 h_flex()
668 .id("gutter")
669 .max_w(gutter_dimensions.full_width())
670 .min_w(gutter_dimensions.full_width())
671 .size_full()
672 .child(
673 h_flex()
674 .id("gutter hunk")
675 .bg(gutter_color)
676 .pl(gutter_dimensions.margin
677 + gutter_dimensions
678 .git_blame_entries_width
679 .unwrap_or_default())
680 .max_w(width)
681 .min_w(width)
682 .size_full()
683 .cursor(CursorStyle::PointingHand)
684 .on_mouse_down(MouseButton::Left, {
685 let editor = editor.clone();
686 let hunk = hunk.clone();
687 move |_event, cx| {
688 editor.update(cx, |editor, cx| {
689 editor.toggle_hovered_hunk(&hunk, cx);
690 });
691 }
692 }),
693 ),
694 )
695 .child(editor_with_deleted_text.clone())
696 .into_any_element()
697 }),
698 }
699 }
700
701 pub(super) fn clear_clicked_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
702 self.expanded_hunks.hunk_update_tasks.clear();
703 self.clear_row_highlights::<DiffRowHighlight>();
704 let to_remove = self
705 .expanded_hunks
706 .hunks
707 .drain(..)
708 .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
709 .collect::<HashSet<_>>();
710 if to_remove.is_empty() {
711 false
712 } else {
713 self.remove_blocks(to_remove, None, cx);
714 true
715 }
716 }
717
718 pub(super) fn sync_expanded_diff_hunks(
719 &mut self,
720 buffer: Model<Buffer>,
721 cx: &mut ViewContext<'_, Self>,
722 ) {
723 let buffer_id = buffer.read(cx).remote_id();
724 let buffer_diff_base_version = buffer.read(cx).diff_base_version();
725 self.expanded_hunks
726 .hunk_update_tasks
727 .remove(&Some(buffer_id));
728 let diff_base_buffer = self.current_diff_base_buffer(&buffer, cx);
729 let new_sync_task = cx.spawn(move |editor, mut cx| async move {
730 let diff_base_buffer_unchanged = diff_base_buffer.is_some();
731 let Ok(diff_base_buffer) =
732 cx.update(|cx| diff_base_buffer.or_else(|| create_diff_base_buffer(&buffer, cx)))
733 else {
734 return;
735 };
736 editor
737 .update(&mut cx, |editor, cx| {
738 if let Some(diff_base_buffer) = &diff_base_buffer {
739 editor.expanded_hunks.diff_base.insert(
740 buffer_id,
741 DiffBaseBuffer {
742 buffer: diff_base_buffer.clone(),
743 diff_base_version: buffer_diff_base_version,
744 },
745 );
746 }
747
748 let snapshot = editor.snapshot(cx);
749 let mut recalculated_hunks = snapshot
750 .buffer_snapshot
751 .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
752 .filter(|hunk| hunk.buffer_id == buffer_id)
753 .fuse()
754 .peekable();
755 let mut highlights_to_remove =
756 Vec::with_capacity(editor.expanded_hunks.hunks.len());
757 let mut blocks_to_remove = HashSet::default();
758 let mut hunks_to_reexpand =
759 Vec::with_capacity(editor.expanded_hunks.hunks.len());
760 editor.expanded_hunks.hunks.retain_mut(|expanded_hunk| {
761 if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) {
762 return true;
763 };
764
765 let mut retain = false;
766 if diff_base_buffer_unchanged {
767 let expanded_hunk_display_range = expanded_hunk
768 .hunk_range
769 .start
770 .to_display_point(&snapshot)
771 .row()
772 ..expanded_hunk
773 .hunk_range
774 .end
775 .to_display_point(&snapshot)
776 .row();
777 while let Some(buffer_hunk) = recalculated_hunks.peek() {
778 match diff_hunk_to_display(buffer_hunk, &snapshot) {
779 DisplayDiffHunk::Folded { display_row } => {
780 recalculated_hunks.next();
781 if !expanded_hunk.folded
782 && expanded_hunk_display_range
783 .to_inclusive()
784 .contains(&display_row)
785 {
786 retain = true;
787 expanded_hunk.folded = true;
788 highlights_to_remove
789 .push(expanded_hunk.hunk_range.clone());
790 for block in expanded_hunk.blocks.drain(..) {
791 blocks_to_remove.insert(block);
792 }
793 break;
794 } else {
795 continue;
796 }
797 }
798 DisplayDiffHunk::Unfolded {
799 diff_base_byte_range,
800 display_row_range,
801 multi_buffer_range,
802 status,
803 } => {
804 let hunk_display_range = display_row_range;
805 if expanded_hunk_display_range.start
806 > hunk_display_range.end
807 {
808 recalculated_hunks.next();
809 continue;
810 } else if expanded_hunk_display_range.end
811 < hunk_display_range.start
812 {
813 break;
814 } else {
815 if !expanded_hunk.folded
816 && expanded_hunk_display_range == hunk_display_range
817 && expanded_hunk.status == hunk_status(buffer_hunk)
818 && expanded_hunk.diff_base_byte_range
819 == buffer_hunk.diff_base_byte_range
820 {
821 recalculated_hunks.next();
822 retain = true;
823 } else {
824 hunks_to_reexpand.push(HoveredHunk {
825 status,
826 multi_buffer_range,
827 diff_base_byte_range,
828 });
829 }
830 break;
831 }
832 }
833 }
834 }
835 }
836 if !retain {
837 blocks_to_remove.extend(expanded_hunk.blocks.drain(..));
838 highlights_to_remove.push(expanded_hunk.hunk_range.clone());
839 }
840 retain
841 });
842
843 editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
844 editor.remove_blocks(blocks_to_remove, None, cx);
845
846 if let Some(diff_base_buffer) = &diff_base_buffer {
847 for hunk in hunks_to_reexpand {
848 editor.expand_diff_hunk(Some(diff_base_buffer.clone()), &hunk, cx);
849 }
850 }
851 })
852 .ok();
853 });
854
855 self.expanded_hunks.hunk_update_tasks.insert(
856 Some(buffer_id),
857 cx.background_executor().spawn(new_sync_task),
858 );
859 }
860
861 fn current_diff_base_buffer(
862 &mut self,
863 buffer: &Model<Buffer>,
864 cx: &mut AppContext,
865 ) -> Option<Model<Buffer>> {
866 buffer.update(cx, |buffer, _| {
867 match self.expanded_hunks.diff_base.entry(buffer.remote_id()) {
868 hash_map::Entry::Occupied(o) => {
869 if o.get().diff_base_version != buffer.diff_base_version() {
870 o.remove();
871 None
872 } else {
873 Some(o.get().buffer.clone())
874 }
875 }
876 hash_map::Entry::Vacant(_) => None,
877 }
878 })
879 }
880}
881
882fn to_diff_hunk(
883 hovered_hunk: &HoveredHunk,
884 multi_buffer_snapshot: &MultiBufferSnapshot,
885) -> Option<MultiBufferDiffHunk> {
886 let buffer_id = hovered_hunk
887 .multi_buffer_range
888 .start
889 .buffer_id
890 .or(hovered_hunk.multi_buffer_range.end.buffer_id)?;
891 let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor
892 ..hovered_hunk.multi_buffer_range.end.text_anchor;
893 let point_range = hovered_hunk
894 .multi_buffer_range
895 .to_point(multi_buffer_snapshot);
896 Some(MultiBufferDiffHunk {
897 row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
898 buffer_id,
899 buffer_range,
900 diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
901 })
902}
903
904fn create_diff_base_buffer(buffer: &Model<Buffer>, cx: &mut AppContext) -> Option<Model<Buffer>> {
905 buffer
906 .update(cx, |buffer, _| {
907 let language = buffer.language().cloned();
908 let diff_base = buffer.diff_base()?.clone();
909 Some((buffer.line_ending(), diff_base, language))
910 })
911 .map(|(line_ending, diff_base, language)| {
912 cx.new_model(|cx| {
913 let buffer = Buffer::local_normalized(diff_base, line_ending, cx);
914 match language {
915 Some(language) => buffer.with_language(language, cx),
916 None => buffer,
917 }
918 })
919 })
920}
921
922fn added_hunk_color(cx: &AppContext) -> Hsla {
923 let mut created_color = cx.theme().status().git().created;
924 created_color.fade_out(0.7);
925 created_color
926}
927
928fn deleted_hunk_color(cx: &AppContext) -> Hsla {
929 let mut deleted_color = cx.theme().status().deleted;
930 deleted_color.fade_out(0.7);
931 deleted_color
932}
933
934fn editor_with_deleted_text(
935 diff_base_buffer: Model<Buffer>,
936 deleted_color: Hsla,
937 hunk: &HoveredHunk,
938 cx: &mut ViewContext<'_, Editor>,
939) -> (u32, View<Editor>) {
940 let parent_editor = cx.view().downgrade();
941 let editor = cx.new_view(|cx| {
942 let multi_buffer =
943 cx.new_model(|_| MultiBuffer::without_headers(language::Capability::ReadOnly));
944 multi_buffer.update(cx, |multi_buffer, cx| {
945 multi_buffer.push_excerpts(
946 diff_base_buffer,
947 Some(ExcerptRange {
948 context: hunk.diff_base_byte_range.clone(),
949 primary: None,
950 }),
951 cx,
952 );
953 });
954
955 let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
956 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
957 editor.set_show_wrap_guides(false, cx);
958 editor.set_show_gutter(false, cx);
959 editor.scroll_manager.set_forbid_vertical_scroll(true);
960 editor.set_read_only(true);
961 editor.set_show_inline_completions(Some(false), cx);
962 editor.highlight_rows::<DiffRowHighlight>(
963 Anchor::min()..=Anchor::max(),
964 deleted_color,
965 false,
966 cx,
967 );
968 editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
969 editor
970 ._subscriptions
971 .extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
972 editor.change_selections(None, cx, |s| {
973 s.try_cancel();
974 });
975 })]);
976
977 let parent_editor_for_reverts = parent_editor.clone();
978 let original_multi_buffer_range = hunk.multi_buffer_range.clone();
979 let diff_base_range = hunk.diff_base_byte_range.clone();
980 editor
981 .register_action::<RevertSelectedHunks>(move |_, cx| {
982 parent_editor_for_reverts
983 .update(cx, |editor, cx| {
984 let Some((buffer, original_text)) =
985 editor.buffer().update(cx, |buffer, cx| {
986 let (_, buffer, _) = buffer
987 .excerpt_containing(original_multi_buffer_range.start, cx)?;
988 let original_text =
989 buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
990 Some((buffer, Arc::from(original_text.to_string())))
991 })
992 else {
993 return;
994 };
995 buffer.update(cx, |buffer, cx| {
996 buffer.edit(
997 Some((
998 original_multi_buffer_range.start.text_anchor
999 ..original_multi_buffer_range.end.text_anchor,
1000 original_text,
1001 )),
1002 None,
1003 cx,
1004 )
1005 });
1006 })
1007 .ok();
1008 })
1009 .detach();
1010 let hunk = hunk.clone();
1011 editor
1012 .register_action::<ToggleHunkDiff>(move |_, cx| {
1013 parent_editor
1014 .update(cx, |editor, cx| {
1015 editor.toggle_hovered_hunk(&hunk, cx);
1016 })
1017 .ok();
1018 })
1019 .detach();
1020 editor
1021 });
1022
1023 let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0);
1024 (editor_height, editor)
1025}
1026
1027fn buffer_diff_hunk(
1028 buffer_snapshot: &MultiBufferSnapshot,
1029 row_range: Range<Point>,
1030) -> Option<MultiBufferDiffHunk> {
1031 let mut hunks = buffer_snapshot.git_diff_hunks_in_range(
1032 MultiBufferRow(row_range.start.row)..MultiBufferRow(row_range.end.row),
1033 );
1034 let hunk = hunks.next()?;
1035 let second_hunk = hunks.next();
1036 if second_hunk.is_none() {
1037 return Some(hunk);
1038 }
1039 None
1040}
1041
1042fn to_inclusive_row_range(
1043 row_range: Range<Anchor>,
1044 snapshot: &EditorSnapshot,
1045) -> RangeInclusive<Anchor> {
1046 let mut end = row_range.end.to_point(&snapshot.buffer_snapshot);
1047 if end.column == 0 && end.row > 0 {
1048 end = Point::new(
1049 end.row - 1,
1050 snapshot
1051 .buffer_snapshot
1052 .line_len(MultiBufferRow(end.row - 1)),
1053 );
1054 }
1055 row_range.start..=snapshot.buffer_snapshot.anchor_after(end)
1056}
1057
1058impl DisplayDiffHunk {
1059 pub fn start_display_row(&self) -> DisplayRow {
1060 match self {
1061 &DisplayDiffHunk::Folded { display_row } => display_row,
1062 DisplayDiffHunk::Unfolded {
1063 display_row_range, ..
1064 } => display_row_range.start,
1065 }
1066 }
1067
1068 pub fn contains_display_row(&self, display_row: DisplayRow) -> bool {
1069 let range = match self {
1070 &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
1071
1072 DisplayDiffHunk::Unfolded {
1073 display_row_range, ..
1074 } => display_row_range.start..=display_row_range.end,
1075 };
1076
1077 range.contains(&display_row)
1078 }
1079}
1080
1081pub fn diff_hunk_to_display(
1082 hunk: &MultiBufferDiffHunk,
1083 snapshot: &DisplaySnapshot,
1084) -> DisplayDiffHunk {
1085 let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
1086 let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0);
1087 let hunk_end_point_sub = Point::new(
1088 hunk.row_range
1089 .end
1090 .0
1091 .saturating_sub(1)
1092 .max(hunk.row_range.start.0),
1093 0,
1094 );
1095
1096 let status = hunk_status(hunk);
1097 let is_removal = status == DiffHunkStatus::Removed;
1098
1099 let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0);
1100 let folds_end = Point::new(hunk.row_range.end.0 + 2, 0);
1101 let folds_range = folds_start..folds_end;
1102
1103 let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
1104 let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
1105 let fold_point_range = fold_point_range.start..=fold_point_range.end;
1106
1107 let folded_start = fold_point_range.contains(&hunk_start_point);
1108 let folded_end = fold_point_range.contains(&hunk_end_point_sub);
1109 let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
1110
1111 (folded_start && folded_end) || (is_removal && folded_start_sub)
1112 });
1113
1114 if let Some(fold) = containing_fold {
1115 let row = fold.range.start.to_display_point(snapshot).row();
1116 DisplayDiffHunk::Folded { display_row: row }
1117 } else {
1118 let start = hunk_start_point.to_display_point(snapshot).row();
1119
1120 let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start);
1121 let hunk_end_point = Point::new(hunk_end_row.0, 0);
1122
1123 let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point);
1124 let multi_buffer_end = snapshot.buffer_snapshot.anchor_after(hunk_end_point);
1125 let end = hunk_end_point.to_display_point(snapshot).row();
1126
1127 DisplayDiffHunk::Unfolded {
1128 display_row_range: start..end,
1129 multi_buffer_range: multi_buffer_start..multi_buffer_end,
1130 status,
1131 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
1132 }
1133 }
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138 use super::*;
1139 use crate::{editor_tests::init_test, hunk_status};
1140 use gpui::{Context, TestAppContext};
1141 use language::Capability::ReadWrite;
1142 use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow};
1143 use project::{FakeFs, Project};
1144 use unindent::Unindent as _;
1145
1146 #[gpui::test]
1147 async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
1148 use git::diff::DiffHunkStatus;
1149 init_test(cx, |_| {});
1150
1151 let fs = FakeFs::new(cx.background_executor.clone());
1152 let project = Project::test(fs, [], cx).await;
1153
1154 // buffer has two modified hunks with two rows each
1155 let buffer_1 = project.update(cx, |project, cx| {
1156 project.create_local_buffer(
1157 "
1158 1.zero
1159 1.ONE
1160 1.TWO
1161 1.three
1162 1.FOUR
1163 1.FIVE
1164 1.six
1165 "
1166 .unindent()
1167 .as_str(),
1168 None,
1169 cx,
1170 )
1171 });
1172 buffer_1.update(cx, |buffer, cx| {
1173 buffer.set_diff_base(
1174 Some(
1175 "
1176 1.zero
1177 1.one
1178 1.two
1179 1.three
1180 1.four
1181 1.five
1182 1.six
1183 "
1184 .unindent(),
1185 ),
1186 cx,
1187 );
1188 });
1189
1190 // buffer has a deletion hunk and an insertion hunk
1191 let buffer_2 = project.update(cx, |project, cx| {
1192 project.create_local_buffer(
1193 "
1194 2.zero
1195 2.one
1196 2.two
1197 2.three
1198 2.four
1199 2.five
1200 2.six
1201 "
1202 .unindent()
1203 .as_str(),
1204 None,
1205 cx,
1206 )
1207 });
1208 buffer_2.update(cx, |buffer, cx| {
1209 buffer.set_diff_base(
1210 Some(
1211 "
1212 2.zero
1213 2.one
1214 2.one-and-a-half
1215 2.two
1216 2.three
1217 2.four
1218 2.six
1219 "
1220 .unindent(),
1221 ),
1222 cx,
1223 );
1224 });
1225
1226 cx.background_executor.run_until_parked();
1227
1228 let multibuffer = cx.new_model(|cx| {
1229 let mut multibuffer = MultiBuffer::new(ReadWrite);
1230 multibuffer.push_excerpts(
1231 buffer_1.clone(),
1232 [
1233 // excerpt ends in the middle of a modified hunk
1234 ExcerptRange {
1235 context: Point::new(0, 0)..Point::new(1, 5),
1236 primary: Default::default(),
1237 },
1238 // excerpt begins in the middle of a modified hunk
1239 ExcerptRange {
1240 context: Point::new(5, 0)..Point::new(6, 5),
1241 primary: Default::default(),
1242 },
1243 ],
1244 cx,
1245 );
1246 multibuffer.push_excerpts(
1247 buffer_2.clone(),
1248 [
1249 // excerpt ends at a deletion
1250 ExcerptRange {
1251 context: Point::new(0, 0)..Point::new(1, 5),
1252 primary: Default::default(),
1253 },
1254 // excerpt starts at a deletion
1255 ExcerptRange {
1256 context: Point::new(2, 0)..Point::new(2, 5),
1257 primary: Default::default(),
1258 },
1259 // excerpt fully contains a deletion hunk
1260 ExcerptRange {
1261 context: Point::new(1, 0)..Point::new(2, 5),
1262 primary: Default::default(),
1263 },
1264 // excerpt fully contains an insertion hunk
1265 ExcerptRange {
1266 context: Point::new(4, 0)..Point::new(6, 5),
1267 primary: Default::default(),
1268 },
1269 ],
1270 cx,
1271 );
1272 multibuffer
1273 });
1274
1275 let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
1276
1277 assert_eq!(
1278 snapshot.text(),
1279 "
1280 1.zero
1281 1.ONE
1282 1.FIVE
1283 1.six
1284 2.zero
1285 2.one
1286 2.two
1287 2.one
1288 2.two
1289 2.four
1290 2.five
1291 2.six"
1292 .unindent()
1293 );
1294
1295 let expected = [
1296 (
1297 DiffHunkStatus::Modified,
1298 MultiBufferRow(1)..MultiBufferRow(2),
1299 ),
1300 (
1301 DiffHunkStatus::Modified,
1302 MultiBufferRow(2)..MultiBufferRow(3),
1303 ),
1304 //TODO: Define better when and where removed hunks show up at range extremities
1305 (
1306 DiffHunkStatus::Removed,
1307 MultiBufferRow(6)..MultiBufferRow(6),
1308 ),
1309 (
1310 DiffHunkStatus::Removed,
1311 MultiBufferRow(8)..MultiBufferRow(8),
1312 ),
1313 (
1314 DiffHunkStatus::Added,
1315 MultiBufferRow(10)..MultiBufferRow(11),
1316 ),
1317 ];
1318
1319 assert_eq!(
1320 snapshot
1321 .git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12))
1322 .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1323 .collect::<Vec<_>>(),
1324 &expected,
1325 );
1326
1327 assert_eq!(
1328 snapshot
1329 .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12))
1330 .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1331 .collect::<Vec<_>>(),
1332 expected
1333 .iter()
1334 .rev()
1335 .cloned()
1336 .collect::<Vec<_>>()
1337 .as_slice(),
1338 );
1339 }
1340}