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