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