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