hunk_diff.rs

   1use collections::{hash_map, HashMap, HashSet};
   2use git::diff::DiffHunkStatus;
   3use gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View};
   4use language::{Buffer, BufferId, Point};
   5use multi_buffer::{
   6    Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
   7    MultiBufferSnapshot, ToPoint,
   8};
   9use settings::SettingsStore;
  10use std::{
  11    ops::{Range, RangeInclusive},
  12    sync::Arc,
  13};
  14use ui::{
  15    prelude::*, ActiveTheme, ContextMenu, InteractiveElement, IntoElement, ParentElement, Pixels,
  16    Styled, ViewContext, VisualContext,
  17};
  18use util::{debug_panic, RangeExt};
  19
  20use crate::{
  21    editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections,
  22    mouse_context_menu::MouseContextMenu, BlockDisposition, BlockProperties, BlockStyle,
  23    CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, Editor, EditorElement,
  24    EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertFile, RevertSelectedHunks,
  25    ToDisplayPoint, 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}
  41
  42#[derive(Debug, Clone)]
  43pub(super) struct ExpandedHunk {
  44    pub block: Option<CustomBlockId>,
  45    pub hunk_range: Range<Anchor>,
  46    pub diff_base_byte_range: Range<usize>,
  47    pub status: DiffHunkStatus,
  48    pub folded: bool,
  49}
  50
  51#[derive(Debug)]
  52struct DiffBaseBuffer {
  53    buffer: Model<Buffer>,
  54    diff_base_version: usize,
  55}
  56
  57#[derive(Debug, Clone, PartialEq, Eq)]
  58pub enum DisplayDiffHunk {
  59    Folded {
  60        display_row: DisplayRow,
  61    },
  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(super) fn open_hunk_context_menu(
  81        &mut self,
  82        hovered_hunk: HoveredHunk,
  83        clicked_point: gpui::Point<Pixels>,
  84        cx: &mut ViewContext<Editor>,
  85    ) {
  86        let focus_handle = self.focus_handle.clone();
  87        let expanded = self
  88            .expanded_hunks
  89            .hunks(false)
  90            .any(|expanded_hunk| expanded_hunk.hunk_range == hovered_hunk.multi_buffer_range);
  91        let editor_handle = cx.view().clone();
  92        let editor_snapshot = self.snapshot(cx);
  93        let start_point = self
  94            .to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx)
  95            .unwrap_or(clicked_point);
  96        let end_point = self
  97            .to_pixel_point(hovered_hunk.multi_buffer_range.start, &editor_snapshot, cx)
  98            .unwrap_or(clicked_point);
  99        let norm =
 100            |a: gpui::Point<Pixels>, b: gpui::Point<Pixels>| (a.x - b.x).abs() + (a.y - b.y).abs();
 101        let closest_source = if norm(start_point, clicked_point) < norm(end_point, clicked_point) {
 102            hovered_hunk.multi_buffer_range.start
 103        } else {
 104            hovered_hunk.multi_buffer_range.end
 105        };
 106
 107        self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
 108            self,
 109            closest_source,
 110            clicked_point,
 111            ContextMenu::build(cx, move |menu, _| {
 112                menu.on_blur_subscription(Subscription::new(|| {}))
 113                    .context(focus_handle)
 114                    .entry(
 115                        if expanded {
 116                            "Collapse Hunk"
 117                        } else {
 118                            "Expand Hunk"
 119                        },
 120                        Some(ToggleHunkDiff.boxed_clone()),
 121                        {
 122                            let editor = editor_handle.clone();
 123                            let hunk = hovered_hunk.clone();
 124                            move |cx| {
 125                                editor.update(cx, |editor, cx| {
 126                                    editor.toggle_hovered_hunk(&hunk, cx);
 127                                });
 128                            }
 129                        },
 130                    )
 131                    .entry("Revert Hunk", Some(RevertSelectedHunks.boxed_clone()), {
 132                        let editor = editor_handle.clone();
 133                        let hunk = hovered_hunk.clone();
 134                        move |cx| {
 135                            let multi_buffer = editor.read(cx).buffer().clone();
 136                            let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
 137                            let mut revert_changes = HashMap::default();
 138                            if let Some(hunk) =
 139                                crate::hunk_diff::to_diff_hunk(&hunk, &multi_buffer_snapshot)
 140                            {
 141                                Editor::prepare_revert_change(
 142                                    &mut revert_changes,
 143                                    &multi_buffer,
 144                                    &hunk,
 145                                    cx,
 146                                );
 147                            }
 148                            if !revert_changes.is_empty() {
 149                                editor.update(cx, |editor, cx| editor.revert(revert_changes, cx));
 150                            }
 151                        }
 152                    })
 153                    .action("Revert File", RevertFile.boxed_clone())
 154            }),
 155            cx,
 156        )
 157    }
 158
 159    pub(super) fn toggle_hovered_hunk(
 160        &mut self,
 161        hovered_hunk: &HoveredHunk,
 162        cx: &mut ViewContext<Editor>,
 163    ) {
 164        let editor_snapshot = self.snapshot(cx);
 165        if let Some(diff_hunk) = to_diff_hunk(hovered_hunk, &editor_snapshot.buffer_snapshot) {
 166            self.toggle_hunks_expanded(vec![diff_hunk], cx);
 167            self.change_selections(None, cx, |selections| selections.refresh());
 168        }
 169    }
 170
 171    pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
 172        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
 173        let selections = self.selections.disjoint_anchors();
 174        self.toggle_hunks_expanded(
 175            hunks_for_selections(&multi_buffer_snapshot, &selections),
 176            cx,
 177        );
 178    }
 179
 180    pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext<Self>) {
 181        let snapshot = self.snapshot(cx);
 182        let display_rows_with_expanded_hunks = self
 183            .expanded_hunks
 184            .hunks(false)
 185            .map(|hunk| &hunk.hunk_range)
 186            .map(|anchor_range| {
 187                (
 188                    anchor_range
 189                        .start
 190                        .to_display_point(&snapshot.display_snapshot)
 191                        .row(),
 192                    anchor_range
 193                        .end
 194                        .to_display_point(&snapshot.display_snapshot)
 195                        .row(),
 196                )
 197            })
 198            .collect::<HashMap<_, _>>();
 199        let hunks = snapshot
 200            .display_snapshot
 201            .buffer_snapshot
 202            .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
 203            .filter(|hunk| {
 204                let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0)
 205                    .to_display_point(&snapshot.display_snapshot)
 206                    ..Point::new(hunk.row_range.end.0, 0)
 207                        .to_display_point(&snapshot.display_snapshot);
 208                let row_range_end =
 209                    display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row());
 210                row_range_end.is_none() || row_range_end != Some(&hunk_display_row_range.end.row())
 211            });
 212        self.toggle_hunks_expanded(hunks.collect(), cx);
 213    }
 214
 215    fn toggle_hunks_expanded(
 216        &mut self,
 217        hunks_to_toggle: Vec<MultiBufferDiffHunk>,
 218        cx: &mut ViewContext<Self>,
 219    ) {
 220        let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None);
 221        let new_toggle_task = cx.spawn(move |editor, mut cx| async move {
 222            if let Some(task) = previous_toggle_task {
 223                task.await;
 224            }
 225
 226            editor
 227                .update(&mut cx, |editor, cx| {
 228                    let snapshot = editor.snapshot(cx);
 229                    let mut hunks_to_toggle = hunks_to_toggle.into_iter().fuse().peekable();
 230                    let mut highlights_to_remove =
 231                        Vec::with_capacity(editor.expanded_hunks.hunks.len());
 232                    let mut blocks_to_remove = HashSet::default();
 233                    let mut hunks_to_expand = Vec::new();
 234                    editor.expanded_hunks.hunks.retain(|expanded_hunk| {
 235                        if expanded_hunk.folded {
 236                            return true;
 237                        }
 238                        let expanded_hunk_row_range = expanded_hunk
 239                            .hunk_range
 240                            .start
 241                            .to_display_point(&snapshot)
 242                            .row()
 243                            ..expanded_hunk
 244                                .hunk_range
 245                                .end
 246                                .to_display_point(&snapshot)
 247                                .row();
 248                        let mut retain = true;
 249                        while let Some(hunk_to_toggle) = hunks_to_toggle.peek() {
 250                            match diff_hunk_to_display(hunk_to_toggle, &snapshot) {
 251                                DisplayDiffHunk::Folded { .. } => {
 252                                    hunks_to_toggle.next();
 253                                    continue;
 254                                }
 255                                DisplayDiffHunk::Unfolded {
 256                                    diff_base_byte_range,
 257                                    display_row_range,
 258                                    multi_buffer_range,
 259                                    status,
 260                                } => {
 261                                    let hunk_to_toggle_row_range = display_row_range;
 262                                    if hunk_to_toggle_row_range.start > expanded_hunk_row_range.end
 263                                    {
 264                                        break;
 265                                    } else if expanded_hunk_row_range == hunk_to_toggle_row_range {
 266                                        highlights_to_remove.push(expanded_hunk.hunk_range.clone());
 267                                        blocks_to_remove.extend(expanded_hunk.block);
 268                                        hunks_to_toggle.next();
 269                                        retain = false;
 270                                        break;
 271                                    } else {
 272                                        hunks_to_expand.push(HoveredHunk {
 273                                            status,
 274                                            multi_buffer_range,
 275                                            diff_base_byte_range,
 276                                        });
 277                                        hunks_to_toggle.next();
 278                                        continue;
 279                                    }
 280                                }
 281                            }
 282                        }
 283
 284                        retain
 285                    });
 286                    for remaining_hunk in hunks_to_toggle {
 287                        let remaining_hunk_point_range =
 288                            Point::new(remaining_hunk.row_range.start.0, 0)
 289                                ..Point::new(remaining_hunk.row_range.end.0, 0);
 290                        hunks_to_expand.push(HoveredHunk {
 291                            status: hunk_status(&remaining_hunk),
 292                            multi_buffer_range: snapshot
 293                                .buffer_snapshot
 294                                .anchor_before(remaining_hunk_point_range.start)
 295                                ..snapshot
 296                                    .buffer_snapshot
 297                                    .anchor_after(remaining_hunk_point_range.end),
 298                            diff_base_byte_range: remaining_hunk.diff_base_byte_range.clone(),
 299                        });
 300                    }
 301
 302                    for removed_rows in highlights_to_remove {
 303                        editor.highlight_rows::<DiffRowHighlight>(
 304                            to_inclusive_row_range(removed_rows, &snapshot),
 305                            None,
 306                            false,
 307                            cx,
 308                        );
 309                    }
 310                    editor.remove_blocks(blocks_to_remove, None, cx);
 311                    for hunk in hunks_to_expand {
 312                        editor.expand_diff_hunk(None, &hunk, cx);
 313                    }
 314                    cx.notify();
 315                })
 316                .ok();
 317        });
 318
 319        self.expanded_hunks
 320            .hunk_update_tasks
 321            .insert(None, cx.background_executor().spawn(new_toggle_task));
 322    }
 323
 324    pub(super) fn expand_diff_hunk(
 325        &mut self,
 326        diff_base_buffer: Option<Model<Buffer>>,
 327        hunk: &HoveredHunk,
 328        cx: &mut ViewContext<'_, Editor>,
 329    ) -> Option<()> {
 330        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
 331        let multi_buffer_row_range = hunk
 332            .multi_buffer_range
 333            .start
 334            .to_point(&multi_buffer_snapshot)
 335            ..hunk.multi_buffer_range.end.to_point(&multi_buffer_snapshot);
 336        let hunk_start = hunk.multi_buffer_range.start;
 337        let hunk_end = hunk.multi_buffer_range.end;
 338
 339        let buffer = self.buffer().clone();
 340        let snapshot = self.snapshot(cx);
 341        let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| {
 342            let hunk = buffer_diff_hunk(&snapshot.buffer_snapshot, multi_buffer_row_range.clone())?;
 343            let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx);
 344            if buffer_ranges.len() == 1 {
 345                let (buffer, _, _) = buffer_ranges.pop()?;
 346                let diff_base_buffer = diff_base_buffer
 347                    .or_else(|| self.current_diff_base_buffer(&buffer, cx))
 348                    .or_else(|| create_diff_base_buffer(&buffer, cx))?;
 349                let buffer = buffer.read(cx);
 350                let deleted_text_lines = buffer.diff_base().map(|diff_base| {
 351                    let diff_start_row = diff_base
 352                        .offset_to_point(hunk.diff_base_byte_range.start)
 353                        .row;
 354                    let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
 355
 356                    diff_end_row - diff_start_row
 357                })?;
 358                Some((diff_base_buffer, deleted_text_lines))
 359            } else {
 360                None
 361            }
 362        })?;
 363
 364        let block_insert_index = match self.expanded_hunks.hunks.binary_search_by(|probe| {
 365            probe
 366                .hunk_range
 367                .start
 368                .cmp(&hunk_start, &multi_buffer_snapshot)
 369        }) {
 370            Ok(_already_present) => return None,
 371            Err(ix) => ix,
 372        };
 373
 374        let block = match hunk.status {
 375            DiffHunkStatus::Removed => {
 376                self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, hunk, cx)
 377            }
 378            DiffHunkStatus::Added => {
 379                self.highlight_rows::<DiffRowHighlight>(
 380                    to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
 381                    Some(added_hunk_color(cx)),
 382                    false,
 383                    cx,
 384                );
 385                None
 386            }
 387            DiffHunkStatus::Modified => {
 388                self.highlight_rows::<DiffRowHighlight>(
 389                    to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
 390                    Some(added_hunk_color(cx)),
 391                    false,
 392                    cx,
 393                );
 394                self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, hunk, cx)
 395            }
 396        };
 397        self.expanded_hunks.hunks.insert(
 398            block_insert_index,
 399            ExpandedHunk {
 400                block,
 401                hunk_range: hunk_start..hunk_end,
 402                status: hunk.status,
 403                folded: false,
 404                diff_base_byte_range: hunk.diff_base_byte_range.clone(),
 405            },
 406        );
 407
 408        Some(())
 409    }
 410
 411    fn insert_deleted_text_block(
 412        &mut self,
 413        diff_base_buffer: Model<Buffer>,
 414        deleted_text_height: u32,
 415        hunk: &HoveredHunk,
 416        cx: &mut ViewContext<'_, Self>,
 417    ) -> Option<CustomBlockId> {
 418        let deleted_hunk_color = deleted_hunk_color(cx);
 419        let (editor_height, editor_with_deleted_text) =
 420            editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
 421        let editor = cx.view().clone();
 422        let hunk = hunk.clone();
 423        let height = editor_height.max(deleted_text_height);
 424        let mut new_block_ids = self.insert_blocks(
 425            Some(BlockProperties {
 426                position: hunk.multi_buffer_range.start,
 427                height,
 428                style: BlockStyle::Flex,
 429                disposition: BlockDisposition::Above,
 430                render: Box::new(move |cx| {
 431                    let width = EditorElement::diff_hunk_strip_width(cx.line_height());
 432                    let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
 433
 434                    let close_button = editor.update(cx.context, |editor, cx| {
 435                        let editor_snapshot = editor.snapshot(cx);
 436                        let hunk_display_range = hunk
 437                            .multi_buffer_range
 438                            .clone()
 439                            .to_display_points(&editor_snapshot);
 440                        editor.close_hunk_diff_button(
 441                            hunk.clone(),
 442                            hunk_display_range.start.row(),
 443                            cx,
 444                        )
 445                    });
 446
 447                    h_flex()
 448                        .id("gutter with editor")
 449                        .bg(deleted_hunk_color)
 450                        .h(height as f32 * cx.line_height())
 451                        .w_full()
 452                        .child(
 453                            h_flex()
 454                                .id("gutter")
 455                                .max_w(gutter_dimensions.full_width())
 456                                .min_w(gutter_dimensions.full_width())
 457                                .size_full()
 458                                .child(
 459                                    h_flex()
 460                                        .id("gutter hunk")
 461                                        .bg(cx.theme().status().deleted)
 462                                        .pl(gutter_dimensions.margin
 463                                            + gutter_dimensions
 464                                                .git_blame_entries_width
 465                                                .unwrap_or_default())
 466                                        .max_w(width)
 467                                        .min_w(width)
 468                                        .size_full()
 469                                        .cursor(CursorStyle::PointingHand)
 470                                        .on_mouse_down(MouseButton::Left, {
 471                                            let editor = editor.clone();
 472                                            let hunk = hunk.clone();
 473                                            move |event, cx| {
 474                                                let modifiers = event.modifiers;
 475                                                if modifiers.control || modifiers.platform {
 476                                                    editor.update(cx, |editor, cx| {
 477                                                        editor.toggle_hovered_hunk(&hunk, cx);
 478                                                    });
 479                                                } else {
 480                                                    editor.update(cx, |editor, cx| {
 481                                                        editor.open_hunk_context_menu(
 482                                                            hunk.clone(),
 483                                                            event.position,
 484                                                            cx,
 485                                                        );
 486                                                    });
 487                                                }
 488                                            }
 489                                        }),
 490                                )
 491                                .child(
 492                                    v_flex()
 493                                        .size_full()
 494                                        .pt(rems(0.25))
 495                                        .justify_start()
 496                                        .child(close_button),
 497                                ),
 498                        )
 499                        .child(editor_with_deleted_text.clone())
 500                        .into_any_element()
 501                }),
 502                priority: 0,
 503            }),
 504            None,
 505            cx,
 506        );
 507        if new_block_ids.len() == 1 {
 508            new_block_ids.pop()
 509        } else {
 510            debug_panic!(
 511                "Inserted one editor block but did not receive exactly one block id: {new_block_ids:?}"
 512            );
 513            None
 514        }
 515    }
 516
 517    pub(super) fn clear_clicked_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
 518        self.expanded_hunks.hunk_update_tasks.clear();
 519        self.clear_row_highlights::<DiffRowHighlight>();
 520        let to_remove = self
 521            .expanded_hunks
 522            .hunks
 523            .drain(..)
 524            .filter_map(|expanded_hunk| expanded_hunk.block)
 525            .collect::<HashSet<_>>();
 526        if to_remove.is_empty() {
 527            false
 528        } else {
 529            self.remove_blocks(to_remove, None, cx);
 530            true
 531        }
 532    }
 533
 534    pub(super) fn sync_expanded_diff_hunks(
 535        &mut self,
 536        buffer: Model<Buffer>,
 537        cx: &mut ViewContext<'_, Self>,
 538    ) {
 539        let buffer_id = buffer.read(cx).remote_id();
 540        let buffer_diff_base_version = buffer.read(cx).diff_base_version();
 541        self.expanded_hunks
 542            .hunk_update_tasks
 543            .remove(&Some(buffer_id));
 544        let diff_base_buffer = self.current_diff_base_buffer(&buffer, cx);
 545        let new_sync_task = cx.spawn(move |editor, mut cx| async move {
 546            let diff_base_buffer_unchanged = diff_base_buffer.is_some();
 547            let Ok(diff_base_buffer) =
 548                cx.update(|cx| diff_base_buffer.or_else(|| create_diff_base_buffer(&buffer, cx)))
 549            else {
 550                return;
 551            };
 552            editor
 553                .update(&mut cx, |editor, cx| {
 554                    if let Some(diff_base_buffer) = &diff_base_buffer {
 555                        editor.expanded_hunks.diff_base.insert(
 556                            buffer_id,
 557                            DiffBaseBuffer {
 558                                buffer: diff_base_buffer.clone(),
 559                                diff_base_version: buffer_diff_base_version,
 560                            },
 561                        );
 562                    }
 563
 564                    let snapshot = editor.snapshot(cx);
 565                    let mut recalculated_hunks = snapshot
 566                        .buffer_snapshot
 567                        .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
 568                        .filter(|hunk| hunk.buffer_id == buffer_id)
 569                        .fuse()
 570                        .peekable();
 571                    let mut highlights_to_remove =
 572                        Vec::with_capacity(editor.expanded_hunks.hunks.len());
 573                    let mut blocks_to_remove = HashSet::default();
 574                    let mut hunks_to_reexpand =
 575                        Vec::with_capacity(editor.expanded_hunks.hunks.len());
 576                    editor.expanded_hunks.hunks.retain_mut(|expanded_hunk| {
 577                        if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) {
 578                            return true;
 579                        };
 580
 581                        let mut retain = false;
 582                        if diff_base_buffer_unchanged {
 583                            let expanded_hunk_display_range = expanded_hunk
 584                                .hunk_range
 585                                .start
 586                                .to_display_point(&snapshot)
 587                                .row()
 588                                ..expanded_hunk
 589                                    .hunk_range
 590                                    .end
 591                                    .to_display_point(&snapshot)
 592                                    .row();
 593                            while let Some(buffer_hunk) = recalculated_hunks.peek() {
 594                                match diff_hunk_to_display(buffer_hunk, &snapshot) {
 595                                    DisplayDiffHunk::Folded { display_row } => {
 596                                        recalculated_hunks.next();
 597                                        if !expanded_hunk.folded
 598                                            && expanded_hunk_display_range
 599                                                .to_inclusive()
 600                                                .contains(&display_row)
 601                                        {
 602                                            retain = true;
 603                                            expanded_hunk.folded = true;
 604                                            highlights_to_remove
 605                                                .push(expanded_hunk.hunk_range.clone());
 606                                            if let Some(block) = expanded_hunk.block.take() {
 607                                                blocks_to_remove.insert(block);
 608                                            }
 609                                            break;
 610                                        } else {
 611                                            continue;
 612                                        }
 613                                    }
 614                                    DisplayDiffHunk::Unfolded {
 615                                        diff_base_byte_range,
 616                                        display_row_range,
 617                                        multi_buffer_range,
 618                                        status,
 619                                    } => {
 620                                        let hunk_display_range = display_row_range;
 621                                        if expanded_hunk_display_range.start
 622                                            > hunk_display_range.end
 623                                        {
 624                                            recalculated_hunks.next();
 625                                            continue;
 626                                        } else if expanded_hunk_display_range.end
 627                                            < hunk_display_range.start
 628                                        {
 629                                            break;
 630                                        } else {
 631                                            if !expanded_hunk.folded
 632                                                && expanded_hunk_display_range == hunk_display_range
 633                                                && expanded_hunk.status == hunk_status(buffer_hunk)
 634                                                && expanded_hunk.diff_base_byte_range
 635                                                    == buffer_hunk.diff_base_byte_range
 636                                            {
 637                                                recalculated_hunks.next();
 638                                                retain = true;
 639                                            } else {
 640                                                hunks_to_reexpand.push(HoveredHunk {
 641                                                    status,
 642                                                    multi_buffer_range,
 643                                                    diff_base_byte_range,
 644                                                });
 645                                            }
 646                                            break;
 647                                        }
 648                                    }
 649                                }
 650                            }
 651                        }
 652                        if !retain {
 653                            blocks_to_remove.extend(expanded_hunk.block);
 654                            highlights_to_remove.push(expanded_hunk.hunk_range.clone());
 655                        }
 656                        retain
 657                    });
 658
 659                    for removed_rows in highlights_to_remove {
 660                        editor.highlight_rows::<DiffRowHighlight>(
 661                            to_inclusive_row_range(removed_rows, &snapshot),
 662                            None,
 663                            false,
 664                            cx,
 665                        );
 666                    }
 667                    editor.remove_blocks(blocks_to_remove, None, cx);
 668
 669                    if let Some(diff_base_buffer) = &diff_base_buffer {
 670                        for hunk in hunks_to_reexpand {
 671                            editor.expand_diff_hunk(Some(diff_base_buffer.clone()), &hunk, cx);
 672                        }
 673                    }
 674                })
 675                .ok();
 676        });
 677
 678        self.expanded_hunks.hunk_update_tasks.insert(
 679            Some(buffer_id),
 680            cx.background_executor().spawn(new_sync_task),
 681        );
 682    }
 683
 684    fn current_diff_base_buffer(
 685        &mut self,
 686        buffer: &Model<Buffer>,
 687        cx: &mut AppContext,
 688    ) -> Option<Model<Buffer>> {
 689        buffer.update(cx, |buffer, _| {
 690            match self.expanded_hunks.diff_base.entry(buffer.remote_id()) {
 691                hash_map::Entry::Occupied(o) => {
 692                    if o.get().diff_base_version != buffer.diff_base_version() {
 693                        o.remove();
 694                        None
 695                    } else {
 696                        Some(o.get().buffer.clone())
 697                    }
 698                }
 699                hash_map::Entry::Vacant(_) => None,
 700            }
 701        })
 702    }
 703}
 704
 705fn to_diff_hunk(
 706    hovered_hunk: &HoveredHunk,
 707    multi_buffer_snapshot: &MultiBufferSnapshot,
 708) -> Option<MultiBufferDiffHunk> {
 709    let buffer_id = hovered_hunk
 710        .multi_buffer_range
 711        .start
 712        .buffer_id
 713        .or(hovered_hunk.multi_buffer_range.end.buffer_id)?;
 714    let buffer_range = hovered_hunk.multi_buffer_range.start.text_anchor
 715        ..hovered_hunk.multi_buffer_range.end.text_anchor;
 716    let point_range = hovered_hunk
 717        .multi_buffer_range
 718        .to_point(multi_buffer_snapshot);
 719    Some(MultiBufferDiffHunk {
 720        row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
 721        buffer_id,
 722        buffer_range,
 723        diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
 724    })
 725}
 726
 727fn create_diff_base_buffer(buffer: &Model<Buffer>, cx: &mut AppContext) -> Option<Model<Buffer>> {
 728    buffer
 729        .update(cx, |buffer, _| {
 730            let language = buffer.language().cloned();
 731            let diff_base = buffer.diff_base()?.clone();
 732            Some((buffer.line_ending(), diff_base, language))
 733        })
 734        .map(|(line_ending, diff_base, language)| {
 735            cx.new_model(|cx| {
 736                let buffer = Buffer::local_normalized(diff_base, line_ending, cx);
 737                match language {
 738                    Some(language) => buffer.with_language(language, cx),
 739                    None => buffer,
 740                }
 741            })
 742        })
 743}
 744
 745fn added_hunk_color(cx: &AppContext) -> Hsla {
 746    let mut created_color = cx.theme().status().git().created;
 747    created_color.fade_out(0.7);
 748    created_color
 749}
 750
 751fn deleted_hunk_color(cx: &AppContext) -> Hsla {
 752    let mut deleted_color = cx.theme().status().git().deleted;
 753    deleted_color.fade_out(0.7);
 754    deleted_color
 755}
 756
 757fn editor_with_deleted_text(
 758    diff_base_buffer: Model<Buffer>,
 759    deleted_color: Hsla,
 760    hunk: &HoveredHunk,
 761    cx: &mut ViewContext<'_, Editor>,
 762) -> (u32, View<Editor>) {
 763    let parent_editor = cx.view().downgrade();
 764    let editor = cx.new_view(|cx| {
 765        let multi_buffer =
 766            cx.new_model(|_| MultiBuffer::without_headers(language::Capability::ReadOnly));
 767        multi_buffer.update(cx, |multi_buffer, cx| {
 768            multi_buffer.push_excerpts(
 769                diff_base_buffer,
 770                Some(ExcerptRange {
 771                    context: hunk.diff_base_byte_range.clone(),
 772                    primary: None,
 773                }),
 774                cx,
 775            );
 776        });
 777
 778        let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
 779        editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
 780        editor.set_show_wrap_guides(false, cx);
 781        editor.set_show_gutter(false, cx);
 782        editor.scroll_manager.set_forbid_vertical_scroll(true);
 783        editor.set_read_only(true);
 784        editor.set_show_inline_completions(Some(false), cx);
 785        editor.highlight_rows::<DiffRowHighlight>(
 786            Anchor::min()..=Anchor::max(),
 787            Some(deleted_color),
 788            false,
 789            cx,
 790        );
 791
 792        let subscription_editor = parent_editor.clone();
 793        editor._subscriptions.extend([
 794            cx.on_blur(&editor.focus_handle, |editor, cx| {
 795                editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
 796                editor.change_selections(None, cx, |s| {
 797                    s.try_cancel();
 798                });
 799                cx.notify();
 800            }),
 801            cx.on_focus(&editor.focus_handle, move |editor, cx| {
 802                let restored_highlight = if let Some(parent_editor) = subscription_editor.upgrade()
 803                {
 804                    parent_editor.read(cx).current_line_highlight
 805                } else {
 806                    None
 807                };
 808                editor.set_current_line_highlight(restored_highlight);
 809                cx.notify();
 810            }),
 811            cx.observe_global::<SettingsStore>(|editor, cx| {
 812                if !editor.is_focused(cx) {
 813                    editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
 814                }
 815            }),
 816        ]);
 817        let parent_editor_for_reverts = parent_editor.clone();
 818        let original_multi_buffer_range = hunk.multi_buffer_range.clone();
 819        let diff_base_range = hunk.diff_base_byte_range.clone();
 820        editor
 821            .register_action::<RevertSelectedHunks>(move |_, cx| {
 822                parent_editor_for_reverts
 823                    .update(cx, |editor, cx| {
 824                        let Some((buffer, original_text)) =
 825                            editor.buffer().update(cx, |buffer, cx| {
 826                                let (_, buffer, _) = buffer
 827                                    .excerpt_containing(original_multi_buffer_range.start, cx)?;
 828                                let original_text =
 829                                    buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
 830                                Some((buffer, Arc::from(original_text.to_string())))
 831                            })
 832                        else {
 833                            return;
 834                        };
 835                        buffer.update(cx, |buffer, cx| {
 836                            buffer.edit(
 837                                Some((
 838                                    original_multi_buffer_range.start.text_anchor
 839                                        ..original_multi_buffer_range.end.text_anchor,
 840                                    original_text,
 841                                )),
 842                                None,
 843                                cx,
 844                            )
 845                        });
 846                    })
 847                    .ok();
 848            })
 849            .detach();
 850        let hunk = hunk.clone();
 851        editor
 852            .register_action::<ToggleHunkDiff>(move |_, cx| {
 853                parent_editor
 854                    .update(cx, |editor, cx| {
 855                        editor.toggle_hovered_hunk(&hunk, cx);
 856                    })
 857                    .ok();
 858            })
 859            .detach();
 860        editor
 861    });
 862
 863    let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0);
 864    (editor_height, editor)
 865}
 866
 867fn buffer_diff_hunk(
 868    buffer_snapshot: &MultiBufferSnapshot,
 869    row_range: Range<Point>,
 870) -> Option<MultiBufferDiffHunk> {
 871    let mut hunks = buffer_snapshot.git_diff_hunks_in_range(
 872        MultiBufferRow(row_range.start.row)..MultiBufferRow(row_range.end.row),
 873    );
 874    let hunk = hunks.next()?;
 875    let second_hunk = hunks.next();
 876    if second_hunk.is_none() {
 877        return Some(hunk);
 878    }
 879    None
 880}
 881
 882fn to_inclusive_row_range(
 883    row_range: Range<Anchor>,
 884    snapshot: &EditorSnapshot,
 885) -> RangeInclusive<Anchor> {
 886    let mut display_row_range =
 887        row_range.start.to_display_point(snapshot)..row_range.end.to_display_point(snapshot);
 888    if display_row_range.end.row() > display_row_range.start.row() {
 889        *display_row_range.end.row_mut() -= 1;
 890    }
 891    let point_range = display_row_range.start.to_point(&snapshot.display_snapshot)
 892        ..display_row_range.end.to_point(&snapshot.display_snapshot);
 893    let new_range = point_range.to_anchors(&snapshot.buffer_snapshot);
 894    new_range.start..=new_range.end
 895}
 896
 897impl DisplayDiffHunk {
 898    pub fn start_display_row(&self) -> DisplayRow {
 899        match self {
 900            &DisplayDiffHunk::Folded { display_row } => display_row,
 901            DisplayDiffHunk::Unfolded {
 902                display_row_range, ..
 903            } => display_row_range.start,
 904        }
 905    }
 906
 907    pub fn contains_display_row(&self, display_row: DisplayRow) -> bool {
 908        let range = match self {
 909            &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
 910
 911            DisplayDiffHunk::Unfolded {
 912                display_row_range, ..
 913            } => display_row_range.start..=display_row_range.end,
 914        };
 915
 916        range.contains(&display_row)
 917    }
 918}
 919
 920pub fn diff_hunk_to_display(
 921    hunk: &MultiBufferDiffHunk,
 922    snapshot: &DisplaySnapshot,
 923) -> DisplayDiffHunk {
 924    let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
 925    let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0);
 926    let hunk_end_point_sub = Point::new(
 927        hunk.row_range
 928            .end
 929            .0
 930            .saturating_sub(1)
 931            .max(hunk.row_range.start.0),
 932        0,
 933    );
 934
 935    let status = hunk_status(hunk);
 936    let is_removal = status == DiffHunkStatus::Removed;
 937
 938    let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0);
 939    let folds_end = Point::new(hunk.row_range.end.0 + 2, 0);
 940    let folds_range = folds_start..folds_end;
 941
 942    let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
 943        let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
 944        let fold_point_range = fold_point_range.start..=fold_point_range.end;
 945
 946        let folded_start = fold_point_range.contains(&hunk_start_point);
 947        let folded_end = fold_point_range.contains(&hunk_end_point_sub);
 948        let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
 949
 950        (folded_start && folded_end) || (is_removal && folded_start_sub)
 951    });
 952
 953    if let Some(fold) = containing_fold {
 954        let row = fold.range.start.to_display_point(snapshot).row();
 955        DisplayDiffHunk::Folded { display_row: row }
 956    } else {
 957        let start = hunk_start_point.to_display_point(snapshot).row();
 958
 959        let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start);
 960        let hunk_end_point = Point::new(hunk_end_row.0, 0);
 961
 962        let multi_buffer_start = snapshot.buffer_snapshot.anchor_before(hunk_start_point);
 963        let multi_buffer_end = snapshot.buffer_snapshot.anchor_after(hunk_end_point);
 964        let end = hunk_end_point.to_display_point(snapshot).row();
 965
 966        DisplayDiffHunk::Unfolded {
 967            display_row_range: start..end,
 968            multi_buffer_range: multi_buffer_start..multi_buffer_end,
 969            status,
 970            diff_base_byte_range: hunk.diff_base_byte_range.clone(),
 971        }
 972    }
 973}
 974
 975#[cfg(test)]
 976mod tests {
 977    use super::*;
 978    use crate::{editor_tests::init_test, hunk_status};
 979    use gpui::{Context, TestAppContext};
 980    use language::Capability::ReadWrite;
 981    use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow};
 982    use project::{FakeFs, Project};
 983    use unindent::Unindent as _;
 984
 985    #[gpui::test]
 986    async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
 987        use git::diff::DiffHunkStatus;
 988        init_test(cx, |_| {});
 989
 990        let fs = FakeFs::new(cx.background_executor.clone());
 991        let project = Project::test(fs, [], cx).await;
 992
 993        // buffer has two modified hunks with two rows each
 994        let buffer_1 = project.update(cx, |project, cx| {
 995            project.create_local_buffer(
 996                "
 997                        1.zero
 998                        1.ONE
 999                        1.TWO
1000                        1.three
1001                        1.FOUR
1002                        1.FIVE
1003                        1.six
1004                    "
1005                .unindent()
1006                .as_str(),
1007                None,
1008                cx,
1009            )
1010        });
1011        buffer_1.update(cx, |buffer, cx| {
1012            buffer.set_diff_base(
1013                Some(
1014                    "
1015                        1.zero
1016                        1.one
1017                        1.two
1018                        1.three
1019                        1.four
1020                        1.five
1021                        1.six
1022                    "
1023                    .unindent(),
1024                ),
1025                cx,
1026            );
1027        });
1028
1029        // buffer has a deletion hunk and an insertion hunk
1030        let buffer_2 = project.update(cx, |project, cx| {
1031            project.create_local_buffer(
1032                "
1033                        2.zero
1034                        2.one
1035                        2.two
1036                        2.three
1037                        2.four
1038                        2.five
1039                        2.six
1040                    "
1041                .unindent()
1042                .as_str(),
1043                None,
1044                cx,
1045            )
1046        });
1047        buffer_2.update(cx, |buffer, cx| {
1048            buffer.set_diff_base(
1049                Some(
1050                    "
1051                        2.zero
1052                        2.one
1053                        2.one-and-a-half
1054                        2.two
1055                        2.three
1056                        2.four
1057                        2.six
1058                    "
1059                    .unindent(),
1060                ),
1061                cx,
1062            );
1063        });
1064
1065        cx.background_executor.run_until_parked();
1066
1067        let multibuffer = cx.new_model(|cx| {
1068            let mut multibuffer = MultiBuffer::new(ReadWrite);
1069            multibuffer.push_excerpts(
1070                buffer_1.clone(),
1071                [
1072                    // excerpt ends in the middle of a modified hunk
1073                    ExcerptRange {
1074                        context: Point::new(0, 0)..Point::new(1, 5),
1075                        primary: Default::default(),
1076                    },
1077                    // excerpt begins in the middle of a modified hunk
1078                    ExcerptRange {
1079                        context: Point::new(5, 0)..Point::new(6, 5),
1080                        primary: Default::default(),
1081                    },
1082                ],
1083                cx,
1084            );
1085            multibuffer.push_excerpts(
1086                buffer_2.clone(),
1087                [
1088                    // excerpt ends at a deletion
1089                    ExcerptRange {
1090                        context: Point::new(0, 0)..Point::new(1, 5),
1091                        primary: Default::default(),
1092                    },
1093                    // excerpt starts at a deletion
1094                    ExcerptRange {
1095                        context: Point::new(2, 0)..Point::new(2, 5),
1096                        primary: Default::default(),
1097                    },
1098                    // excerpt fully contains a deletion hunk
1099                    ExcerptRange {
1100                        context: Point::new(1, 0)..Point::new(2, 5),
1101                        primary: Default::default(),
1102                    },
1103                    // excerpt fully contains an insertion hunk
1104                    ExcerptRange {
1105                        context: Point::new(4, 0)..Point::new(6, 5),
1106                        primary: Default::default(),
1107                    },
1108                ],
1109                cx,
1110            );
1111            multibuffer
1112        });
1113
1114        let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
1115
1116        assert_eq!(
1117            snapshot.text(),
1118            "
1119                1.zero
1120                1.ONE
1121                1.FIVE
1122                1.six
1123                2.zero
1124                2.one
1125                2.two
1126                2.one
1127                2.two
1128                2.four
1129                2.five
1130                2.six"
1131                .unindent()
1132        );
1133
1134        let expected = [
1135            (
1136                DiffHunkStatus::Modified,
1137                MultiBufferRow(1)..MultiBufferRow(2),
1138            ),
1139            (
1140                DiffHunkStatus::Modified,
1141                MultiBufferRow(2)..MultiBufferRow(3),
1142            ),
1143            //TODO: Define better when and where removed hunks show up at range extremities
1144            (
1145                DiffHunkStatus::Removed,
1146                MultiBufferRow(6)..MultiBufferRow(6),
1147            ),
1148            (
1149                DiffHunkStatus::Removed,
1150                MultiBufferRow(8)..MultiBufferRow(8),
1151            ),
1152            (
1153                DiffHunkStatus::Added,
1154                MultiBufferRow(10)..MultiBufferRow(11),
1155            ),
1156        ];
1157
1158        assert_eq!(
1159            snapshot
1160                .git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12))
1161                .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1162                .collect::<Vec<_>>(),
1163            &expected,
1164        );
1165
1166        assert_eq!(
1167            snapshot
1168                .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12))
1169                .map(|hunk| (hunk_status(&hunk), hunk.row_range))
1170                .collect::<Vec<_>>(),
1171            expected
1172                .iter()
1173                .rev()
1174                .cloned()
1175                .collect::<Vec<_>>()
1176                .as_slice(),
1177        );
1178    }
1179}