1use collections::{HashMap, HashSet};
2use git::diff::DiffHunkStatus;
3use gpui::{
4 Action, AppContext as _, 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_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_with_tooltip(
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 Tooltip::simple("Hunk Controls", cx),
779 )
780 .anchor(Corner::TopRight)
781 .with_handle(hunk_controls_menu_handle)
782 .menu(move |window, cx| {
783 let focus = focus.clone();
784 let menu = ContextMenu::build(
785 window,
786 cx,
787 move |menu, _, _| {
788 menu.context(focus.clone())
789 .action(
790 "Discard All Hunks",
791 RevertFile
792 .boxed_clone(),
793 )
794 },
795 );
796 Some(menu)
797 })
798 })
799 }
800 }),
801 )
802 .when(!is_branch_buffer, |div| {
803 div.child(
804 IconButton::new("collapse", IconName::Close)
805 .shape(IconButtonShape::Square)
806 .icon_size(IconSize::Small)
807 .tooltip({
808 let focus_handle = editor.focus_handle(cx);
809 move |window, cx| {
810 Tooltip::for_action_in(
811 "Collapse Hunk",
812 &ToggleHunkDiff,
813 &focus_handle,
814 window,
815 cx,
816 )
817 }
818 })
819 .on_click({
820 let editor = editor.clone();
821 let hunk = hunk.clone();
822 move |_event, window, cx| {
823 editor.update(cx, |editor, cx| {
824 editor
825 .toggle_hovered_hunk(&hunk, window, cx);
826 });
827 }
828 }),
829 )
830 }),
831 )
832 .into_any_element()
833 }
834 }),
835 }
836 }
837
838 fn deleted_text_block(
839 hunk: &HoveredHunk,
840 diff_base_buffer: Model<Buffer>,
841 deleted_text_height: u32,
842 window: &mut Window,
843 cx: &mut Context<Editor>,
844 ) -> BlockProperties<Anchor> {
845 let gutter_color = match hunk.status {
846 DiffHunkStatus::Added => unreachable!(),
847 DiffHunkStatus::Modified => cx.theme().status().modified,
848 DiffHunkStatus::Removed => cx.theme().status().deleted,
849 };
850 let deleted_hunk_color = deleted_hunk_color(cx);
851 let (editor_height, editor_with_deleted_text) =
852 editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, window, cx);
853 let editor = cx.entity().clone();
854 let hunk = hunk.clone();
855 let height = editor_height.max(deleted_text_height);
856 BlockProperties {
857 placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
858 height,
859 style: BlockStyle::Flex,
860 priority: 0,
861 render: Arc::new(move |cx| {
862 let width = EditorElement::diff_hunk_strip_width(cx.window.line_height());
863 let gutter_dimensions = editor.read(cx.app).gutter_dimensions;
864
865 h_flex()
866 .id(cx.block_id)
867 .block_mouse_down()
868 .bg(deleted_hunk_color)
869 .h(height as f32 * cx.window.line_height())
870 .w_full()
871 .child(
872 h_flex()
873 .id("gutter")
874 .max_w(gutter_dimensions.full_width())
875 .min_w(gutter_dimensions.full_width())
876 .size_full()
877 .child(
878 h_flex()
879 .id("gutter hunk")
880 .bg(gutter_color)
881 .pl(gutter_dimensions.margin
882 + gutter_dimensions
883 .git_blame_entries_width
884 .unwrap_or_default())
885 .max_w(width)
886 .min_w(width)
887 .size_full()
888 .cursor(CursorStyle::PointingHand)
889 .on_mouse_down(MouseButton::Left, {
890 let editor = editor.clone();
891 let hunk = hunk.clone();
892 move |_event, window, cx| {
893 editor.update(cx, |editor, cx| {
894 editor.toggle_hovered_hunk(&hunk, window, cx);
895 });
896 }
897 }),
898 ),
899 )
900 .child(editor_with_deleted_text.clone())
901 .into_any_element()
902 }),
903 }
904 }
905
906 pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut Context<Editor>) -> bool {
907 if self.diff_map.expand_all {
908 return false;
909 }
910 self.diff_map.hunk_update_tasks.clear();
911 self.clear_row_highlights::<DiffRowHighlight>();
912 let to_remove = self
913 .diff_map
914 .hunks
915 .drain(..)
916 .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
917 .collect::<HashSet<_>>();
918 if to_remove.is_empty() {
919 false
920 } else {
921 self.remove_blocks(to_remove, None, cx);
922 true
923 }
924 }
925
926 pub(super) fn sync_expanded_diff_hunks(
927 diff_map: &mut DiffMap,
928 buffer_id: BufferId,
929 window: &mut Window,
930 cx: &mut Context<Self>,
931 ) {
932 let diff_base_state = diff_map.diff_bases.get_mut(&buffer_id);
933 let mut diff_base_buffer = None;
934 let mut diff_base_buffer_unchanged = true;
935 if let Some(diff_base_state) = diff_base_state {
936 diff_base_state.diff.update(cx, |diff, _| {
937 if diff_base_state.last_version != Some(diff.base_text_version) {
938 diff_base_state.last_version = Some(diff.base_text_version);
939 diff_base_buffer_unchanged = false;
940 }
941 diff_base_buffer = diff.base_text.clone();
942 })
943 }
944
945 diff_map.hunk_update_tasks.remove(&Some(buffer_id));
946
947 let new_sync_task = cx.spawn_in(window, move |editor, mut cx| async move {
948 editor
949 .update_in(&mut cx, |editor, window, cx| {
950 let snapshot = editor.snapshot(window, cx);
951 let mut recalculated_hunks = snapshot
952 .diff_map
953 .diff_hunks(&snapshot.buffer_snapshot)
954 .filter(|hunk| hunk.buffer_id == buffer_id)
955 .fuse()
956 .peekable();
957 let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len());
958 let mut blocks_to_remove = HashSet::default();
959 let mut hunks_to_reexpand = Vec::with_capacity(editor.diff_map.hunks.len());
960 editor.diff_map.hunks.retain_mut(|expanded_hunk| {
961 if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) {
962 return true;
963 };
964
965 let mut retain = false;
966 if diff_base_buffer_unchanged {
967 let expanded_hunk_display_range = expanded_hunk
968 .hunk_range
969 .start
970 .to_display_point(&snapshot)
971 .row()
972 ..expanded_hunk
973 .hunk_range
974 .end
975 .to_display_point(&snapshot)
976 .row();
977 while let Some(buffer_hunk) = recalculated_hunks.peek() {
978 match diff_hunk_to_display(buffer_hunk, &snapshot) {
979 DisplayDiffHunk::Folded { display_row } => {
980 recalculated_hunks.next();
981 if !expanded_hunk.folded
982 && expanded_hunk_display_range
983 .to_inclusive()
984 .contains(&display_row)
985 {
986 retain = true;
987 expanded_hunk.folded = true;
988 highlights_to_remove
989 .push(expanded_hunk.hunk_range.clone());
990 for block in expanded_hunk.blocks.drain(..) {
991 blocks_to_remove.insert(block);
992 }
993 break;
994 } else {
995 continue;
996 }
997 }
998 DisplayDiffHunk::Unfolded {
999 diff_base_byte_range,
1000 display_row_range,
1001 multi_buffer_range,
1002 status,
1003 } => {
1004 let hunk_display_range = display_row_range;
1005
1006 if expanded_hunk_display_range.start
1007 > hunk_display_range.end
1008 {
1009 recalculated_hunks.next();
1010 if editor.diff_map.expand_all {
1011 hunks_to_reexpand.push(HoveredHunk {
1012 status,
1013 multi_buffer_range,
1014 diff_base_byte_range,
1015 });
1016 }
1017 continue;
1018 }
1019
1020 if expanded_hunk_display_range.end
1021 < hunk_display_range.start
1022 {
1023 break;
1024 }
1025
1026 if !expanded_hunk.folded
1027 && expanded_hunk_display_range == hunk_display_range
1028 && expanded_hunk.status == hunk_status(buffer_hunk)
1029 && expanded_hunk.diff_base_byte_range
1030 == buffer_hunk.diff_base_byte_range
1031 {
1032 recalculated_hunks.next();
1033 retain = true;
1034 } else {
1035 hunks_to_reexpand.push(HoveredHunk {
1036 status,
1037 multi_buffer_range,
1038 diff_base_byte_range,
1039 });
1040 }
1041 break;
1042 }
1043 }
1044 }
1045 }
1046 if !retain {
1047 blocks_to_remove.extend(expanded_hunk.blocks.drain(..));
1048 highlights_to_remove.push(expanded_hunk.hunk_range.clone());
1049 }
1050 retain
1051 });
1052
1053 if editor.diff_map.expand_all {
1054 for hunk in recalculated_hunks {
1055 match diff_hunk_to_display(&hunk, &snapshot) {
1056 DisplayDiffHunk::Folded { .. } => {}
1057 DisplayDiffHunk::Unfolded {
1058 diff_base_byte_range,
1059 multi_buffer_range,
1060 status,
1061 ..
1062 } => {
1063 hunks_to_reexpand.push(HoveredHunk {
1064 status,
1065 multi_buffer_range,
1066 diff_base_byte_range,
1067 });
1068 }
1069 }
1070 }
1071 } else {
1072 drop(recalculated_hunks);
1073 }
1074
1075 editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
1076 editor.remove_blocks(blocks_to_remove, None, cx);
1077
1078 if let Some(diff_base_buffer) = &diff_base_buffer {
1079 for hunk in hunks_to_reexpand {
1080 editor.expand_diff_hunk(
1081 Some(diff_base_buffer.clone()),
1082 &hunk,
1083 window,
1084 cx,
1085 );
1086 }
1087 }
1088 })
1089 .ok();
1090 });
1091
1092 diff_map
1093 .hunk_update_tasks
1094 .insert(Some(buffer_id), cx.background_spawn(new_sync_task));
1095 }
1096
1097 fn go_to_subsequent_hunk(
1098 &mut self,
1099 position: Anchor,
1100 window: &mut Window,
1101 cx: &mut Context<Self>,
1102 ) {
1103 let snapshot = self.snapshot(window, cx);
1104 let position = position.to_point(&snapshot.buffer_snapshot);
1105 if let Some(hunk) = self.go_to_hunk_after_position(&snapshot, position, window, cx) {
1106 let multi_buffer_start = snapshot
1107 .buffer_snapshot
1108 .anchor_before(Point::new(hunk.row_range.start.0, 0));
1109 let multi_buffer_end = snapshot
1110 .buffer_snapshot
1111 .anchor_after(Point::new(hunk.row_range.end.0, 0));
1112 self.expand_diff_hunk(
1113 None,
1114 &HoveredHunk {
1115 multi_buffer_range: multi_buffer_start..multi_buffer_end,
1116 status: hunk_status(&hunk),
1117 diff_base_byte_range: hunk.diff_base_byte_range,
1118 },
1119 window,
1120 cx,
1121 );
1122 }
1123 }
1124
1125 fn go_to_preceding_hunk(
1126 &mut self,
1127 position: Anchor,
1128 window: &mut Window,
1129 cx: &mut Context<Self>,
1130 ) {
1131 let snapshot = self.snapshot(window, cx);
1132 let position = position.to_point(&snapshot.buffer_snapshot);
1133 let hunk = self.go_to_hunk_before_position(&snapshot, position, window, cx);
1134 if let Some(hunk) = hunk {
1135 let multi_buffer_start = snapshot
1136 .buffer_snapshot
1137 .anchor_before(Point::new(hunk.row_range.start.0, 0));
1138 let multi_buffer_end = snapshot
1139 .buffer_snapshot
1140 .anchor_after(Point::new(hunk.row_range.end.0, 0));
1141 self.expand_diff_hunk(
1142 None,
1143 &HoveredHunk {
1144 multi_buffer_range: multi_buffer_start..multi_buffer_end,
1145 status: hunk_status(&hunk),
1146 diff_base_byte_range: hunk.diff_base_byte_range,
1147 },
1148 window,
1149 cx,
1150 );
1151 }
1152 }
1153}
1154
1155pub(crate) fn to_diff_hunk(
1156 hovered_hunk: &HoveredHunk,
1157 multi_buffer_snapshot: &MultiBufferSnapshot,
1158) -> Option<MultiBufferDiffHunk> {
1159 let buffer_id = hovered_hunk
1160 .multi_buffer_range
1161 .start
1162 .buffer_id
1163 .or(hovered_hunk.multi_buffer_range.end.buffer_id)?;
1164 let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor
1165 ..hovered_hunk.multi_buffer_range.end.text_anchor;
1166 let point_range = hovered_hunk
1167 .multi_buffer_range
1168 .to_point(multi_buffer_snapshot);
1169 Some(MultiBufferDiffHunk {
1170 row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
1171 buffer_id,
1172 buffer_range,
1173 diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
1174 })
1175}
1176
1177fn added_hunk_color(cx: &AppContext) -> Hsla {
1178 let mut created_color = cx.theme().status().git().created;
1179 created_color.fade_out(0.7);
1180 created_color
1181}
1182
1183fn deleted_hunk_color(cx: &AppContext) -> Hsla {
1184 let mut deleted_color = cx.theme().status().deleted;
1185 deleted_color.fade_out(0.7);
1186 deleted_color
1187}
1188
1189fn editor_with_deleted_text(
1190 diff_base_buffer: Model<Buffer>,
1191 deleted_color: Hsla,
1192 hunk: &HoveredHunk,
1193 window: &mut Window,
1194 cx: &mut Context<Editor>,
1195) -> (u32, Model<Editor>) {
1196 let parent_editor = cx.entity().downgrade();
1197 let editor = cx.new(|cx| {
1198 let multi_buffer = cx.new(|_| MultiBuffer::without_headers(language::Capability::ReadOnly));
1199 multi_buffer.update(cx, |multi_buffer, cx| {
1200 multi_buffer.push_excerpts(
1201 diff_base_buffer,
1202 Some(ExcerptRange {
1203 context: hunk.diff_base_byte_range.clone(),
1204 primary: None,
1205 }),
1206 cx,
1207 );
1208 });
1209
1210 let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx);
1211 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
1212 editor.set_show_wrap_guides(false, cx);
1213 editor.set_show_gutter(false, cx);
1214 editor.set_show_line_numbers(false, cx);
1215 editor.set_show_scrollbars(false, cx);
1216 editor.set_show_runnables(false, cx);
1217 editor.set_show_git_diff_gutter(false, cx);
1218 editor.set_show_code_actions(false, cx);
1219 editor.scroll_manager.set_forbid_vertical_scroll(true);
1220 editor.set_read_only(true);
1221 editor.set_show_inline_completions(Some(false), window, cx);
1222
1223 enum DeletedBlockRowHighlight {}
1224 editor.highlight_rows::<DeletedBlockRowHighlight>(
1225 Anchor::min()..Anchor::max(),
1226 deleted_color,
1227 false,
1228 cx,
1229 );
1230 editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
1231 editor._subscriptions.extend([cx.on_blur(
1232 &editor.focus_handle,
1233 window,
1234 |editor, window, cx| {
1235 editor.change_selections(None, window, cx, |s| {
1236 s.try_cancel();
1237 });
1238 },
1239 )]);
1240
1241 editor
1242 .register_action::<RevertSelectedHunks>({
1243 let hunk = hunk.clone();
1244 let parent_editor = parent_editor.clone();
1245 move |_, window, cx| {
1246 parent_editor
1247 .update(cx, |editor, cx| {
1248 editor.revert_hunk(hunk.clone(), window, cx)
1249 })
1250 .ok();
1251 }
1252 })
1253 .detach();
1254 editor
1255 .register_action::<ToggleHunkDiff>({
1256 let hunk = hunk.clone();
1257 move |_, window, cx| {
1258 parent_editor
1259 .update(cx, |editor, cx| {
1260 editor.toggle_hovered_hunk(&hunk, window, cx);
1261 })
1262 .ok();
1263 }
1264 })
1265 .detach();
1266 editor
1267 });
1268
1269 let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0);
1270 (editor_height, editor)
1271}
1272
1273impl DisplayDiffHunk {
1274 pub fn start_display_row(&self) -> DisplayRow {
1275 match self {
1276 &DisplayDiffHunk::Folded { display_row } => display_row,
1277 DisplayDiffHunk::Unfolded {
1278 display_row_range, ..
1279 } => display_row_range.start,
1280 }
1281 }
1282
1283 pub fn contains_display_row(&self, display_row: DisplayRow) -> bool {
1284 let range = match self {
1285 &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
1286
1287 DisplayDiffHunk::Unfolded {
1288 display_row_range, ..
1289 } => display_row_range.start..=display_row_range.end,
1290 };
1291
1292 range.contains(&display_row)
1293 }
1294}
1295
1296pub fn diff_hunk_to_display(
1297 hunk: &MultiBufferDiffHunk,
1298 snapshot: &DisplaySnapshot,
1299) -> DisplayDiffHunk {
1300 let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
1301 let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0);
1302 let hunk_end_point_sub = Point::new(
1303 hunk.row_range
1304 .end
1305 .0
1306 .saturating_sub(1)
1307 .max(hunk.row_range.start.0),
1308 0,
1309 );
1310
1311 let status = hunk_status(hunk);
1312 let is_removal = status == DiffHunkStatus::Removed;
1313
1314 let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0);
1315 let folds_end = Point::new(hunk.row_range.end.0 + 2, 0);
1316 let folds_range = folds_start..folds_end;
1317
1318 let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
1319 let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
1320 let fold_point_range = fold_point_range.start..=fold_point_range.end;
1321
1322 let folded_start = fold_point_range.contains(&hunk_start_point);
1323 let folded_end = fold_point_range.contains(&hunk_end_point_sub);
1324 let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
1325
1326 (folded_start && folded_end) || (is_removal && folded_start_sub)
1327 });
1328
1329 if let Some(fold) = containing_fold {
1330 let row = fold.range.start.to_display_point(snapshot).row();
1331 DisplayDiffHunk::Folded { display_row: row }
1332 } else {
1333 let start = hunk_start_point.to_display_point(snapshot).row();
1334
1335 let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start);
1336 let hunk_end_point = Point::new(hunk_end_row.0, 0);
1337
1338 let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point);
1339 let multi_buffer_end = snapshot
1340 .buffer_snapshot
1341 .anchor_in_excerpt(multi_buffer_start.excerpt_id, hunk.buffer_range.end)
1342 .unwrap();
1343 let end = hunk_end_point.to_display_point(snapshot).row();
1344
1345 DisplayDiffHunk::Unfolded {
1346 display_row_range: start..end,
1347 multi_buffer_range: multi_buffer_start..multi_buffer_end,
1348 status,
1349 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
1350 }
1351 }
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356 use super::*;
1357 use crate::{editor_tests::init_test, hunk_status};
1358 use gpui::{Context, TestAppContext};
1359 use language::Capability::ReadWrite;
1360 use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow};
1361 use project::{FakeFs, Project};
1362 use unindent::Unindent as _;
1363
1364 #[gpui::test]
1365 async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
1366 use git::diff::DiffHunkStatus;
1367 init_test(cx, |_| {});
1368
1369 let fs = FakeFs::new(cx.background_executor.clone());
1370 let project = Project::test(fs, [], cx).await;
1371
1372 // buffer has two modified hunks with two rows each
1373 let diff_base_1 = "
1374 1.zero
1375 1.one
1376 1.two
1377 1.three
1378 1.four
1379 1.five
1380 1.six
1381 "
1382 .unindent();
1383
1384 let text_1 = "
1385 1.zero
1386 1.ONE
1387 1.TWO
1388 1.three
1389 1.FOUR
1390 1.FIVE
1391 1.six
1392 "
1393 .unindent();
1394
1395 // buffer has a deletion hunk and an insertion hunk
1396 let diff_base_2 = "
1397 2.zero
1398 2.one
1399 2.one-and-a-half
1400 2.two
1401 2.three
1402 2.four
1403 2.six
1404 "
1405 .unindent();
1406
1407 let text_2 = "
1408 2.zero
1409 2.one
1410 2.two
1411 2.three
1412 2.four
1413 2.five
1414 2.six
1415 "
1416 .unindent();
1417
1418 let buffer_1 = project.update(cx, |project, cx| {
1419 project.create_local_buffer(text_1.as_str(), None, cx)
1420 });
1421 let buffer_2 = project.update(cx, |project, cx| {
1422 project.create_local_buffer(text_2.as_str(), None, cx)
1423 });
1424
1425 let multibuffer = cx.new(|cx| {
1426 let mut multibuffer = MultiBuffer::new(ReadWrite);
1427 multibuffer.push_excerpts(
1428 buffer_1.clone(),
1429 [
1430 // excerpt ends in the middle of a modified hunk
1431 ExcerptRange {
1432 context: Point::new(0, 0)..Point::new(1, 5),
1433 primary: Default::default(),
1434 },
1435 // excerpt begins in the middle of a modified hunk
1436 ExcerptRange {
1437 context: Point::new(5, 0)..Point::new(6, 5),
1438 primary: Default::default(),
1439 },
1440 ],
1441 cx,
1442 );
1443 multibuffer.push_excerpts(
1444 buffer_2.clone(),
1445 [
1446 // excerpt ends at a deletion
1447 ExcerptRange {
1448 context: Point::new(0, 0)..Point::new(1, 5),
1449 primary: Default::default(),
1450 },
1451 // excerpt starts at a deletion
1452 ExcerptRange {
1453 context: Point::new(2, 0)..Point::new(2, 5),
1454 primary: Default::default(),
1455 },
1456 // excerpt fully contains a deletion hunk
1457 ExcerptRange {
1458 context: Point::new(1, 0)..Point::new(2, 5),
1459 primary: Default::default(),
1460 },
1461 // excerpt fully contains an insertion hunk
1462 ExcerptRange {
1463 context: Point::new(4, 0)..Point::new(6, 5),
1464 primary: Default::default(),
1465 },
1466 ],
1467 cx,
1468 );
1469 multibuffer
1470 });
1471
1472 let editor = cx
1473 .add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, false, window, cx));
1474 editor
1475 .update(cx, |editor, window, cx| {
1476 for (buffer, diff_base) in [
1477 (buffer_1.clone(), diff_base_1),
1478 (buffer_2.clone(), diff_base_2),
1479 ] {
1480 let diff = cx.new(|cx| {
1481 BufferChangeSet::new_with_base_text(
1482 diff_base.to_string(),
1483 buffer.read(cx).text_snapshot(),
1484 cx,
1485 )
1486 });
1487 editor.diff_map.add_diff(diff, window, cx)
1488 }
1489 })
1490 .unwrap();
1491 cx.background_executor.run_until_parked();
1492
1493 let snapshot = editor
1494 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1495 .unwrap();
1496
1497 assert_eq!(
1498 snapshot.buffer_snapshot.text(),
1499 "
1500 1.zero
1501 1.ONE
1502 1.FIVE
1503 1.six
1504 2.zero
1505 2.one
1506 2.two
1507 2.one
1508 2.two
1509 2.four
1510 2.five
1511 2.six"
1512 .unindent()
1513 );
1514
1515 let expected = [
1516 (
1517 DiffHunkStatus::Modified,
1518 MultiBufferRow(1)..MultiBufferRow(2),
1519 ),
1520 (
1521 DiffHunkStatus::Modified,
1522 MultiBufferRow(2)..MultiBufferRow(3),
1523 ),
1524 //TODO: Define better when and where removed hunks show up at range extremities
1525 (
1526 DiffHunkStatus::Removed,
1527 MultiBufferRow(6)..MultiBufferRow(6),
1528 ),
1529 (
1530 DiffHunkStatus::Removed,
1531 MultiBufferRow(8)..MultiBufferRow(8),
1532 ),
1533 (
1534 DiffHunkStatus::Added,
1535 MultiBufferRow(10)..MultiBufferRow(11),
1536 ),
1537 ];
1538
1539 assert_eq!(
1540 snapshot
1541 .diff_map
1542 .diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot)
1543 .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1544 .collect::<Vec<_>>(),
1545 &expected,
1546 );
1547
1548 assert_eq!(
1549 snapshot
1550 .diff_map
1551 .diff_hunks_in_range_rev(
1552 Point::zero()..Point::new(12, 0),
1553 &snapshot.buffer_snapshot
1554 )
1555 .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1556 .collect::<Vec<_>>(),
1557 expected
1558 .iter()
1559 .rev()
1560 .cloned()
1561 .collect::<Vec<_>>()
1562 .as_slice(),
1563 );
1564 }
1565}