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