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