hunk_diff.rs

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