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