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