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