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_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.hunk_update_tasks.insert(
1093 Some(buffer_id),
1094 cx.background_executor().spawn(new_sync_task),
1095 );
1096 }
1097
1098 fn go_to_subsequent_hunk(
1099 &mut self,
1100 position: Anchor,
1101 window: &mut Window,
1102 cx: &mut Context<Self>,
1103 ) {
1104 let snapshot = self.snapshot(window, cx);
1105 let position = position.to_point(&snapshot.buffer_snapshot);
1106 if let Some(hunk) = self.go_to_hunk_after_position(&snapshot, position, window, cx) {
1107 let multi_buffer_start = snapshot
1108 .buffer_snapshot
1109 .anchor_before(Point::new(hunk.row_range.start.0, 0));
1110 let multi_buffer_end = snapshot
1111 .buffer_snapshot
1112 .anchor_after(Point::new(hunk.row_range.end.0, 0));
1113 self.expand_diff_hunk(
1114 None,
1115 &HoveredHunk {
1116 multi_buffer_range: multi_buffer_start..multi_buffer_end,
1117 status: hunk_status(&hunk),
1118 diff_base_byte_range: hunk.diff_base_byte_range,
1119 },
1120 window,
1121 cx,
1122 );
1123 }
1124 }
1125
1126 fn go_to_preceding_hunk(
1127 &mut self,
1128 position: Anchor,
1129 window: &mut Window,
1130 cx: &mut Context<Self>,
1131 ) {
1132 let snapshot = self.snapshot(window, cx);
1133 let position = position.to_point(&snapshot.buffer_snapshot);
1134 let hunk = self.go_to_hunk_before_position(&snapshot, position, window, cx);
1135 if let Some(hunk) = hunk {
1136 let multi_buffer_start = snapshot
1137 .buffer_snapshot
1138 .anchor_before(Point::new(hunk.row_range.start.0, 0));
1139 let multi_buffer_end = snapshot
1140 .buffer_snapshot
1141 .anchor_after(Point::new(hunk.row_range.end.0, 0));
1142 self.expand_diff_hunk(
1143 None,
1144 &HoveredHunk {
1145 multi_buffer_range: multi_buffer_start..multi_buffer_end,
1146 status: hunk_status(&hunk),
1147 diff_base_byte_range: hunk.diff_base_byte_range,
1148 },
1149 window,
1150 cx,
1151 );
1152 }
1153 }
1154}
1155
1156pub(crate) fn to_diff_hunk(
1157 hovered_hunk: &HoveredHunk,
1158 multi_buffer_snapshot: &MultiBufferSnapshot,
1159) -> Option<MultiBufferDiffHunk> {
1160 let buffer_id = hovered_hunk
1161 .multi_buffer_range
1162 .start
1163 .buffer_id
1164 .or(hovered_hunk.multi_buffer_range.end.buffer_id)?;
1165 let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor
1166 ..hovered_hunk.multi_buffer_range.end.text_anchor;
1167 let point_range = hovered_hunk
1168 .multi_buffer_range
1169 .to_point(multi_buffer_snapshot);
1170 Some(MultiBufferDiffHunk {
1171 row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
1172 buffer_id,
1173 buffer_range,
1174 diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
1175 })
1176}
1177
1178fn added_hunk_color(cx: &AppContext) -> Hsla {
1179 let mut created_color = cx.theme().status().git().created;
1180 created_color.fade_out(0.7);
1181 created_color
1182}
1183
1184fn deleted_hunk_color(cx: &AppContext) -> Hsla {
1185 let mut deleted_color = cx.theme().status().deleted;
1186 deleted_color.fade_out(0.7);
1187 deleted_color
1188}
1189
1190fn editor_with_deleted_text(
1191 diff_base_buffer: Model<Buffer>,
1192 deleted_color: Hsla,
1193 hunk: &HoveredHunk,
1194 window: &mut Window,
1195 cx: &mut Context<Editor>,
1196) -> (u32, Model<Editor>) {
1197 let parent_editor = cx.entity().downgrade();
1198 let editor = cx.new(|cx| {
1199 let multi_buffer = cx.new(|_| MultiBuffer::without_headers(language::Capability::ReadOnly));
1200 multi_buffer.update(cx, |multi_buffer, cx| {
1201 multi_buffer.push_excerpts(
1202 diff_base_buffer,
1203 Some(ExcerptRange {
1204 context: hunk.diff_base_byte_range.clone(),
1205 primary: None,
1206 }),
1207 cx,
1208 );
1209 });
1210
1211 let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx);
1212 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
1213 editor.set_show_wrap_guides(false, cx);
1214 editor.set_show_gutter(false, cx);
1215 editor.set_show_line_numbers(false, cx);
1216 editor.set_show_scrollbars(false, cx);
1217 editor.set_show_runnables(false, cx);
1218 editor.set_show_git_diff_gutter(false, cx);
1219 editor.set_show_code_actions(false, cx);
1220 editor.scroll_manager.set_forbid_vertical_scroll(true);
1221 editor.set_read_only(true);
1222 editor.set_show_inline_completions(Some(false), window, cx);
1223
1224 enum DeletedBlockRowHighlight {}
1225 editor.highlight_rows::<DeletedBlockRowHighlight>(
1226 Anchor::min()..Anchor::max(),
1227 deleted_color,
1228 false,
1229 cx,
1230 );
1231 editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
1232 editor._subscriptions.extend([cx.on_blur(
1233 &editor.focus_handle,
1234 window,
1235 |editor, window, cx| {
1236 editor.change_selections(None, window, cx, |s| {
1237 s.try_cancel();
1238 });
1239 },
1240 )]);
1241
1242 editor
1243 .register_action::<RevertSelectedHunks>({
1244 let hunk = hunk.clone();
1245 let parent_editor = parent_editor.clone();
1246 move |_, window, cx| {
1247 parent_editor
1248 .update(cx, |editor, cx| {
1249 editor.revert_hunk(hunk.clone(), window, cx)
1250 })
1251 .ok();
1252 }
1253 })
1254 .detach();
1255 editor
1256 .register_action::<ToggleHunkDiff>({
1257 let hunk = hunk.clone();
1258 move |_, window, cx| {
1259 parent_editor
1260 .update(cx, |editor, cx| {
1261 editor.toggle_hovered_hunk(&hunk, window, cx);
1262 })
1263 .ok();
1264 }
1265 })
1266 .detach();
1267 editor
1268 });
1269
1270 let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0);
1271 (editor_height, editor)
1272}
1273
1274impl DisplayDiffHunk {
1275 pub fn start_display_row(&self) -> DisplayRow {
1276 match self {
1277 &DisplayDiffHunk::Folded { display_row } => display_row,
1278 DisplayDiffHunk::Unfolded {
1279 display_row_range, ..
1280 } => display_row_range.start,
1281 }
1282 }
1283
1284 pub fn contains_display_row(&self, display_row: DisplayRow) -> bool {
1285 let range = match self {
1286 &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
1287
1288 DisplayDiffHunk::Unfolded {
1289 display_row_range, ..
1290 } => display_row_range.start..=display_row_range.end,
1291 };
1292
1293 range.contains(&display_row)
1294 }
1295}
1296
1297pub fn diff_hunk_to_display(
1298 hunk: &MultiBufferDiffHunk,
1299 snapshot: &DisplaySnapshot,
1300) -> DisplayDiffHunk {
1301 let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
1302 let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0);
1303 let hunk_end_point_sub = Point::new(
1304 hunk.row_range
1305 .end
1306 .0
1307 .saturating_sub(1)
1308 .max(hunk.row_range.start.0),
1309 0,
1310 );
1311
1312 let status = hunk_status(hunk);
1313 let is_removal = status == DiffHunkStatus::Removed;
1314
1315 let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0);
1316 let folds_end = Point::new(hunk.row_range.end.0 + 2, 0);
1317 let folds_range = folds_start..folds_end;
1318
1319 let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
1320 let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
1321 let fold_point_range = fold_point_range.start..=fold_point_range.end;
1322
1323 let folded_start = fold_point_range.contains(&hunk_start_point);
1324 let folded_end = fold_point_range.contains(&hunk_end_point_sub);
1325 let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
1326
1327 (folded_start && folded_end) || (is_removal && folded_start_sub)
1328 });
1329
1330 if let Some(fold) = containing_fold {
1331 let row = fold.range.start.to_display_point(snapshot).row();
1332 DisplayDiffHunk::Folded { display_row: row }
1333 } else {
1334 let start = hunk_start_point.to_display_point(snapshot).row();
1335
1336 let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start);
1337 let hunk_end_point = Point::new(hunk_end_row.0, 0);
1338
1339 let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point);
1340 let multi_buffer_end = snapshot
1341 .buffer_snapshot
1342 .anchor_in_excerpt(multi_buffer_start.excerpt_id, hunk.buffer_range.end)
1343 .unwrap();
1344 let end = hunk_end_point.to_display_point(snapshot).row();
1345
1346 DisplayDiffHunk::Unfolded {
1347 display_row_range: start..end,
1348 multi_buffer_range: multi_buffer_start..multi_buffer_end,
1349 status,
1350 diff_base_byte_range: hunk.diff_base_byte_range.clone(),
1351 }
1352 }
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357 use super::*;
1358 use crate::{editor_tests::init_test, hunk_status};
1359 use gpui::{Context, TestAppContext};
1360 use language::Capability::ReadWrite;
1361 use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow};
1362 use project::{FakeFs, Project};
1363 use unindent::Unindent as _;
1364
1365 #[gpui::test]
1366 async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
1367 use git::diff::DiffHunkStatus;
1368 init_test(cx, |_| {});
1369
1370 let fs = FakeFs::new(cx.background_executor.clone());
1371 let project = Project::test(fs, [], cx).await;
1372
1373 // buffer has two modified hunks with two rows each
1374 let diff_base_1 = "
1375 1.zero
1376 1.one
1377 1.two
1378 1.three
1379 1.four
1380 1.five
1381 1.six
1382 "
1383 .unindent();
1384
1385 let text_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 // buffer has a deletion hunk and an insertion hunk
1397 let diff_base_2 = "
1398 2.zero
1399 2.one
1400 2.one-and-a-half
1401 2.two
1402 2.three
1403 2.four
1404 2.six
1405 "
1406 .unindent();
1407
1408 let text_2 = "
1409 2.zero
1410 2.one
1411 2.two
1412 2.three
1413 2.four
1414 2.five
1415 2.six
1416 "
1417 .unindent();
1418
1419 let buffer_1 = project.update(cx, |project, cx| {
1420 project.create_local_buffer(text_1.as_str(), None, cx)
1421 });
1422 let buffer_2 = project.update(cx, |project, cx| {
1423 project.create_local_buffer(text_2.as_str(), None, cx)
1424 });
1425
1426 let multibuffer = cx.new(|cx| {
1427 let mut multibuffer = MultiBuffer::new(ReadWrite);
1428 multibuffer.push_excerpts(
1429 buffer_1.clone(),
1430 [
1431 // excerpt ends in the middle of a modified hunk
1432 ExcerptRange {
1433 context: Point::new(0, 0)..Point::new(1, 5),
1434 primary: Default::default(),
1435 },
1436 // excerpt begins in the middle of a modified hunk
1437 ExcerptRange {
1438 context: Point::new(5, 0)..Point::new(6, 5),
1439 primary: Default::default(),
1440 },
1441 ],
1442 cx,
1443 );
1444 multibuffer.push_excerpts(
1445 buffer_2.clone(),
1446 [
1447 // excerpt ends at a deletion
1448 ExcerptRange {
1449 context: Point::new(0, 0)..Point::new(1, 5),
1450 primary: Default::default(),
1451 },
1452 // excerpt starts at a deletion
1453 ExcerptRange {
1454 context: Point::new(2, 0)..Point::new(2, 5),
1455 primary: Default::default(),
1456 },
1457 // excerpt fully contains a deletion hunk
1458 ExcerptRange {
1459 context: Point::new(1, 0)..Point::new(2, 5),
1460 primary: Default::default(),
1461 },
1462 // excerpt fully contains an insertion hunk
1463 ExcerptRange {
1464 context: Point::new(4, 0)..Point::new(6, 5),
1465 primary: Default::default(),
1466 },
1467 ],
1468 cx,
1469 );
1470 multibuffer
1471 });
1472
1473 let editor = cx
1474 .add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, false, window, cx));
1475 editor
1476 .update(cx, |editor, window, cx| {
1477 for (buffer, diff_base) in [
1478 (buffer_1.clone(), diff_base_1),
1479 (buffer_2.clone(), diff_base_2),
1480 ] {
1481 let diff = cx.new(|cx| {
1482 BufferChangeSet::new_with_base_text(
1483 diff_base.to_string(),
1484 buffer.read(cx).text_snapshot(),
1485 cx,
1486 )
1487 });
1488 editor.diff_map.add_diff(diff, window, cx)
1489 }
1490 })
1491 .unwrap();
1492 cx.background_executor.run_until_parked();
1493
1494 let snapshot = editor
1495 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1496 .unwrap();
1497
1498 assert_eq!(
1499 snapshot.buffer_snapshot.text(),
1500 "
1501 1.zero
1502 1.ONE
1503 1.FIVE
1504 1.six
1505 2.zero
1506 2.one
1507 2.two
1508 2.one
1509 2.two
1510 2.four
1511 2.five
1512 2.six"
1513 .unindent()
1514 );
1515
1516 let expected = [
1517 (
1518 DiffHunkStatus::Modified,
1519 MultiBufferRow(1)..MultiBufferRow(2),
1520 ),
1521 (
1522 DiffHunkStatus::Modified,
1523 MultiBufferRow(2)..MultiBufferRow(3),
1524 ),
1525 //TODO: Define better when and where removed hunks show up at range extremities
1526 (
1527 DiffHunkStatus::Removed,
1528 MultiBufferRow(6)..MultiBufferRow(6),
1529 ),
1530 (
1531 DiffHunkStatus::Removed,
1532 MultiBufferRow(8)..MultiBufferRow(8),
1533 ),
1534 (
1535 DiffHunkStatus::Added,
1536 MultiBufferRow(10)..MultiBufferRow(11),
1537 ),
1538 ];
1539
1540 assert_eq!(
1541 snapshot
1542 .diff_map
1543 .diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot)
1544 .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1545 .collect::<Vec<_>>(),
1546 &expected,
1547 );
1548
1549 assert_eq!(
1550 snapshot
1551 .diff_map
1552 .diff_hunks_in_range_rev(
1553 Point::zero()..Point::new(12, 0),
1554 &snapshot.buffer_snapshot
1555 )
1556 .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1557 .collect::<Vec<_>>(),
1558 expected
1559 .iter()
1560 .rev()
1561 .cloned()
1562 .collect::<Vec<_>>()
1563 .as_slice(),
1564 );
1565 }
1566}