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