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