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