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