hunk_diff.rs

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