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