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