split.rs

   1use std::{
   2    ops::{Bound, Range, RangeInclusive},
   3    sync::Arc,
   4};
   5
   6use buffer_diff::{BufferDiff, BufferDiffSnapshot};
   7use collections::HashMap;
   8use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
   9use gpui::{Action, AppContext as _, Entity, EventEmitter, Focusable, Subscription, WeakEntity};
  10use itertools::Itertools;
  11use language::{Buffer, Capability};
  12use multi_buffer::{
  13    Anchor, BufferOffset, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer,
  14    MultiBufferDiffHunk, MultiBufferPoint, MultiBufferSnapshot, PathKey,
  15};
  16use project::Project;
  17use rope::Point;
  18use settings::DiffViewStyle;
  19use text::{BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
  20use ui::{
  21    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
  22    Styled as _, Window, div,
  23};
  24
  25use crate::{
  26    display_map::CompanionExcerptPatch,
  27    split_editor_view::{SplitEditorState, SplitEditorView},
  28};
  29use workspace::{
  30    ActivatePaneLeft, ActivatePaneRight, Item, ToolbarItemLocation, Workspace,
  31    item::{BreadcrumbText, ItemBufferKind, ItemEvent, SaveOptions, TabContentParams},
  32    searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
  33};
  34
  35use crate::{
  36    Autoscroll, DisplayMap, Editor, EditorEvent, RenderDiffHunkControlsFn, ToggleCodeActions,
  37    ToggleSoftWrap,
  38    actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
  39    display_map::Companion,
  40};
  41use zed_actions::assistant::InlineAssist;
  42
  43pub(crate) fn convert_lhs_rows_to_rhs(
  44    lhs_excerpt_to_rhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
  45    rhs_snapshot: &MultiBufferSnapshot,
  46    lhs_snapshot: &MultiBufferSnapshot,
  47    lhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
  48) -> Vec<CompanionExcerptPatch> {
  49    patches_for_range(
  50        lhs_excerpt_to_rhs_excerpt,
  51        lhs_snapshot,
  52        rhs_snapshot,
  53        lhs_bounds,
  54        |diff, range, buffer| diff.patch_for_base_text_range(range, buffer),
  55    )
  56}
  57
  58pub(crate) fn convert_rhs_rows_to_lhs(
  59    rhs_excerpt_to_lhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
  60    lhs_snapshot: &MultiBufferSnapshot,
  61    rhs_snapshot: &MultiBufferSnapshot,
  62    rhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
  63) -> Vec<CompanionExcerptPatch> {
  64    patches_for_range(
  65        rhs_excerpt_to_lhs_excerpt,
  66        rhs_snapshot,
  67        lhs_snapshot,
  68        rhs_bounds,
  69        |diff, range, buffer| diff.patch_for_buffer_range(range, buffer),
  70    )
  71}
  72
  73fn translate_lhs_selections_to_rhs(
  74    selections_by_buffer: &HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>,
  75    splittable: &SplittableEditor,
  76    cx: &App,
  77) -> HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> {
  78    let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx);
  79    let Some(companion) = rhs_display_map.companion() else {
  80        return HashMap::default();
  81    };
  82    let companion = companion.read(cx);
  83
  84    let mut translated: HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> =
  85        HashMap::default();
  86
  87    for (lhs_buffer_id, (ranges, scroll_offset)) in selections_by_buffer {
  88        let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(*lhs_buffer_id) else {
  89            continue;
  90        };
  91
  92        let Some(rhs_buffer) = splittable
  93            .rhs_editor
  94            .read(cx)
  95            .buffer()
  96            .read(cx)
  97            .buffer(rhs_buffer_id)
  98        else {
  99            continue;
 100        };
 101
 102        let Some(diff) = splittable
 103            .rhs_editor
 104            .read(cx)
 105            .buffer()
 106            .read(cx)
 107            .diff_for(rhs_buffer_id)
 108        else {
 109            continue;
 110        };
 111
 112        let diff_snapshot = diff.read(cx).snapshot(cx);
 113        let rhs_buffer_snapshot = rhs_buffer.read(cx).snapshot();
 114        let base_text_buffer = diff.read(cx).base_text_buffer();
 115        let base_text_snapshot = base_text_buffer.read(cx).snapshot();
 116
 117        let translated_ranges: Vec<Range<BufferOffset>> = ranges
 118            .iter()
 119            .map(|range| {
 120                let start_point = base_text_snapshot.offset_to_point(range.start.0);
 121                let end_point = base_text_snapshot.offset_to_point(range.end.0);
 122
 123                let rhs_start = diff_snapshot
 124                    .base_text_point_to_buffer_point(start_point, &rhs_buffer_snapshot);
 125                let rhs_end =
 126                    diff_snapshot.base_text_point_to_buffer_point(end_point, &rhs_buffer_snapshot);
 127
 128                BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_start))
 129                    ..BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_end))
 130            })
 131            .collect();
 132
 133        translated.insert(rhs_buffer, (translated_ranges, *scroll_offset));
 134    }
 135
 136    translated
 137}
 138
 139fn translate_lhs_hunks_to_rhs(
 140    lhs_hunks: &[MultiBufferDiffHunk],
 141    splittable: &SplittableEditor,
 142    cx: &App,
 143) -> Vec<MultiBufferDiffHunk> {
 144    let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx);
 145    let Some(companion) = rhs_display_map.companion() else {
 146        return vec![];
 147    };
 148    let companion = companion.read(cx);
 149    let rhs_snapshot = splittable.rhs_multibuffer.read(cx).snapshot(cx);
 150    let rhs_hunks: Vec<MultiBufferDiffHunk> = rhs_snapshot.diff_hunks().collect();
 151
 152    let mut translated = Vec::new();
 153    for lhs_hunk in lhs_hunks {
 154        let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(lhs_hunk.buffer_id) else {
 155            continue;
 156        };
 157        if let Some(rhs_hunk) = rhs_hunks.iter().find(|rhs_hunk| {
 158            rhs_hunk.buffer_id == rhs_buffer_id
 159                && rhs_hunk.diff_base_byte_range == lhs_hunk.diff_base_byte_range
 160        }) {
 161            translated.push(rhs_hunk.clone());
 162        }
 163    }
 164    translated
 165}
 166
 167fn patches_for_range<F>(
 168    excerpt_map: &HashMap<ExcerptId, ExcerptId>,
 169    source_snapshot: &MultiBufferSnapshot,
 170    target_snapshot: &MultiBufferSnapshot,
 171    source_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
 172    translate_fn: F,
 173) -> Vec<CompanionExcerptPatch>
 174where
 175    F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>,
 176{
 177    let mut result = Vec::new();
 178    let mut patches = HashMap::default();
 179
 180    for (source_buffer, buffer_offset_range, source_excerpt_id) in
 181        source_snapshot.range_to_buffer_ranges(source_bounds)
 182    {
 183        let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied().unwrap();
 184        let target_buffer = target_snapshot
 185            .buffer_for_excerpt(target_excerpt_id)
 186            .unwrap();
 187        let patch = patches.entry(source_buffer.remote_id()).or_insert_with(|| {
 188            let diff = source_snapshot
 189                .diff_for_buffer_id(source_buffer.remote_id())
 190                .unwrap();
 191            let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() {
 192                &target_buffer
 193            } else {
 194                source_buffer
 195            };
 196            // TODO(split-diff) pass only the union of the ranges for the affected excerpts
 197            translate_fn(diff, Point::zero()..=source_buffer.max_point(), rhs_buffer)
 198        });
 199        let buffer_point_range = buffer_offset_range.to_point(source_buffer);
 200
 201        // TODO(split-diff) maybe narrow the patch to only the edited part of the excerpt
 202        // (less useful for project diff, but important if we want to do singleton side-by-side diff)
 203        result.push(patch_for_excerpt(
 204            source_snapshot,
 205            target_snapshot,
 206            source_excerpt_id,
 207            target_excerpt_id,
 208            source_buffer,
 209            target_buffer,
 210            patch,
 211            buffer_point_range,
 212        ));
 213    }
 214
 215    result
 216}
 217
 218fn patch_for_excerpt(
 219    source_snapshot: &MultiBufferSnapshot,
 220    target_snapshot: &MultiBufferSnapshot,
 221    source_excerpt_id: ExcerptId,
 222    target_excerpt_id: ExcerptId,
 223    source_buffer: &text::BufferSnapshot,
 224    target_buffer: &text::BufferSnapshot,
 225    patch: &Patch<Point>,
 226    source_edited_range: Range<Point>,
 227) -> CompanionExcerptPatch {
 228    let source_multibuffer_range = source_snapshot
 229        .range_for_excerpt(source_excerpt_id)
 230        .unwrap();
 231    let source_excerpt_start_in_multibuffer = source_multibuffer_range.start;
 232    let source_context_range = source_snapshot
 233        .context_range_for_excerpt(source_excerpt_id)
 234        .unwrap();
 235    let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer);
 236    let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer);
 237    let target_multibuffer_range = target_snapshot
 238        .range_for_excerpt(target_excerpt_id)
 239        .unwrap();
 240    let target_excerpt_start_in_multibuffer = target_multibuffer_range.start;
 241    let target_context_range = target_snapshot
 242        .context_range_for_excerpt(target_excerpt_id)
 243        .unwrap();
 244    let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer);
 245    let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer);
 246
 247    let edits = patch
 248        .edits()
 249        .iter()
 250        .skip_while(|edit| edit.old.end < source_excerpt_start_in_buffer)
 251        .take_while(|edit| edit.old.start <= source_excerpt_end_in_buffer)
 252        .map(|edit| {
 253            let clamped_source_start = edit.old.start.max(source_excerpt_start_in_buffer);
 254            let clamped_source_end = edit.old.end.min(source_excerpt_end_in_buffer);
 255            let source_multibuffer_start = source_excerpt_start_in_multibuffer
 256                + (clamped_source_start - source_excerpt_start_in_buffer);
 257            let source_multibuffer_end = source_excerpt_start_in_multibuffer
 258                + (clamped_source_end - source_excerpt_start_in_buffer);
 259            let clamped_target_start = edit
 260                .new
 261                .start
 262                .max(target_excerpt_start_in_buffer)
 263                .min(target_excerpt_end_in_buffer);
 264            let clamped_target_end = edit
 265                .new
 266                .end
 267                .max(target_excerpt_start_in_buffer)
 268                .min(target_excerpt_end_in_buffer);
 269            let target_multibuffer_start = target_excerpt_start_in_multibuffer
 270                + (clamped_target_start - target_excerpt_start_in_buffer);
 271            let target_multibuffer_end = target_excerpt_start_in_multibuffer
 272                + (clamped_target_end - target_excerpt_start_in_buffer);
 273            text::Edit {
 274                old: source_multibuffer_start..source_multibuffer_end,
 275                new: target_multibuffer_start..target_multibuffer_end,
 276            }
 277        });
 278
 279    let edits = [text::Edit {
 280        old: source_excerpt_start_in_multibuffer..source_excerpt_start_in_multibuffer,
 281        new: target_excerpt_start_in_multibuffer..target_excerpt_start_in_multibuffer,
 282    }]
 283    .into_iter()
 284    .chain(edits);
 285
 286    let mut merged_edits: Vec<text::Edit<Point>> = Vec::new();
 287    for edit in edits {
 288        if let Some(last) = merged_edits.last_mut() {
 289            if edit.new.start <= last.new.end {
 290                last.old.end = last.old.end.max(edit.old.end);
 291                last.new.end = last.new.end.max(edit.new.end);
 292                continue;
 293            }
 294        }
 295        merged_edits.push(edit);
 296    }
 297
 298    let edited_range = source_excerpt_start_in_multibuffer
 299        + (source_edited_range.start - source_excerpt_start_in_buffer)
 300        ..source_excerpt_start_in_multibuffer
 301            + (source_edited_range.end - source_excerpt_start_in_buffer);
 302
 303    let source_excerpt_end = source_excerpt_start_in_multibuffer
 304        + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer);
 305    let target_excerpt_end = target_excerpt_start_in_multibuffer
 306        + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer);
 307
 308    CompanionExcerptPatch {
 309        patch: Patch::new(merged_edits),
 310        edited_range,
 311        source_excerpt_range: source_excerpt_start_in_multibuffer..source_excerpt_end,
 312        target_excerpt_range: target_excerpt_start_in_multibuffer..target_excerpt_end,
 313    }
 314}
 315
 316pub struct SplitDiffFeatureFlag;
 317
 318impl FeatureFlag for SplitDiffFeatureFlag {
 319    const NAME: &'static str = "split-diff";
 320
 321    fn enabled_for_staff() -> bool {
 322        true
 323    }
 324}
 325
 326#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 327#[action(namespace = editor)]
 328pub struct SplitDiff;
 329
 330#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 331#[action(namespace = editor)]
 332struct UnsplitDiff;
 333
 334#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 335#[action(namespace = editor)]
 336pub struct ToggleSplitDiff;
 337
 338#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 339#[action(namespace = editor)]
 340struct JumpToCorrespondingRow;
 341
 342/// When locked cursors mode is enabled, cursor movements in one editor will
 343/// update the cursor position in the other editor to the corresponding row.
 344#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 345#[action(namespace = editor)]
 346pub struct ToggleLockedCursors;
 347
 348pub struct SplittableEditor {
 349    rhs_multibuffer: Entity<MultiBuffer>,
 350    rhs_editor: Entity<Editor>,
 351    lhs: Option<LhsEditor>,
 352    workspace: WeakEntity<Workspace>,
 353    split_state: Entity<SplitEditorState>,
 354    locked_cursors: bool,
 355    _subscriptions: Vec<Subscription>,
 356}
 357
 358struct LhsEditor {
 359    multibuffer: Entity<MultiBuffer>,
 360    editor: Entity<Editor>,
 361    was_last_focused: bool,
 362    _subscriptions: Vec<Subscription>,
 363}
 364
 365impl SplittableEditor {
 366    pub fn rhs_editor(&self) -> &Entity<Editor> {
 367        &self.rhs_editor
 368    }
 369
 370    pub fn lhs_editor(&self) -> Option<&Entity<Editor>> {
 371        self.lhs.as_ref().map(|s| &s.editor)
 372    }
 373
 374    pub fn is_split(&self) -> bool {
 375        self.lhs.is_some()
 376    }
 377
 378    pub fn set_render_diff_hunk_controls(
 379        &self,
 380        render_diff_hunk_controls: RenderDiffHunkControlsFn,
 381        cx: &mut Context<Self>,
 382    ) {
 383        self.rhs_editor.update(cx, |editor, cx| {
 384            editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
 385        });
 386
 387        if let Some(lhs) = &self.lhs {
 388            lhs.editor.update(cx, |editor, cx| {
 389                editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
 390            });
 391        }
 392    }
 393
 394    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 395        if let Some(lhs) = &self.lhs
 396            && lhs.was_last_focused
 397        {
 398            &lhs.editor
 399        } else {
 400            &self.rhs_editor
 401        }
 402    }
 403
 404    pub fn new(
 405        style: DiffViewStyle,
 406        rhs_multibuffer: Entity<MultiBuffer>,
 407        project: Entity<Project>,
 408        workspace: Entity<Workspace>,
 409        window: &mut Window,
 410        cx: &mut Context<Self>,
 411    ) -> Self {
 412        let rhs_editor = cx.new(|cx| {
 413            let mut editor =
 414                Editor::for_multibuffer(rhs_multibuffer.clone(), Some(project.clone()), window, cx);
 415            editor.set_expand_all_diff_hunks(cx);
 416            editor
 417        });
 418        // TODO(split-diff) we might want to tag editor events with whether they came from rhs/lhs
 419        let subscriptions = vec![
 420            cx.subscribe(
 421                &rhs_editor,
 422                |this, _, event: &EditorEvent, cx| match event {
 423                    EditorEvent::ExpandExcerptsRequested {
 424                        excerpt_ids,
 425                        lines,
 426                        direction,
 427                    } => {
 428                        this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
 429                    }
 430                    _ => cx.emit(event.clone()),
 431                },
 432            ),
 433            cx.subscribe(&rhs_editor, |_, _, event: &SearchEvent, cx| {
 434                cx.emit(event.clone());
 435            }),
 436        ];
 437
 438        let this = cx.weak_entity();
 439        window.defer(cx, {
 440            let workspace = workspace.downgrade();
 441            let rhs_editor = rhs_editor.downgrade();
 442            move |window, cx| {
 443                workspace
 444                    .update(cx, |workspace, cx| {
 445                        rhs_editor
 446                            .update(cx, |editor, cx| {
 447                                editor.added_to_workspace(workspace, window, cx);
 448                            })
 449                            .ok();
 450                    })
 451                    .ok();
 452                if style == DiffViewStyle::SideBySide {
 453                    this.update(cx, |this, cx| {
 454                        this.split(&Default::default(), window, cx);
 455                    })
 456                    .ok();
 457                }
 458            }
 459        });
 460        let split_state = cx.new(|cx| SplitEditorState::new(cx));
 461        Self {
 462            rhs_editor,
 463            rhs_multibuffer,
 464            lhs: None,
 465            workspace: workspace.downgrade(),
 466            split_state,
 467            locked_cursors: false,
 468            _subscriptions: subscriptions,
 469        }
 470    }
 471
 472    pub fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
 473        if !cx.has_flag::<SplitDiffFeatureFlag>() {
 474            return;
 475        }
 476        if self.lhs.is_some() {
 477            return;
 478        }
 479        let Some(workspace) = self.workspace.upgrade() else {
 480            return;
 481        };
 482        let project = workspace.read(cx).project().clone();
 483
 484        let lhs_multibuffer = cx.new(|cx| {
 485            let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
 486            multibuffer.set_all_diff_hunks_expanded(cx);
 487            multibuffer
 488        });
 489
 490        let render_diff_hunk_controls = self.rhs_editor.read(cx).render_diff_hunk_controls.clone();
 491        let lhs_editor = cx.new(|cx| {
 492            let mut editor =
 493                Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx);
 494            editor.set_number_deleted_lines(true, cx);
 495            editor.set_delegate_expand_excerpts(true);
 496            editor.set_delegate_stage_and_restore(true);
 497            editor.set_delegate_open_excerpts(true);
 498            editor.set_show_vertical_scrollbar(false, cx);
 499            editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
 500            editor
 501        });
 502
 503        lhs_editor.update(cx, |editor, cx| {
 504            editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
 505        });
 506
 507        let mut subscriptions = vec![cx.subscribe_in(
 508            &lhs_editor,
 509            window,
 510            |this, _, event: &EditorEvent, window, cx| match event {
 511                EditorEvent::ExpandExcerptsRequested {
 512                    excerpt_ids,
 513                    lines,
 514                    direction,
 515                } => {
 516                    if this.lhs.is_some() {
 517                        let rhs_display_map = this.rhs_editor.read(cx).display_map.read(cx);
 518                        let rhs_ids: Vec<_> = excerpt_ids
 519                            .iter()
 520                            .filter_map(|id| {
 521                                rhs_display_map.companion_excerpt_to_my_excerpt(*id, cx)
 522                            })
 523                            .collect();
 524                        this.expand_excerpts(rhs_ids.into_iter(), *lines, *direction, cx);
 525                    }
 526                }
 527                EditorEvent::StageOrUnstageRequested { stage, hunks } => {
 528                    if this.lhs.is_some() {
 529                        let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
 530                        if !translated.is_empty() {
 531                            let stage = *stage;
 532                            this.rhs_editor.update(cx, |editor, cx| {
 533                                let chunk_by = translated.into_iter().chunk_by(|h| h.buffer_id);
 534                                for (buffer_id, hunks) in &chunk_by {
 535                                    editor.do_stage_or_unstage(stage, buffer_id, hunks, cx);
 536                                }
 537                            });
 538                        }
 539                    }
 540                }
 541                EditorEvent::RestoreRequested { hunks } => {
 542                    if this.lhs.is_some() {
 543                        let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
 544                        if !translated.is_empty() {
 545                            this.rhs_editor.update(cx, |editor, cx| {
 546                                editor.restore_diff_hunks(translated, cx);
 547                            });
 548                        }
 549                    }
 550                }
 551                EditorEvent::OpenExcerptsRequested {
 552                    selections_by_buffer,
 553                    split,
 554                } => {
 555                    if this.lhs.is_some() {
 556                        let translated =
 557                            translate_lhs_selections_to_rhs(selections_by_buffer, this, cx);
 558                        if !translated.is_empty() {
 559                            let workspace = this.workspace.clone();
 560                            let split = *split;
 561                            Editor::open_buffers_in_workspace(
 562                                workspace, translated, split, window, cx,
 563                            );
 564                        }
 565                    }
 566                }
 567                _ => cx.emit(event.clone()),
 568            },
 569        )];
 570
 571        let lhs_focus_handle = lhs_editor.read(cx).focus_handle(cx);
 572        subscriptions.push(
 573            cx.on_focus_in(&lhs_focus_handle, window, |this, _window, cx| {
 574                if let Some(lhs) = &mut this.lhs {
 575                    if !lhs.was_last_focused {
 576                        lhs.was_last_focused = true;
 577                        cx.emit(SearchEvent::MatchesInvalidated);
 578                        cx.notify();
 579                    }
 580                }
 581            }),
 582        );
 583
 584        let rhs_focus_handle = self.rhs_editor.read(cx).focus_handle(cx);
 585        subscriptions.push(
 586            cx.on_focus_in(&rhs_focus_handle, window, |this, _window, cx| {
 587                if let Some(lhs) = &mut this.lhs {
 588                    if lhs.was_last_focused {
 589                        lhs.was_last_focused = false;
 590                        cx.emit(SearchEvent::MatchesInvalidated);
 591                        cx.notify();
 592                    }
 593                }
 594            }),
 595        );
 596
 597        let mut lhs = LhsEditor {
 598            editor: lhs_editor,
 599            multibuffer: lhs_multibuffer,
 600            was_last_focused: false,
 601            _subscriptions: subscriptions,
 602        };
 603        let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
 604        let lhs_display_map = lhs.editor.read(cx).display_map.clone();
 605        let rhs_display_map_id = rhs_display_map.entity_id();
 606
 607        self.rhs_editor.update(cx, |editor, cx| {
 608            editor.set_delegate_expand_excerpts(true);
 609            editor.buffer().update(cx, |rhs_multibuffer, cx| {
 610                rhs_multibuffer.set_show_deleted_hunks(false, cx);
 611                rhs_multibuffer.set_use_extended_diff_range(true, cx);
 612            })
 613        });
 614
 615        let path_diffs: Vec<_> = {
 616            let rhs_multibuffer = self.rhs_multibuffer.read(cx);
 617            rhs_multibuffer
 618                .paths()
 619                .filter_map(|path| {
 620                    let excerpt_id = rhs_multibuffer.excerpts_for_path(path).next()?;
 621                    let snapshot = rhs_multibuffer.snapshot(cx);
 622                    let buffer = snapshot.buffer_for_excerpt(excerpt_id)?;
 623                    let diff = rhs_multibuffer.diff_for(buffer.remote_id())?;
 624                    Some((path.clone(), diff))
 625                })
 626                .collect()
 627        };
 628
 629        let mut companion = Companion::new(
 630            rhs_display_map_id,
 631            convert_rhs_rows_to_lhs,
 632            convert_lhs_rows_to_rhs,
 633        );
 634
 635        // stream this
 636        for (path, diff) in path_diffs {
 637            for (lhs, rhs) in
 638                lhs.update_path_excerpts_from_rhs(path, &self.rhs_multibuffer, diff.clone(), cx)
 639            {
 640                companion.add_excerpt_mapping(lhs, rhs);
 641            }
 642            companion.add_buffer_mapping(
 643                diff.read(cx).base_text(cx).remote_id(),
 644                diff.read(cx).buffer_id,
 645            );
 646        }
 647
 648        let companion = cx.new(|_| companion);
 649
 650        rhs_display_map.update(cx, |dm, cx| {
 651            dm.set_companion(Some((lhs_display_map, companion.clone())), cx);
 652        });
 653
 654        let shared_scroll_anchor = self
 655            .rhs_editor
 656            .read(cx)
 657            .scroll_manager
 658            .scroll_anchor_entity();
 659        lhs.editor.update(cx, |editor, _cx| {
 660            editor
 661                .scroll_manager
 662                .set_shared_scroll_anchor(shared_scroll_anchor);
 663        });
 664
 665        let this = cx.entity().downgrade();
 666        self.rhs_editor.update(cx, |editor, _cx| {
 667            let this = this.clone();
 668            editor.set_on_local_selections_changed(Some(Box::new(
 669                move |cursor_position, window, cx| {
 670                    let this = this.clone();
 671                    window.defer(cx, move |window, cx| {
 672                        this.update(cx, |this, cx| {
 673                            if this.locked_cursors {
 674                                this.sync_cursor_to_other_side(true, cursor_position, window, cx);
 675                            }
 676                        })
 677                        .ok();
 678                    })
 679                },
 680            )));
 681        });
 682        lhs.editor.update(cx, |editor, _cx| {
 683            let this = this.clone();
 684            editor.set_on_local_selections_changed(Some(Box::new(
 685                move |cursor_position, window, cx| {
 686                    let this = this.clone();
 687                    window.defer(cx, move |window, cx| {
 688                        this.update(cx, |this, cx| {
 689                            if this.locked_cursors {
 690                                this.sync_cursor_to_other_side(false, cursor_position, window, cx);
 691                            }
 692                        })
 693                        .ok();
 694                    })
 695                },
 696            )));
 697        });
 698
 699        // Copy soft wrap state from rhs (source of truth) to lhs
 700        let rhs_soft_wrap_override = self.rhs_editor.read(cx).soft_wrap_mode_override;
 701        lhs.editor.update(cx, |editor, cx| {
 702            editor.soft_wrap_mode_override = rhs_soft_wrap_override;
 703            cx.notify();
 704        });
 705
 706        self.lhs = Some(lhs);
 707
 708        cx.notify();
 709    }
 710
 711    fn activate_pane_left(
 712        &mut self,
 713        _: &ActivatePaneLeft,
 714        window: &mut Window,
 715        cx: &mut Context<Self>,
 716    ) {
 717        if let Some(lhs) = &self.lhs {
 718            if !lhs.was_last_focused {
 719                lhs.editor.read(cx).focus_handle(cx).focus(window, cx);
 720                lhs.editor.update(cx, |editor, cx| {
 721                    editor.request_autoscroll(Autoscroll::fit(), cx);
 722                });
 723            } else {
 724                cx.propagate();
 725            }
 726        } else {
 727            cx.propagate();
 728        }
 729    }
 730
 731    fn activate_pane_right(
 732        &mut self,
 733        _: &ActivatePaneRight,
 734        window: &mut Window,
 735        cx: &mut Context<Self>,
 736    ) {
 737        if let Some(lhs) = &self.lhs {
 738            if lhs.was_last_focused {
 739                self.rhs_editor.read(cx).focus_handle(cx).focus(window, cx);
 740                self.rhs_editor.update(cx, |editor, cx| {
 741                    editor.request_autoscroll(Autoscroll::fit(), cx);
 742                });
 743            } else {
 744                cx.propagate();
 745            }
 746        } else {
 747            cx.propagate();
 748        }
 749    }
 750
 751    fn toggle_locked_cursors(
 752        &mut self,
 753        _: &ToggleLockedCursors,
 754        _window: &mut Window,
 755        cx: &mut Context<Self>,
 756    ) {
 757        self.locked_cursors = !self.locked_cursors;
 758        cx.notify();
 759    }
 760
 761    pub fn locked_cursors(&self) -> bool {
 762        self.locked_cursors
 763    }
 764
 765    fn sync_cursor_to_other_side(
 766        &mut self,
 767        from_rhs: bool,
 768        source_point: Point,
 769        window: &mut Window,
 770        cx: &mut Context<Self>,
 771    ) {
 772        let Some(lhs) = &self.lhs else {
 773            return;
 774        };
 775
 776        let target_editor = if from_rhs {
 777            &lhs.editor
 778        } else {
 779            &self.rhs_editor
 780        };
 781
 782        let (source_multibuffer, target_multibuffer) = if from_rhs {
 783            (&self.rhs_multibuffer, &lhs.multibuffer)
 784        } else {
 785            (&lhs.multibuffer, &self.rhs_multibuffer)
 786        };
 787
 788        let source_snapshot = source_multibuffer.read(cx).snapshot(cx);
 789        let target_snapshot = target_multibuffer.read(cx).snapshot(cx);
 790
 791        let target_range = target_editor.update(cx, |target_editor, cx| {
 792            target_editor.display_map.update(cx, |display_map, cx| {
 793                let display_map_id = cx.entity_id();
 794                display_map.companion().unwrap().update(cx, |companion, _| {
 795                    companion.convert_point_from_companion(
 796                        display_map_id,
 797                        &target_snapshot,
 798                        &source_snapshot,
 799                        source_point,
 800                    )
 801                })
 802            })
 803        });
 804
 805        target_editor.update(cx, |editor, cx| {
 806            editor.set_suppress_selection_callback(true);
 807            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
 808                s.select_ranges([target_range]);
 809            });
 810            editor.set_suppress_selection_callback(false);
 811        });
 812    }
 813
 814    fn toggle_split(&mut self, _: &ToggleSplitDiff, window: &mut Window, cx: &mut Context<Self>) {
 815        if self.lhs.is_some() {
 816            self.unsplit(&UnsplitDiff, window, cx);
 817        } else {
 818            self.split(&SplitDiff, window, cx);
 819        }
 820    }
 821
 822    fn intercept_toggle_code_actions(
 823        &mut self,
 824        _: &ToggleCodeActions,
 825        _window: &mut Window,
 826        cx: &mut Context<Self>,
 827    ) {
 828        if self.lhs.is_some() {
 829            cx.stop_propagation();
 830        } else {
 831            cx.propagate();
 832        }
 833    }
 834
 835    fn intercept_toggle_breakpoint(
 836        &mut self,
 837        _: &ToggleBreakpoint,
 838        _window: &mut Window,
 839        cx: &mut Context<Self>,
 840    ) {
 841        // Only block breakpoint actions when the left (lhs) editor has focus
 842        if let Some(lhs) = &self.lhs {
 843            if lhs.was_last_focused {
 844                cx.stop_propagation();
 845            } else {
 846                cx.propagate();
 847            }
 848        } else {
 849            cx.propagate();
 850        }
 851    }
 852
 853    fn intercept_enable_breakpoint(
 854        &mut self,
 855        _: &EnableBreakpoint,
 856        _window: &mut Window,
 857        cx: &mut Context<Self>,
 858    ) {
 859        // Only block breakpoint actions when the left (lhs) editor has focus
 860        if let Some(lhs) = &self.lhs {
 861            if lhs.was_last_focused {
 862                cx.stop_propagation();
 863            } else {
 864                cx.propagate();
 865            }
 866        } else {
 867            cx.propagate();
 868        }
 869    }
 870
 871    fn intercept_disable_breakpoint(
 872        &mut self,
 873        _: &DisableBreakpoint,
 874        _window: &mut Window,
 875        cx: &mut Context<Self>,
 876    ) {
 877        // Only block breakpoint actions when the left (lhs) editor has focus
 878        if let Some(lhs) = &self.lhs {
 879            if lhs.was_last_focused {
 880                cx.stop_propagation();
 881            } else {
 882                cx.propagate();
 883            }
 884        } else {
 885            cx.propagate();
 886        }
 887    }
 888
 889    fn intercept_edit_log_breakpoint(
 890        &mut self,
 891        _: &EditLogBreakpoint,
 892        _window: &mut Window,
 893        cx: &mut Context<Self>,
 894    ) {
 895        // Only block breakpoint actions when the left (lhs) editor has focus
 896        if let Some(lhs) = &self.lhs {
 897            if lhs.was_last_focused {
 898                cx.stop_propagation();
 899            } else {
 900                cx.propagate();
 901            }
 902        } else {
 903            cx.propagate();
 904        }
 905    }
 906
 907    fn intercept_inline_assist(
 908        &mut self,
 909        _: &InlineAssist,
 910        _window: &mut Window,
 911        cx: &mut Context<Self>,
 912    ) {
 913        if self.lhs.is_some() {
 914            cx.stop_propagation();
 915        } else {
 916            cx.propagate();
 917        }
 918    }
 919
 920    fn toggle_soft_wrap(
 921        &mut self,
 922        _: &ToggleSoftWrap,
 923        window: &mut Window,
 924        cx: &mut Context<Self>,
 925    ) {
 926        if let Some(lhs) = &self.lhs {
 927            cx.stop_propagation();
 928
 929            let is_lhs_focused = lhs.was_last_focused;
 930            let (focused_editor, other_editor) = if is_lhs_focused {
 931                (&lhs.editor, &self.rhs_editor)
 932            } else {
 933                (&self.rhs_editor, &lhs.editor)
 934            };
 935
 936            // Toggle the focused editor
 937            focused_editor.update(cx, |editor, cx| {
 938                editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
 939            });
 940
 941            // Copy the soft wrap state from the focused editor to the other editor
 942            let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
 943            other_editor.update(cx, |editor, cx| {
 944                editor.soft_wrap_mode_override = soft_wrap_override;
 945                cx.notify();
 946            });
 947        } else {
 948            cx.propagate();
 949        }
 950    }
 951
 952    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
 953        let Some(lhs) = self.lhs.take() else {
 954            return;
 955        };
 956        self.rhs_editor.update(cx, |rhs, cx| {
 957            let rhs_snapshot = rhs.display_map.update(cx, |dm, cx| dm.snapshot(cx));
 958            let native_anchor = rhs.scroll_manager.native_anchor(&rhs_snapshot, cx);
 959            let rhs_display_map_id = rhs_snapshot.display_map_id;
 960            rhs.scroll_manager
 961                .scroll_anchor_entity()
 962                .update(cx, |shared, _| {
 963                    shared.scroll_anchor = native_anchor;
 964                    shared.display_map_id = Some(rhs_display_map_id);
 965                });
 966
 967            rhs.set_on_local_selections_changed(None);
 968            rhs.set_delegate_expand_excerpts(false);
 969            rhs.buffer().update(cx, |buffer, cx| {
 970                buffer.set_show_deleted_hunks(true, cx);
 971                buffer.set_use_extended_diff_range(false, cx);
 972            });
 973            rhs.display_map.update(cx, |dm, cx| {
 974                dm.set_companion(None, cx);
 975            });
 976        });
 977        lhs.editor.update(cx, |editor, _cx| {
 978            editor.set_on_local_selections_changed(None);
 979        });
 980        cx.notify();
 981    }
 982
 983    pub fn set_excerpts_for_path(
 984        &mut self,
 985        path: PathKey,
 986        buffer: Entity<Buffer>,
 987        ranges: impl IntoIterator<Item = Range<Point>> + Clone,
 988        context_line_count: u32,
 989        diff: Entity<BufferDiff>,
 990        cx: &mut Context<Self>,
 991    ) -> (Vec<Range<Anchor>>, bool) {
 992        let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
 993        let lhs_display_map = self
 994            .lhs
 995            .as_ref()
 996            .map(|s| s.editor.read(cx).display_map.clone());
 997
 998        let (anchors, added_a_new_excerpt) =
 999            self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1000                let (anchors, added_a_new_excerpt) = rhs_multibuffer.set_excerpts_for_path(
1001                    path.clone(),
1002                    buffer.clone(),
1003                    ranges,
1004                    context_line_count,
1005                    cx,
1006                );
1007                if !anchors.is_empty()
1008                    && rhs_multibuffer
1009                        .diff_for(buffer.read(cx).remote_id())
1010                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1011                {
1012                    rhs_multibuffer.add_diff(diff.clone(), cx);
1013                }
1014                (anchors, added_a_new_excerpt)
1015            });
1016
1017        if let Some(lhs) = &mut self.lhs {
1018            if let Some(lhs_display_map) = &lhs_display_map {
1019                lhs.sync_path_excerpts(
1020                    path,
1021                    &self.rhs_multibuffer,
1022                    diff,
1023                    &rhs_display_map,
1024                    lhs_display_map,
1025                    cx,
1026                );
1027            }
1028        }
1029
1030        (anchors, added_a_new_excerpt)
1031    }
1032
1033    fn expand_excerpts(
1034        &mut self,
1035        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
1036        lines: u32,
1037        direction: ExpandExcerptDirection,
1038        cx: &mut Context<Self>,
1039    ) {
1040        let mut corresponding_paths = HashMap::default();
1041        self.rhs_multibuffer.update(cx, |multibuffer, cx| {
1042            let snapshot = multibuffer.snapshot(cx);
1043            if self.lhs.is_some() {
1044                corresponding_paths = excerpt_ids
1045                    .clone()
1046                    .map(|excerpt_id| {
1047                        let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
1048                        let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
1049                        let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
1050                        (path, diff)
1051                    })
1052                    .collect::<HashMap<_, _>>();
1053            }
1054            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
1055        });
1056
1057        if let Some(lhs) = &mut self.lhs {
1058            let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
1059            let lhs_display_map = lhs.editor.read(cx).display_map.clone();
1060            for (path, diff) in corresponding_paths {
1061                lhs.sync_path_excerpts(
1062                    path,
1063                    &self.rhs_multibuffer,
1064                    diff,
1065                    &rhs_display_map,
1066                    &lhs_display_map,
1067                    cx,
1068                );
1069            }
1070        }
1071    }
1072
1073    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
1074        self.rhs_multibuffer.update(cx, |buffer, cx| {
1075            buffer.remove_excerpts_for_path(path.clone(), cx)
1076        });
1077        if let Some(lhs) = &self.lhs {
1078            let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
1079            let lhs_display_map = lhs.editor.read(cx).display_map.clone();
1080            lhs.remove_mappings_for_path(
1081                &path,
1082                &self.rhs_multibuffer,
1083                &rhs_display_map,
1084                &lhs_display_map,
1085                cx,
1086            );
1087            lhs.multibuffer
1088                .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
1089        }
1090    }
1091}
1092
1093#[cfg(test)]
1094impl SplittableEditor {
1095    fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1096        use multi_buffer::MultiBufferRow;
1097        use text::Bias;
1098
1099        use crate::display_map::Block;
1100        use crate::display_map::DisplayRow;
1101
1102        self.debug_print(cx);
1103
1104        let lhs = self.lhs.as_ref().unwrap();
1105        let rhs_excerpts = self.rhs_multibuffer.read(cx).excerpt_ids();
1106        let lhs_excerpts = lhs.multibuffer.read(cx).excerpt_ids();
1107        assert_eq!(
1108            lhs_excerpts.len(),
1109            rhs_excerpts.len(),
1110            "mismatch in excerpt count"
1111        );
1112
1113        if quiesced {
1114            let rhs_snapshot = lhs
1115                .editor
1116                .update(cx, |editor, cx| editor.display_snapshot(cx));
1117            let lhs_snapshot = self
1118                .rhs_editor
1119                .update(cx, |editor, cx| editor.display_snapshot(cx));
1120
1121            let lhs_max_row = lhs_snapshot.max_point().row();
1122            let rhs_max_row = rhs_snapshot.max_point().row();
1123            assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1124
1125            let lhs_excerpt_block_rows = lhs_snapshot
1126                .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1127                .filter(|(_, block)| {
1128                    matches!(
1129                        block,
1130                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1131                    )
1132                })
1133                .map(|(row, _)| row)
1134                .collect::<Vec<_>>();
1135            let rhs_excerpt_block_rows = rhs_snapshot
1136                .blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
1137                .filter(|(_, block)| {
1138                    matches!(
1139                        block,
1140                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1141                    )
1142                })
1143                .map(|(row, _)| row)
1144                .collect::<Vec<_>>();
1145            assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1146
1147            for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1148                assert_eq!(
1149                    lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1150                    "mismatch in hunks"
1151                );
1152                assert_eq!(
1153                    lhs_hunk.status, rhs_hunk.status,
1154                    "mismatch in hunk statuses"
1155                );
1156
1157                let (lhs_point, rhs_point) =
1158                    if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1159                        (
1160                            Point::new(lhs_hunk.row_range.end.0, 0),
1161                            Point::new(rhs_hunk.row_range.end.0, 0),
1162                        )
1163                    } else {
1164                        (
1165                            Point::new(lhs_hunk.row_range.start.0, 0),
1166                            Point::new(rhs_hunk.row_range.start.0, 0),
1167                        )
1168                    };
1169                let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1170                let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1171                assert_eq!(
1172                    lhs_point.row(),
1173                    rhs_point.row(),
1174                    "mismatch in hunk position"
1175                );
1176            }
1177
1178            // Filtering out empty lines is a bit of a hack, to work around a case where
1179            // the base text has a trailing newline but the current text doesn't, or vice versa.
1180            // In this case, we get the additional newline on one side, but that line is not
1181            // marked as added/deleted by rowinfos.
1182            self.check_sides_match(cx, |snapshot| {
1183                snapshot
1184                    .buffer_snapshot()
1185                    .text()
1186                    .split("\n")
1187                    .zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
1188                    .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
1189                    .map(|(line, _)| line.to_owned())
1190                    .collect::<Vec<_>>()
1191            });
1192        }
1193    }
1194
1195    #[track_caller]
1196    fn check_sides_match<T: std::fmt::Debug + PartialEq>(
1197        &self,
1198        cx: &mut App,
1199        mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
1200    ) {
1201        let lhs = self.lhs.as_ref().expect("requires split");
1202        let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1203            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1204        });
1205        let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1206            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1207        });
1208
1209        let rhs_t = extract(&rhs_snapshot);
1210        let lhs_t = extract(&lhs_snapshot);
1211
1212        if rhs_t != lhs_t {
1213            self.debug_print(cx);
1214            pretty_assertions::assert_eq!(rhs_t, lhs_t);
1215        }
1216    }
1217
1218    fn debug_print(&self, cx: &mut App) {
1219        use crate::DisplayRow;
1220        use crate::display_map::Block;
1221        use buffer_diff::DiffHunkStatusKind;
1222
1223        assert!(
1224            self.lhs.is_some(),
1225            "debug_print is only useful when lhs editor exists"
1226        );
1227
1228        let lhs = self.lhs.as_ref().unwrap();
1229
1230        // Get terminal width, default to 80 if unavailable
1231        let terminal_width = std::env::var("COLUMNS")
1232            .ok()
1233            .and_then(|s| s.parse::<usize>().ok())
1234            .unwrap_or(80);
1235
1236        // Each side gets half the terminal width minus the separator
1237        let separator = "";
1238        let side_width = (terminal_width - separator.len()) / 2;
1239
1240        // Get display snapshots for both editors
1241        let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1242            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1243        });
1244        let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1245            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1246        });
1247
1248        let lhs_max_row = lhs_snapshot.max_point().row().0;
1249        let rhs_max_row = rhs_snapshot.max_point().row().0;
1250        let max_row = lhs_max_row.max(rhs_max_row);
1251
1252        // Build a map from display row -> block type string
1253        // Each row of a multi-row block gets an entry with the same block type
1254        // For spacers, the ID is included in brackets
1255        fn build_block_map(
1256            snapshot: &crate::DisplaySnapshot,
1257            max_row: u32,
1258        ) -> std::collections::HashMap<u32, String> {
1259            let mut block_map = std::collections::HashMap::new();
1260            for (start_row, block) in
1261                snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1262            {
1263                let (block_type, height) = match block {
1264                    Block::Spacer {
1265                        id,
1266                        height,
1267                        is_below: _,
1268                    } => (format!("SPACER[{}]", id.0), *height),
1269                    Block::ExcerptBoundary { height, .. } => {
1270                        ("EXCERPT_BOUNDARY".to_string(), *height)
1271                    }
1272                    Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1273                    Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1274                    Block::Custom(custom) => {
1275                        ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1276                    }
1277                };
1278                for offset in 0..height {
1279                    block_map.insert(start_row.0 + offset, block_type.clone());
1280                }
1281            }
1282            block_map
1283        }
1284
1285        let lhs_blocks = build_block_map(&lhs_snapshot, lhs_max_row);
1286        let rhs_blocks = build_block_map(&rhs_snapshot, rhs_max_row);
1287
1288        fn display_width(s: &str) -> usize {
1289            unicode_width::UnicodeWidthStr::width(s)
1290        }
1291
1292        fn truncate_line(line: &str, max_width: usize) -> String {
1293            let line_width = display_width(line);
1294            if line_width <= max_width {
1295                return line.to_string();
1296            }
1297            if max_width < 9 {
1298                let mut result = String::new();
1299                let mut width = 0;
1300                for c in line.chars() {
1301                    let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1302                    if width + c_width > max_width {
1303                        break;
1304                    }
1305                    result.push(c);
1306                    width += c_width;
1307                }
1308                return result;
1309            }
1310            let ellipsis = "...";
1311            let target_prefix_width = 3;
1312            let target_suffix_width = 3;
1313
1314            let mut prefix = String::new();
1315            let mut prefix_width = 0;
1316            for c in line.chars() {
1317                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1318                if prefix_width + c_width > target_prefix_width {
1319                    break;
1320                }
1321                prefix.push(c);
1322                prefix_width += c_width;
1323            }
1324
1325            let mut suffix_chars: Vec<char> = Vec::new();
1326            let mut suffix_width = 0;
1327            for c in line.chars().rev() {
1328                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1329                if suffix_width + c_width > target_suffix_width {
1330                    break;
1331                }
1332                suffix_chars.push(c);
1333                suffix_width += c_width;
1334            }
1335            suffix_chars.reverse();
1336            let suffix: String = suffix_chars.into_iter().collect();
1337
1338            format!("{}{}{}", prefix, ellipsis, suffix)
1339        }
1340
1341        fn pad_to_width(s: &str, target_width: usize) -> String {
1342            let current_width = display_width(s);
1343            if current_width >= target_width {
1344                s.to_string()
1345            } else {
1346                format!("{}{}", s, " ".repeat(target_width - current_width))
1347            }
1348        }
1349
1350        // Helper to format a single row for one side
1351        // Format: "ln# diff bytes(cumul) text" or block info
1352        // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1353        fn format_row(
1354            row: u32,
1355            max_row: u32,
1356            snapshot: &crate::DisplaySnapshot,
1357            blocks: &std::collections::HashMap<u32, String>,
1358            row_infos: &[multi_buffer::RowInfo],
1359            cumulative_bytes: &[usize],
1360            side_width: usize,
1361        ) -> String {
1362            // Get row info if available
1363            let row_info = row_infos.get(row as usize);
1364
1365            // Line number prefix (3 chars + space)
1366            // Use buffer_row from RowInfo, which is None for block rows
1367            let line_prefix = if row > max_row {
1368                "    ".to_string()
1369            } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1370                format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1371            } else {
1372                "    ".to_string() // block rows have no line number
1373            };
1374            let content_width = side_width.saturating_sub(line_prefix.len());
1375
1376            if row > max_row {
1377                return format!("{}{}", line_prefix, " ".repeat(content_width));
1378            }
1379
1380            // Check if this row is a block row
1381            if let Some(block_type) = blocks.get(&row) {
1382                let block_str = format!("~~~[{}]~~~", block_type);
1383                let formatted = format!("{:^width$}", block_str, width = content_width);
1384                return format!(
1385                    "{}{}",
1386                    line_prefix,
1387                    truncate_line(&formatted, content_width)
1388                );
1389            }
1390
1391            // Get line text
1392            let line_text = snapshot.line(DisplayRow(row));
1393            let line_bytes = line_text.len();
1394
1395            // Diff status marker
1396            let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1397                Some(status) => match status.kind {
1398                    DiffHunkStatusKind::Added => "+",
1399                    DiffHunkStatusKind::Deleted => "-",
1400                    DiffHunkStatusKind::Modified => "~",
1401                },
1402                None => " ",
1403            };
1404
1405            // Cumulative bytes
1406            let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1407
1408            // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1409            let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1410            let text_width = content_width.saturating_sub(info_prefix.len());
1411            let truncated_text = truncate_line(&line_text, text_width);
1412
1413            let text_part = pad_to_width(&truncated_text, text_width);
1414            format!("{}{}{}", line_prefix, info_prefix, text_part)
1415        }
1416
1417        // Collect row infos for both sides
1418        let lhs_row_infos: Vec<_> = lhs_snapshot
1419            .row_infos(DisplayRow(0))
1420            .take((lhs_max_row + 1) as usize)
1421            .collect();
1422        let rhs_row_infos: Vec<_> = rhs_snapshot
1423            .row_infos(DisplayRow(0))
1424            .take((rhs_max_row + 1) as usize)
1425            .collect();
1426
1427        // Calculate cumulative bytes for each side (only counting non-block rows)
1428        let mut lhs_cumulative = Vec::with_capacity((lhs_max_row + 1) as usize);
1429        let mut cumulative = 0usize;
1430        for row in 0..=lhs_max_row {
1431            if !lhs_blocks.contains_key(&row) {
1432                cumulative += lhs_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1433            }
1434            lhs_cumulative.push(cumulative);
1435        }
1436
1437        let mut rhs_cumulative = Vec::with_capacity((rhs_max_row + 1) as usize);
1438        cumulative = 0;
1439        for row in 0..=rhs_max_row {
1440            if !rhs_blocks.contains_key(&row) {
1441                cumulative += rhs_snapshot.line(DisplayRow(row)).len() + 1;
1442            }
1443            rhs_cumulative.push(cumulative);
1444        }
1445
1446        // Print header
1447        eprintln!();
1448        eprintln!("{}", "".repeat(terminal_width));
1449        let header_left = format!("{:^width$}", "(LHS)", width = side_width);
1450        let header_right = format!("{:^width$}", "(RHS)", width = side_width);
1451        eprintln!("{}{}{}", header_left, separator, header_right);
1452        eprintln!(
1453            "{:^width$}{}{:^width$}",
1454            "ln# diff len(cum) text",
1455            separator,
1456            "ln# diff len(cum) text",
1457            width = side_width
1458        );
1459        eprintln!("{}", "".repeat(terminal_width));
1460
1461        // Print each row
1462        for row in 0..=max_row {
1463            let left = format_row(
1464                row,
1465                lhs_max_row,
1466                &lhs_snapshot,
1467                &lhs_blocks,
1468                &lhs_row_infos,
1469                &lhs_cumulative,
1470                side_width,
1471            );
1472            let right = format_row(
1473                row,
1474                rhs_max_row,
1475                &rhs_snapshot,
1476                &rhs_blocks,
1477                &rhs_row_infos,
1478                &rhs_cumulative,
1479                side_width,
1480            );
1481            eprintln!("{}{}{}", left, separator, right);
1482        }
1483
1484        eprintln!("{}", "".repeat(terminal_width));
1485        eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1486        eprintln!();
1487    }
1488
1489    fn randomly_edit_excerpts(
1490        &mut self,
1491        rng: &mut impl rand::Rng,
1492        mutation_count: usize,
1493        cx: &mut Context<Self>,
1494    ) {
1495        use collections::HashSet;
1496        use rand::prelude::*;
1497        use std::env;
1498        use util::RandomCharIter;
1499
1500        let max_buffers = env::var("MAX_BUFFERS")
1501            .map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
1502            .unwrap_or(4);
1503
1504        for _ in 0..mutation_count {
1505            let paths = self
1506                .rhs_multibuffer
1507                .read(cx)
1508                .paths()
1509                .cloned()
1510                .collect::<Vec<_>>();
1511            let excerpt_ids = self.rhs_multibuffer.read(cx).excerpt_ids();
1512
1513            if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
1514                let mut excerpts = HashSet::default();
1515                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
1516                    excerpts.extend(excerpt_ids.choose(rng).copied());
1517                }
1518
1519                let line_count = rng.random_range(1..5);
1520
1521                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
1522
1523                self.expand_excerpts(
1524                    excerpts.iter().cloned(),
1525                    line_count,
1526                    ExpandExcerptDirection::UpAndDown,
1527                    cx,
1528                );
1529                continue;
1530            }
1531
1532            if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
1533                let len = rng.random_range(100..500);
1534                let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
1535                let buffer = cx.new(|cx| Buffer::local(text, cx));
1536                log::info!(
1537                    "Creating new buffer {} with text: {:?}",
1538                    buffer.read(cx).remote_id(),
1539                    buffer.read(cx).text()
1540                );
1541                let buffer_snapshot = buffer.read(cx).snapshot();
1542                let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
1543                // Create some initial diff hunks.
1544                buffer.update(cx, |buffer, cx| {
1545                    buffer.randomly_edit(rng, 1, cx);
1546                });
1547                let buffer_snapshot = buffer.read(cx).text_snapshot();
1548                diff.update(cx, |diff, cx| {
1549                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
1550                });
1551                let path = PathKey::for_buffer(&buffer, cx);
1552                let ranges = diff.update(cx, |diff, cx| {
1553                    diff.snapshot(cx)
1554                        .hunks(&buffer_snapshot)
1555                        .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
1556                        .collect::<Vec<_>>()
1557                });
1558                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1559            } else {
1560                log::info!("removing excerpts");
1561                let remove_count = rng.random_range(1..=paths.len());
1562                let paths_to_remove = paths
1563                    .choose_multiple(rng, remove_count)
1564                    .cloned()
1565                    .collect::<Vec<_>>();
1566                for path in paths_to_remove {
1567                    self.remove_excerpts_for_path(path.clone(), cx);
1568                }
1569            }
1570        }
1571    }
1572}
1573
1574impl Item for SplittableEditor {
1575    type Event = EditorEvent;
1576
1577    fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1578        self.rhs_editor.read(cx).tab_content_text(detail, cx)
1579    }
1580
1581    fn tab_tooltip_text(&self, cx: &App) -> Option<ui::SharedString> {
1582        self.rhs_editor.read(cx).tab_tooltip_text(cx)
1583    }
1584
1585    fn tab_icon(&self, window: &Window, cx: &App) -> Option<ui::Icon> {
1586        self.rhs_editor.read(cx).tab_icon(window, cx)
1587    }
1588
1589    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> gpui::AnyElement {
1590        self.rhs_editor.read(cx).tab_content(params, window, cx)
1591    }
1592
1593    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
1594        Editor::to_item_events(event, f)
1595    }
1596
1597    fn for_each_project_item(
1598        &self,
1599        cx: &App,
1600        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1601    ) {
1602        self.rhs_editor.read(cx).for_each_project_item(cx, f)
1603    }
1604
1605    fn buffer_kind(&self, cx: &App) -> ItemBufferKind {
1606        self.rhs_editor.read(cx).buffer_kind(cx)
1607    }
1608
1609    fn is_dirty(&self, cx: &App) -> bool {
1610        self.rhs_editor.read(cx).is_dirty(cx)
1611    }
1612
1613    fn has_conflict(&self, cx: &App) -> bool {
1614        self.rhs_editor.read(cx).has_conflict(cx)
1615    }
1616
1617    fn has_deleted_file(&self, cx: &App) -> bool {
1618        self.rhs_editor.read(cx).has_deleted_file(cx)
1619    }
1620
1621    fn capability(&self, cx: &App) -> language::Capability {
1622        self.rhs_editor.read(cx).capability(cx)
1623    }
1624
1625    fn can_save(&self, cx: &App) -> bool {
1626        self.rhs_editor.read(cx).can_save(cx)
1627    }
1628
1629    fn can_save_as(&self, cx: &App) -> bool {
1630        self.rhs_editor.read(cx).can_save_as(cx)
1631    }
1632
1633    fn save(
1634        &mut self,
1635        options: SaveOptions,
1636        project: Entity<Project>,
1637        window: &mut Window,
1638        cx: &mut Context<Self>,
1639    ) -> gpui::Task<anyhow::Result<()>> {
1640        self.rhs_editor
1641            .update(cx, |editor, cx| editor.save(options, project, window, cx))
1642    }
1643
1644    fn save_as(
1645        &mut self,
1646        project: Entity<Project>,
1647        path: project::ProjectPath,
1648        window: &mut Window,
1649        cx: &mut Context<Self>,
1650    ) -> gpui::Task<anyhow::Result<()>> {
1651        self.rhs_editor
1652            .update(cx, |editor, cx| editor.save_as(project, path, window, cx))
1653    }
1654
1655    fn reload(
1656        &mut self,
1657        project: Entity<Project>,
1658        window: &mut Window,
1659        cx: &mut Context<Self>,
1660    ) -> gpui::Task<anyhow::Result<()>> {
1661        self.rhs_editor
1662            .update(cx, |editor, cx| editor.reload(project, window, cx))
1663    }
1664
1665    fn navigate(
1666        &mut self,
1667        data: Arc<dyn std::any::Any + Send>,
1668        window: &mut Window,
1669        cx: &mut Context<Self>,
1670    ) -> bool {
1671        self.last_selected_editor()
1672            .update(cx, |editor, cx| editor.navigate(data, window, cx))
1673    }
1674
1675    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1676        self.last_selected_editor().update(cx, |editor, cx| {
1677            editor.deactivated(window, cx);
1678        });
1679    }
1680
1681    fn added_to_workspace(
1682        &mut self,
1683        workspace: &mut Workspace,
1684        window: &mut Window,
1685        cx: &mut Context<Self>,
1686    ) {
1687        self.workspace = workspace.weak_handle();
1688        self.rhs_editor.update(cx, |rhs_editor, cx| {
1689            rhs_editor.added_to_workspace(workspace, window, cx);
1690        });
1691        if let Some(lhs) = &self.lhs {
1692            lhs.editor.update(cx, |lhs_editor, cx| {
1693                lhs_editor.added_to_workspace(workspace, window, cx);
1694            });
1695        }
1696    }
1697
1698    fn as_searchable(
1699        &self,
1700        handle: &Entity<Self>,
1701        _: &App,
1702    ) -> Option<Box<dyn SearchableItemHandle>> {
1703        Some(Box::new(handle.clone()))
1704    }
1705
1706    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1707        self.rhs_editor.read(cx).breadcrumb_location(cx)
1708    }
1709
1710    fn breadcrumbs(&self, cx: &App) -> Option<Vec<BreadcrumbText>> {
1711        self.rhs_editor.read(cx).breadcrumbs(cx)
1712    }
1713
1714    fn pixel_position_of_cursor(&self, cx: &App) -> Option<gpui::Point<gpui::Pixels>> {
1715        self.last_selected_editor()
1716            .read(cx)
1717            .pixel_position_of_cursor(cx)
1718    }
1719}
1720
1721impl SearchableItem for SplittableEditor {
1722    type Match = Range<Anchor>;
1723
1724    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1725        self.last_selected_editor().update(cx, |editor, cx| {
1726            editor.clear_matches(window, cx);
1727        });
1728    }
1729
1730    fn update_matches(
1731        &mut self,
1732        matches: &[Self::Match],
1733        active_match_index: Option<usize>,
1734        window: &mut Window,
1735        cx: &mut Context<Self>,
1736    ) {
1737        self.last_selected_editor().update(cx, |editor, cx| {
1738            editor.update_matches(matches, active_match_index, window, cx);
1739        });
1740    }
1741
1742    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1743        self.last_selected_editor()
1744            .update(cx, |editor, cx| editor.query_suggestion(window, cx))
1745    }
1746
1747    fn activate_match(
1748        &mut self,
1749        index: usize,
1750        matches: &[Self::Match],
1751        window: &mut Window,
1752        cx: &mut Context<Self>,
1753    ) {
1754        self.last_selected_editor().update(cx, |editor, cx| {
1755            editor.activate_match(index, matches, window, cx);
1756        });
1757    }
1758
1759    fn select_matches(
1760        &mut self,
1761        matches: &[Self::Match],
1762        window: &mut Window,
1763        cx: &mut Context<Self>,
1764    ) {
1765        self.last_selected_editor().update(cx, |editor, cx| {
1766            editor.select_matches(matches, window, cx);
1767        });
1768    }
1769
1770    fn replace(
1771        &mut self,
1772        identifier: &Self::Match,
1773        query: &project::search::SearchQuery,
1774        window: &mut Window,
1775        cx: &mut Context<Self>,
1776    ) {
1777        self.last_selected_editor().update(cx, |editor, cx| {
1778            editor.replace(identifier, query, window, cx);
1779        });
1780    }
1781
1782    fn find_matches(
1783        &mut self,
1784        query: Arc<project::search::SearchQuery>,
1785        window: &mut Window,
1786        cx: &mut Context<Self>,
1787    ) -> gpui::Task<Vec<Self::Match>> {
1788        self.last_selected_editor()
1789            .update(cx, |editor, cx| editor.find_matches(query, window, cx))
1790    }
1791
1792    fn active_match_index(
1793        &mut self,
1794        direction: workspace::searchable::Direction,
1795        matches: &[Self::Match],
1796        window: &mut Window,
1797        cx: &mut Context<Self>,
1798    ) -> Option<usize> {
1799        self.last_selected_editor().update(cx, |editor, cx| {
1800            editor.active_match_index(direction, matches, window, cx)
1801        })
1802    }
1803}
1804
1805impl EventEmitter<EditorEvent> for SplittableEditor {}
1806impl EventEmitter<SearchEvent> for SplittableEditor {}
1807impl Focusable for SplittableEditor {
1808    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1809        self.last_selected_editor().read(cx).focus_handle(cx)
1810    }
1811}
1812
1813// impl Item for SplittableEditor {
1814//     type Event = EditorEvent;
1815
1816//     fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1817//         self.rhs_editor().tab_content_text(detail, cx)
1818//     }
1819
1820//     fn as_searchable(&self, _this: &Entity<Self>, cx: &App) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
1821//         Some(Box::new(self.last_selected_editor().clone()))
1822//     }
1823// }
1824
1825impl Render for SplittableEditor {
1826    fn render(
1827        &mut self,
1828        _window: &mut ui::Window,
1829        cx: &mut ui::Context<Self>,
1830    ) -> impl ui::IntoElement {
1831        let inner = if self.lhs.is_some() {
1832            let style = self.rhs_editor.read(cx).create_style(cx);
1833            SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
1834        } else {
1835            self.rhs_editor.clone().into_any_element()
1836        };
1837        div()
1838            .id("splittable-editor")
1839            .on_action(cx.listener(Self::split))
1840            .on_action(cx.listener(Self::unsplit))
1841            .on_action(cx.listener(Self::toggle_split))
1842            .on_action(cx.listener(Self::activate_pane_left))
1843            .on_action(cx.listener(Self::activate_pane_right))
1844            .on_action(cx.listener(Self::toggle_locked_cursors))
1845            .on_action(cx.listener(Self::intercept_toggle_code_actions))
1846            .on_action(cx.listener(Self::intercept_toggle_breakpoint))
1847            .on_action(cx.listener(Self::intercept_enable_breakpoint))
1848            .on_action(cx.listener(Self::intercept_disable_breakpoint))
1849            .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
1850            .on_action(cx.listener(Self::intercept_inline_assist))
1851            .capture_action(cx.listener(Self::toggle_soft_wrap))
1852            .size_full()
1853            .child(inner)
1854    }
1855}
1856
1857impl LhsEditor {
1858    fn update_path_excerpts_from_rhs(
1859        &mut self,
1860        path_key: PathKey,
1861        rhs_multibuffer: &Entity<MultiBuffer>,
1862        diff: Entity<BufferDiff>,
1863        cx: &mut App,
1864    ) -> Vec<(ExcerptId, ExcerptId)> {
1865        let rhs_multibuffer_ref = rhs_multibuffer.read(cx);
1866        let rhs_excerpt_ids: Vec<ExcerptId> =
1867            rhs_multibuffer_ref.excerpts_for_path(&path_key).collect();
1868
1869        let Some(excerpt_id) = rhs_multibuffer_ref.excerpts_for_path(&path_key).next() else {
1870            self.multibuffer.update(cx, |multibuffer, cx| {
1871                multibuffer.remove_excerpts_for_path(path_key, cx);
1872            });
1873            return Vec::new();
1874        };
1875
1876        let rhs_multibuffer_snapshot = rhs_multibuffer_ref.snapshot(cx);
1877        let main_buffer = rhs_multibuffer_snapshot
1878            .buffer_for_excerpt(excerpt_id)
1879            .unwrap();
1880        let base_text_buffer = diff.read(cx).base_text_buffer();
1881        let diff_snapshot = diff.read(cx).snapshot(cx);
1882        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
1883        let new = rhs_multibuffer_ref
1884            .excerpts_for_buffer(main_buffer.remote_id(), cx)
1885            .into_iter()
1886            .map(|(_, excerpt_range)| {
1887                let point_range_to_base_text_point_range = |range: Range<Point>| {
1888                    let start = diff_snapshot
1889                        .buffer_point_to_base_text_range(
1890                            Point::new(range.start.row, 0),
1891                            main_buffer,
1892                        )
1893                        .start;
1894                    let end = diff_snapshot
1895                        .buffer_point_to_base_text_range(Point::new(range.end.row, 0), main_buffer)
1896                        .end;
1897                    let end_column = diff_snapshot.base_text().line_len(end.row);
1898                    Point::new(start.row, 0)..Point::new(end.row, end_column)
1899                };
1900                let rhs = excerpt_range.primary.to_point(main_buffer);
1901                let context = excerpt_range.context.to_point(main_buffer);
1902                ExcerptRange {
1903                    primary: point_range_to_base_text_point_range(rhs),
1904                    context: point_range_to_base_text_point_range(context),
1905                }
1906            })
1907            .collect();
1908
1909        self.editor.update(cx, |editor, cx| {
1910            editor.buffer().update(cx, |buffer, cx| {
1911                let (ids, _) = buffer.update_path_excerpts(
1912                    path_key.clone(),
1913                    base_text_buffer.clone(),
1914                    &base_text_buffer_snapshot,
1915                    new,
1916                    cx,
1917                );
1918                if !ids.is_empty()
1919                    && buffer
1920                        .diff_for(base_text_buffer.read(cx).remote_id())
1921                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1922                {
1923                    buffer.add_inverted_diff(diff, cx);
1924                }
1925            })
1926        });
1927
1928        let lhs_excerpt_ids: Vec<ExcerptId> = self
1929            .multibuffer
1930            .read(cx)
1931            .excerpts_for_path(&path_key)
1932            .collect();
1933
1934        debug_assert_eq!(rhs_excerpt_ids.len(), lhs_excerpt_ids.len());
1935
1936        lhs_excerpt_ids.into_iter().zip(rhs_excerpt_ids).collect()
1937    }
1938
1939    fn sync_path_excerpts(
1940        &mut self,
1941        path_key: PathKey,
1942        rhs_multibuffer: &Entity<MultiBuffer>,
1943        diff: Entity<BufferDiff>,
1944        rhs_display_map: &Entity<DisplayMap>,
1945        lhs_display_map: &Entity<DisplayMap>,
1946        cx: &mut App,
1947    ) {
1948        self.remove_mappings_for_path(
1949            &path_key,
1950            rhs_multibuffer,
1951            rhs_display_map,
1952            lhs_display_map,
1953            cx,
1954        );
1955
1956        let mappings =
1957            self.update_path_excerpts_from_rhs(path_key, rhs_multibuffer, diff.clone(), cx);
1958
1959        let lhs_buffer_id = diff.read(cx).base_text(cx).remote_id();
1960        let rhs_buffer_id = diff.read(cx).buffer_id;
1961
1962        if let Some(companion) = rhs_display_map.read(cx).companion().cloned() {
1963            companion.update(cx, |c, _| {
1964                for (lhs, rhs) in mappings {
1965                    c.add_excerpt_mapping(lhs, rhs);
1966                }
1967                c.add_buffer_mapping(lhs_buffer_id, rhs_buffer_id);
1968            });
1969        }
1970    }
1971
1972    fn remove_mappings_for_path(
1973        &self,
1974        path_key: &PathKey,
1975        rhs_multibuffer: &Entity<MultiBuffer>,
1976        rhs_display_map: &Entity<DisplayMap>,
1977        _lhs_display_map: &Entity<DisplayMap>,
1978        cx: &mut App,
1979    ) {
1980        let rhs_excerpt_ids: Vec<ExcerptId> = rhs_multibuffer
1981            .read(cx)
1982            .excerpts_for_path(path_key)
1983            .collect();
1984        let lhs_excerpt_ids: Vec<ExcerptId> = self
1985            .multibuffer
1986            .read(cx)
1987            .excerpts_for_path(path_key)
1988            .collect();
1989
1990        if let Some(companion) = rhs_display_map.read(cx).companion().cloned() {
1991            companion.update(cx, |c, _| {
1992                c.remove_excerpt_mappings(lhs_excerpt_ids, rhs_excerpt_ids);
1993            });
1994        }
1995    }
1996}
1997
1998#[cfg(test)]
1999mod tests {
2000    use std::sync::Arc;
2001
2002    use buffer_diff::BufferDiff;
2003    use collections::{HashMap, HashSet};
2004    use fs::FakeFs;
2005    use gpui::Element as _;
2006    use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
2007    use language::language_settings::SoftWrap;
2008    use language::{Buffer, Capability};
2009    use multi_buffer::{MultiBuffer, PathKey};
2010    use pretty_assertions::assert_eq;
2011    use project::Project;
2012    use rand::rngs::StdRng;
2013    use settings::{DiffViewStyle, SettingsStore};
2014    use ui::{VisualContext as _, div, px};
2015    use workspace::Workspace;
2016
2017    use crate::SplittableEditor;
2018    use crate::display_map::{BlockPlacement, BlockProperties, BlockStyle};
2019    use crate::split::{SplitDiff, UnsplitDiff};
2020    use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
2021
2022    async fn init_test(
2023        cx: &mut gpui::TestAppContext,
2024        soft_wrap: SoftWrap,
2025        style: DiffViewStyle,
2026    ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
2027        cx.update(|cx| {
2028            let store = SettingsStore::test(cx);
2029            cx.set_global(store);
2030            theme::init(theme::LoadThemes::JustBase, cx);
2031            crate::init(cx);
2032        });
2033        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
2034        let (workspace, cx) =
2035            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2036        let rhs_multibuffer = cx.new(|cx| {
2037            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2038            multibuffer.set_all_diff_hunks_expanded(cx);
2039            multibuffer
2040        });
2041        let editor = cx.new_window_entity(|window, cx| {
2042            let editor = SplittableEditor::new(
2043                style,
2044                rhs_multibuffer.clone(),
2045                project.clone(),
2046                workspace,
2047                window,
2048                cx,
2049            );
2050            editor.rhs_editor.update(cx, |editor, cx| {
2051                editor.set_soft_wrap_mode(soft_wrap, cx);
2052            });
2053            if let Some(lhs) = &editor.lhs {
2054                lhs.editor.update(cx, |editor, cx| {
2055                    editor.set_soft_wrap_mode(soft_wrap, cx);
2056                });
2057            }
2058            editor
2059        });
2060        (editor, cx)
2061    }
2062
2063    fn buffer_with_diff(
2064        base_text: &str,
2065        current_text: &str,
2066        cx: &mut VisualTestContext,
2067    ) -> (Entity<Buffer>, Entity<BufferDiff>) {
2068        let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2069        let diff = cx.new(|cx| {
2070            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
2071        });
2072        (buffer, diff)
2073    }
2074
2075    #[track_caller]
2076    fn assert_split_content(
2077        editor: &Entity<SplittableEditor>,
2078        expected_rhs: String,
2079        expected_lhs: String,
2080        cx: &mut VisualTestContext,
2081    ) {
2082        assert_split_content_with_widths(
2083            editor,
2084            px(3000.0),
2085            px(3000.0),
2086            expected_rhs,
2087            expected_lhs,
2088            cx,
2089        );
2090    }
2091
2092    #[track_caller]
2093    fn assert_split_content_with_widths(
2094        editor: &Entity<SplittableEditor>,
2095        rhs_width: Pixels,
2096        lhs_width: Pixels,
2097        expected_rhs: String,
2098        expected_lhs: String,
2099        cx: &mut VisualTestContext,
2100    ) {
2101        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2102            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2103            (editor.rhs_editor.clone(), lhs.editor.clone())
2104        });
2105
2106        // Make sure both sides learn if the other has soft-wrapped
2107        let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2108        cx.run_until_parked();
2109        let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2110        cx.run_until_parked();
2111
2112        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2113        let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2114
2115        if rhs_content != expected_rhs || lhs_content != expected_lhs {
2116            editor.update(cx, |editor, cx| editor.debug_print(cx));
2117        }
2118
2119        assert_eq!(rhs_content, expected_rhs, "rhs");
2120        assert_eq!(lhs_content, expected_lhs, "lhs");
2121    }
2122
2123    #[gpui::test(iterations = 100)]
2124    async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2125        use rand::prelude::*;
2126
2127        let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
2128        let operations = std::env::var("OPERATIONS")
2129            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2130            .unwrap_or(10);
2131        let rng = &mut rng;
2132        for _ in 0..operations {
2133            let buffers = editor.update(cx, |editor, cx| {
2134                editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2135            });
2136
2137            if buffers.is_empty() {
2138                log::info!("adding excerpts to empty multibuffer");
2139                editor.update(cx, |editor, cx| {
2140                    editor.randomly_edit_excerpts(rng, 2, cx);
2141                    editor.check_invariants(true, cx);
2142                });
2143                continue;
2144            }
2145
2146            let mut quiesced = false;
2147
2148            match rng.random_range(0..100) {
2149                0..=44 => {
2150                    log::info!("randomly editing multibuffer");
2151                    editor.update(cx, |editor, cx| {
2152                        editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2153                            multibuffer.randomly_edit(rng, 5, cx);
2154                        })
2155                    })
2156                }
2157                45..=64 => {
2158                    log::info!("randomly undoing/redoing in single buffer");
2159                    let buffer = buffers.iter().choose(rng).unwrap();
2160                    buffer.update(cx, |buffer, cx| {
2161                        buffer.randomly_undo_redo(rng, cx);
2162                    });
2163                }
2164                65..=79 => {
2165                    log::info!("mutating excerpts");
2166                    editor.update(cx, |editor, cx| {
2167                        editor.randomly_edit_excerpts(rng, 2, cx);
2168                    });
2169                }
2170                _ => {
2171                    log::info!("quiescing");
2172                    for buffer in buffers {
2173                        let buffer_snapshot =
2174                            buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2175                        let diff = editor.update(cx, |editor, cx| {
2176                            editor
2177                                .rhs_multibuffer
2178                                .read(cx)
2179                                .diff_for(buffer.read(cx).remote_id())
2180                                .unwrap()
2181                        });
2182                        diff.update(cx, |diff, cx| {
2183                            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2184                        });
2185                        cx.run_until_parked();
2186                        let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2187                        let ranges = diff_snapshot
2188                            .hunks(&buffer_snapshot)
2189                            .map(|hunk| hunk.range)
2190                            .collect::<Vec<_>>();
2191                        editor.update(cx, |editor, cx| {
2192                            let path = PathKey::for_buffer(&buffer, cx);
2193                            editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2194                        });
2195                    }
2196                    quiesced = true;
2197                }
2198            }
2199
2200            editor.update(cx, |editor, cx| {
2201                editor.check_invariants(quiesced, cx);
2202            });
2203        }
2204    }
2205
2206    #[gpui::test]
2207    async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2208        use rope::Point;
2209        use unindent::Unindent as _;
2210
2211        let (editor, mut cx) =
2212            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
2213
2214        let base_text = "
2215            aaa
2216            bbb
2217            ccc
2218            ddd
2219            eee
2220            fff
2221        "
2222        .unindent();
2223        let current_text = "
2224            aaa
2225            ddd
2226            eee
2227            fff
2228        "
2229        .unindent();
2230
2231        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2232
2233        editor.update(cx, |editor, cx| {
2234            let path = PathKey::for_buffer(&buffer, cx);
2235            editor.set_excerpts_for_path(
2236                path,
2237                buffer.clone(),
2238                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2239                0,
2240                diff.clone(),
2241                cx,
2242            );
2243        });
2244
2245        cx.run_until_parked();
2246
2247        assert_split_content(
2248            &editor,
2249            "
2250            § <no file>
2251            § -----
2252            aaa
2253            § spacer
2254            § spacer
2255            ddd
2256            eee
2257            fff"
2258            .unindent(),
2259            "
2260            § <no file>
2261            § -----
2262            aaa
2263            bbb
2264            ccc
2265            ddd
2266            eee
2267            fff"
2268            .unindent(),
2269            &mut cx,
2270        );
2271
2272        buffer.update(cx, |buffer, cx| {
2273            buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2274        });
2275
2276        cx.run_until_parked();
2277
2278        assert_split_content(
2279            &editor,
2280            "
2281            § <no file>
2282            § -----
2283            aaa
2284            § spacer
2285            § spacer
2286            ddd
2287            eee
2288            FFF"
2289            .unindent(),
2290            "
2291            § <no file>
2292            § -----
2293            aaa
2294            bbb
2295            ccc
2296            ddd
2297            eee
2298            fff"
2299            .unindent(),
2300            &mut cx,
2301        );
2302
2303        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2304        diff.update(cx, |diff, cx| {
2305            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2306        });
2307
2308        cx.run_until_parked();
2309
2310        assert_split_content(
2311            &editor,
2312            "
2313            § <no file>
2314            § -----
2315            aaa
2316            § spacer
2317            § spacer
2318            ddd
2319            eee
2320            FFF"
2321            .unindent(),
2322            "
2323            § <no file>
2324            § -----
2325            aaa
2326            bbb
2327            ccc
2328            ddd
2329            eee
2330            fff"
2331            .unindent(),
2332            &mut cx,
2333        );
2334    }
2335
2336    #[gpui::test]
2337    async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2338        use rope::Point;
2339        use unindent::Unindent as _;
2340
2341        let (editor, mut cx) =
2342            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
2343
2344        let base_text1 = "
2345            aaa
2346            bbb
2347            ccc
2348            ddd
2349            eee"
2350        .unindent();
2351
2352        let base_text2 = "
2353            fff
2354            ggg
2355            hhh
2356            iii
2357            jjj"
2358        .unindent();
2359
2360        let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2361        let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2362
2363        editor.update(cx, |editor, cx| {
2364            let path1 = PathKey::for_buffer(&buffer1, cx);
2365            editor.set_excerpts_for_path(
2366                path1,
2367                buffer1.clone(),
2368                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2369                0,
2370                diff1.clone(),
2371                cx,
2372            );
2373            let path2 = PathKey::for_buffer(&buffer2, cx);
2374            editor.set_excerpts_for_path(
2375                path2,
2376                buffer2.clone(),
2377                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2378                1,
2379                diff2.clone(),
2380                cx,
2381            );
2382        });
2383
2384        cx.run_until_parked();
2385
2386        buffer1.update(cx, |buffer, cx| {
2387            buffer.edit(
2388                [
2389                    (Point::new(0, 0)..Point::new(1, 0), ""),
2390                    (Point::new(3, 0)..Point::new(4, 0), ""),
2391                ],
2392                None,
2393                cx,
2394            );
2395        });
2396        buffer2.update(cx, |buffer, cx| {
2397            buffer.edit(
2398                [
2399                    (Point::new(0, 0)..Point::new(1, 0), ""),
2400                    (Point::new(3, 0)..Point::new(4, 0), ""),
2401                ],
2402                None,
2403                cx,
2404            );
2405        });
2406
2407        cx.run_until_parked();
2408
2409        assert_split_content(
2410            &editor,
2411            "
2412            § <no file>
2413            § -----
2414            § spacer
2415            bbb
2416            ccc
2417            § spacer
2418            eee
2419            § <no file>
2420            § -----
2421            § spacer
2422            ggg
2423            hhh
2424            § spacer
2425            jjj"
2426            .unindent(),
2427            "
2428            § <no file>
2429            § -----
2430            aaa
2431            bbb
2432            ccc
2433            ddd
2434            eee
2435            § <no file>
2436            § -----
2437            fff
2438            ggg
2439            hhh
2440            iii
2441            jjj"
2442            .unindent(),
2443            &mut cx,
2444        );
2445
2446        let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2447        diff1.update(cx, |diff, cx| {
2448            diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2449        });
2450        let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2451        diff2.update(cx, |diff, cx| {
2452            diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2453        });
2454
2455        cx.run_until_parked();
2456
2457        assert_split_content(
2458            &editor,
2459            "
2460            § <no file>
2461            § -----
2462            § spacer
2463            bbb
2464            ccc
2465            § spacer
2466            eee
2467            § <no file>
2468            § -----
2469            § spacer
2470            ggg
2471            hhh
2472            § spacer
2473            jjj"
2474            .unindent(),
2475            "
2476            § <no file>
2477            § -----
2478            aaa
2479            bbb
2480            ccc
2481            ddd
2482            eee
2483            § <no file>
2484            § -----
2485            fff
2486            ggg
2487            hhh
2488            iii
2489            jjj"
2490            .unindent(),
2491            &mut cx,
2492        );
2493    }
2494
2495    #[gpui::test]
2496    async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2497        use rope::Point;
2498        use unindent::Unindent as _;
2499
2500        let (editor, mut cx) =
2501            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
2502
2503        let base_text = "
2504            aaa
2505            bbb
2506            ccc
2507            ddd
2508        "
2509        .unindent();
2510
2511        let current_text = "
2512            aaa
2513            NEW1
2514            NEW2
2515            ccc
2516            ddd
2517        "
2518        .unindent();
2519
2520        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2521
2522        editor.update(cx, |editor, cx| {
2523            let path = PathKey::for_buffer(&buffer, cx);
2524            editor.set_excerpts_for_path(
2525                path,
2526                buffer.clone(),
2527                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2528                0,
2529                diff.clone(),
2530                cx,
2531            );
2532        });
2533
2534        cx.run_until_parked();
2535
2536        assert_split_content(
2537            &editor,
2538            "
2539            § <no file>
2540            § -----
2541            aaa
2542            NEW1
2543            NEW2
2544            ccc
2545            ddd"
2546            .unindent(),
2547            "
2548            § <no file>
2549            § -----
2550            aaa
2551            bbb
2552            § spacer
2553            ccc
2554            ddd"
2555            .unindent(),
2556            &mut cx,
2557        );
2558
2559        buffer.update(cx, |buffer, cx| {
2560            buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2561        });
2562
2563        cx.run_until_parked();
2564
2565        assert_split_content(
2566            &editor,
2567            "
2568            § <no file>
2569            § -----
2570            aaa
2571            NEW1
2572            ccc
2573            ddd"
2574            .unindent(),
2575            "
2576            § <no file>
2577            § -----
2578            aaa
2579            bbb
2580            ccc
2581            ddd"
2582            .unindent(),
2583            &mut cx,
2584        );
2585
2586        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2587        diff.update(cx, |diff, cx| {
2588            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2589        });
2590
2591        cx.run_until_parked();
2592
2593        assert_split_content(
2594            &editor,
2595            "
2596            § <no file>
2597            § -----
2598            aaa
2599            NEW1
2600            ccc
2601            ddd"
2602            .unindent(),
2603            "
2604            § <no file>
2605            § -----
2606            aaa
2607            bbb
2608            ccc
2609            ddd"
2610            .unindent(),
2611            &mut cx,
2612        );
2613    }
2614
2615    #[gpui::test]
2616    async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2617        use rope::Point;
2618        use unindent::Unindent as _;
2619
2620        let (editor, mut cx) =
2621            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
2622
2623        let base_text = "
2624            aaa
2625            bbb
2626
2627
2628
2629
2630
2631            ccc
2632            ddd
2633        "
2634        .unindent();
2635        let current_text = "
2636            aaa
2637            bbb
2638
2639
2640
2641
2642
2643            CCC
2644            ddd
2645        "
2646        .unindent();
2647
2648        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2649
2650        editor.update(cx, |editor, cx| {
2651            let path = PathKey::for_buffer(&buffer, cx);
2652            editor.set_excerpts_for_path(
2653                path,
2654                buffer.clone(),
2655                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2656                0,
2657                diff.clone(),
2658                cx,
2659            );
2660        });
2661
2662        cx.run_until_parked();
2663
2664        buffer.update(cx, |buffer, cx| {
2665            buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2666        });
2667
2668        cx.run_until_parked();
2669
2670        assert_split_content(
2671            &editor,
2672            "
2673            § <no file>
2674            § -----
2675            aaa
2676            bbb
2677
2678
2679
2680
2681
2682
2683            CCC
2684            ddd"
2685            .unindent(),
2686            "
2687            § <no file>
2688            § -----
2689            aaa
2690            bbb
2691            § spacer
2692
2693
2694
2695
2696
2697            ccc
2698            ddd"
2699            .unindent(),
2700            &mut cx,
2701        );
2702
2703        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2704        diff.update(cx, |diff, cx| {
2705            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2706        });
2707
2708        cx.run_until_parked();
2709
2710        assert_split_content(
2711            &editor,
2712            "
2713            § <no file>
2714            § -----
2715            aaa
2716            bbb
2717
2718
2719
2720
2721
2722
2723            CCC
2724            ddd"
2725            .unindent(),
2726            "
2727            § <no file>
2728            § -----
2729            aaa
2730            bbb
2731
2732
2733
2734
2735
2736            ccc
2737            § spacer
2738            ddd"
2739            .unindent(),
2740            &mut cx,
2741        );
2742    }
2743
2744    #[gpui::test]
2745    async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2746        use git::Restore;
2747        use rope::Point;
2748        use unindent::Unindent as _;
2749
2750        let (editor, mut cx) =
2751            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
2752
2753        let base_text = "
2754            aaa
2755            bbb
2756            ccc
2757            ddd
2758            eee
2759        "
2760        .unindent();
2761        let current_text = "
2762            aaa
2763            ddd
2764            eee
2765        "
2766        .unindent();
2767
2768        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2769
2770        editor.update(cx, |editor, cx| {
2771            let path = PathKey::for_buffer(&buffer, cx);
2772            editor.set_excerpts_for_path(
2773                path,
2774                buffer.clone(),
2775                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2776                0,
2777                diff.clone(),
2778                cx,
2779            );
2780        });
2781
2782        cx.run_until_parked();
2783
2784        assert_split_content(
2785            &editor,
2786            "
2787            § <no file>
2788            § -----
2789            aaa
2790            § spacer
2791            § spacer
2792            ddd
2793            eee"
2794            .unindent(),
2795            "
2796            § <no file>
2797            § -----
2798            aaa
2799            bbb
2800            ccc
2801            ddd
2802            eee"
2803            .unindent(),
2804            &mut cx,
2805        );
2806
2807        let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
2808        cx.update_window_entity(&rhs_editor, |editor, window, cx| {
2809            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2810                s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2811            });
2812            editor.git_restore(&Restore, window, cx);
2813        });
2814
2815        cx.run_until_parked();
2816
2817        assert_split_content(
2818            &editor,
2819            "
2820            § <no file>
2821            § -----
2822            aaa
2823            bbb
2824            ccc
2825            ddd
2826            eee"
2827            .unindent(),
2828            "
2829            § <no file>
2830            § -----
2831            aaa
2832            bbb
2833            ccc
2834            ddd
2835            eee"
2836            .unindent(),
2837            &mut cx,
2838        );
2839
2840        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2841        diff.update(cx, |diff, cx| {
2842            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2843        });
2844
2845        cx.run_until_parked();
2846
2847        assert_split_content(
2848            &editor,
2849            "
2850            § <no file>
2851            § -----
2852            aaa
2853            bbb
2854            ccc
2855            ddd
2856            eee"
2857            .unindent(),
2858            "
2859            § <no file>
2860            § -----
2861            aaa
2862            bbb
2863            ccc
2864            ddd
2865            eee"
2866            .unindent(),
2867            &mut cx,
2868        );
2869    }
2870
2871    #[gpui::test]
2872    async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
2873        use rope::Point;
2874        use unindent::Unindent as _;
2875
2876        let (editor, mut cx) =
2877            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
2878
2879        let base_text = "
2880            aaa
2881            old1
2882            old2
2883            old3
2884            old4
2885            zzz
2886        "
2887        .unindent();
2888
2889        let current_text = "
2890            aaa
2891            new1
2892            new2
2893            new3
2894            new4
2895            zzz
2896        "
2897        .unindent();
2898
2899        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2900
2901        editor.update(cx, |editor, cx| {
2902            let path = PathKey::for_buffer(&buffer, cx);
2903            editor.set_excerpts_for_path(
2904                path,
2905                buffer.clone(),
2906                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2907                0,
2908                diff.clone(),
2909                cx,
2910            );
2911        });
2912
2913        cx.run_until_parked();
2914
2915        buffer.update(cx, |buffer, cx| {
2916            buffer.edit(
2917                [
2918                    (Point::new(2, 0)..Point::new(3, 0), ""),
2919                    (Point::new(4, 0)..Point::new(5, 0), ""),
2920                ],
2921                None,
2922                cx,
2923            );
2924        });
2925        cx.run_until_parked();
2926
2927        assert_split_content(
2928            &editor,
2929            "
2930            § <no file>
2931            § -----
2932            aaa
2933            new1
2934            new3
2935            § spacer
2936            § spacer
2937            zzz"
2938            .unindent(),
2939            "
2940            § <no file>
2941            § -----
2942            aaa
2943            old1
2944            old2
2945            old3
2946            old4
2947            zzz"
2948            .unindent(),
2949            &mut cx,
2950        );
2951
2952        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2953        diff.update(cx, |diff, cx| {
2954            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2955        });
2956
2957        cx.run_until_parked();
2958
2959        assert_split_content(
2960            &editor,
2961            "
2962            § <no file>
2963            § -----
2964            aaa
2965            new1
2966            new3
2967            § spacer
2968            § spacer
2969            zzz"
2970            .unindent(),
2971            "
2972            § <no file>
2973            § -----
2974            aaa
2975            old1
2976            old2
2977            old3
2978            old4
2979            zzz"
2980            .unindent(),
2981            &mut cx,
2982        );
2983    }
2984
2985    #[gpui::test]
2986    async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
2987        use rope::Point;
2988        use unindent::Unindent as _;
2989
2990        let (editor, mut cx) =
2991            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
2992
2993        let text = "aaaa bbbb cccc dddd eeee ffff";
2994
2995        let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
2996        let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
2997
2998        editor.update(cx, |editor, cx| {
2999            let end = Point::new(0, text.len() as u32);
3000            let path1 = PathKey::for_buffer(&buffer1, cx);
3001            editor.set_excerpts_for_path(
3002                path1,
3003                buffer1.clone(),
3004                vec![Point::new(0, 0)..end],
3005                0,
3006                diff1.clone(),
3007                cx,
3008            );
3009            let path2 = PathKey::for_buffer(&buffer2, cx);
3010            editor.set_excerpts_for_path(
3011                path2,
3012                buffer2.clone(),
3013                vec![Point::new(0, 0)..end],
3014                0,
3015                diff2.clone(),
3016                cx,
3017            );
3018        });
3019
3020        cx.run_until_parked();
3021
3022        assert_split_content_with_widths(
3023            &editor,
3024            px(200.0),
3025            px(400.0),
3026            "
3027            § <no file>
3028            § -----
3029            aaaa bbbb\x20
3030            cccc dddd\x20
3031            eeee ffff
3032            § <no file>
3033            § -----
3034            aaaa bbbb\x20
3035            cccc dddd\x20
3036            eeee ffff"
3037                .unindent(),
3038            "
3039            § <no file>
3040            § -----
3041            aaaa bbbb cccc dddd eeee ffff
3042            § spacer
3043            § spacer
3044            § <no file>
3045            § -----
3046            aaaa bbbb cccc dddd eeee ffff
3047            § spacer
3048            § spacer"
3049                .unindent(),
3050            &mut cx,
3051        );
3052    }
3053
3054    #[gpui::test]
3055    async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
3056        use rope::Point;
3057        use unindent::Unindent as _;
3058
3059        let (editor, mut cx) =
3060            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3061
3062        let base_text = "
3063            aaaa bbbb cccc dddd eeee ffff
3064            old line one
3065            old line two
3066        "
3067        .unindent();
3068
3069        let current_text = "
3070            aaaa bbbb cccc dddd eeee ffff
3071            new line
3072        "
3073        .unindent();
3074
3075        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3076
3077        editor.update(cx, |editor, cx| {
3078            let path = PathKey::for_buffer(&buffer, cx);
3079            editor.set_excerpts_for_path(
3080                path,
3081                buffer.clone(),
3082                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3083                0,
3084                diff.clone(),
3085                cx,
3086            );
3087        });
3088
3089        cx.run_until_parked();
3090
3091        assert_split_content_with_widths(
3092            &editor,
3093            px(200.0),
3094            px(400.0),
3095            "
3096            § <no file>
3097            § -----
3098            aaaa bbbb\x20
3099            cccc dddd\x20
3100            eeee ffff
3101            new line
3102            § spacer"
3103                .unindent(),
3104            "
3105            § <no file>
3106            § -----
3107            aaaa bbbb cccc dddd eeee ffff
3108            § spacer
3109            § spacer
3110            old line one
3111            old line two"
3112                .unindent(),
3113            &mut cx,
3114        );
3115    }
3116
3117    #[gpui::test]
3118    async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3119        use rope::Point;
3120        use unindent::Unindent as _;
3121
3122        let (editor, mut cx) =
3123            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3124
3125        let base_text = "
3126            aaaa bbbb cccc dddd eeee ffff
3127            deleted line one
3128            deleted line two
3129            after
3130        "
3131        .unindent();
3132
3133        let current_text = "
3134            aaaa bbbb cccc dddd eeee ffff
3135            after
3136        "
3137        .unindent();
3138
3139        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3140
3141        editor.update(cx, |editor, cx| {
3142            let path = PathKey::for_buffer(&buffer, cx);
3143            editor.set_excerpts_for_path(
3144                path,
3145                buffer.clone(),
3146                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3147                0,
3148                diff.clone(),
3149                cx,
3150            );
3151        });
3152
3153        cx.run_until_parked();
3154
3155        assert_split_content_with_widths(
3156            &editor,
3157            px(400.0),
3158            px(200.0),
3159            "
3160            § <no file>
3161            § -----
3162            aaaa bbbb cccc dddd eeee ffff
3163            § spacer
3164            § spacer
3165            § spacer
3166            § spacer
3167            § spacer
3168            § spacer
3169            after"
3170                .unindent(),
3171            "
3172            § <no file>
3173            § -----
3174            aaaa bbbb\x20
3175            cccc dddd\x20
3176            eeee ffff
3177            deleted line\x20
3178            one
3179            deleted line\x20
3180            two
3181            after"
3182                .unindent(),
3183            &mut cx,
3184        );
3185    }
3186
3187    #[gpui::test]
3188    async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3189        use rope::Point;
3190        use unindent::Unindent as _;
3191
3192        let (editor, mut cx) =
3193            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3194
3195        let text = "
3196            aaaa bbbb cccc dddd eeee ffff
3197            short
3198        "
3199        .unindent();
3200
3201        let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3202
3203        editor.update(cx, |editor, cx| {
3204            let path = PathKey::for_buffer(&buffer, cx);
3205            editor.set_excerpts_for_path(
3206                path,
3207                buffer.clone(),
3208                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3209                0,
3210                diff.clone(),
3211                cx,
3212            );
3213        });
3214
3215        cx.run_until_parked();
3216
3217        assert_split_content_with_widths(
3218            &editor,
3219            px(400.0),
3220            px(200.0),
3221            "
3222            § <no file>
3223            § -----
3224            aaaa bbbb cccc dddd eeee ffff
3225            § spacer
3226            § spacer
3227            short"
3228                .unindent(),
3229            "
3230            § <no file>
3231            § -----
3232            aaaa bbbb\x20
3233            cccc dddd\x20
3234            eeee ffff
3235            short"
3236                .unindent(),
3237            &mut cx,
3238        );
3239
3240        buffer.update(cx, |buffer, cx| {
3241            buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3242        });
3243
3244        cx.run_until_parked();
3245
3246        assert_split_content_with_widths(
3247            &editor,
3248            px(400.0),
3249            px(200.0),
3250            "
3251            § <no file>
3252            § -----
3253            aaaa bbbb cccc dddd eeee ffff
3254            § spacer
3255            § spacer
3256            modified"
3257                .unindent(),
3258            "
3259            § <no file>
3260            § -----
3261            aaaa bbbb\x20
3262            cccc dddd\x20
3263            eeee ffff
3264            short"
3265                .unindent(),
3266            &mut cx,
3267        );
3268
3269        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3270        diff.update(cx, |diff, cx| {
3271            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3272        });
3273
3274        cx.run_until_parked();
3275
3276        assert_split_content_with_widths(
3277            &editor,
3278            px(400.0),
3279            px(200.0),
3280            "
3281            § <no file>
3282            § -----
3283            aaaa bbbb cccc dddd eeee ffff
3284            § spacer
3285            § spacer
3286            modified"
3287                .unindent(),
3288            "
3289            § <no file>
3290            § -----
3291            aaaa bbbb\x20
3292            cccc dddd\x20
3293            eeee ffff
3294            short"
3295                .unindent(),
3296            &mut cx,
3297        );
3298    }
3299
3300    #[gpui::test]
3301    async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3302        use rope::Point;
3303        use unindent::Unindent as _;
3304
3305        let (editor, mut cx) =
3306            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3307
3308        let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3309
3310        let current_text = "
3311            aaa
3312            bbb
3313            ccc
3314        "
3315        .unindent();
3316
3317        let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3318        let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3319
3320        editor.update(cx, |editor, cx| {
3321            let path1 = PathKey::for_buffer(&buffer1, cx);
3322            editor.set_excerpts_for_path(
3323                path1,
3324                buffer1.clone(),
3325                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3326                0,
3327                diff1.clone(),
3328                cx,
3329            );
3330
3331            let path2 = PathKey::for_buffer(&buffer2, cx);
3332            editor.set_excerpts_for_path(
3333                path2,
3334                buffer2.clone(),
3335                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3336                1,
3337                diff2.clone(),
3338                cx,
3339            );
3340        });
3341
3342        cx.run_until_parked();
3343
3344        assert_split_content(
3345            &editor,
3346            "
3347            § <no file>
3348            § -----
3349            xxx
3350            yyy
3351            § <no file>
3352            § -----
3353            aaa
3354            bbb
3355            ccc"
3356            .unindent(),
3357            "
3358            § <no file>
3359            § -----
3360            xxx
3361            yyy
3362            § <no file>
3363            § -----
3364            § spacer
3365            § spacer
3366            § spacer"
3367                .unindent(),
3368            &mut cx,
3369        );
3370
3371        buffer1.update(cx, |buffer, cx| {
3372            buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3373        });
3374
3375        cx.run_until_parked();
3376
3377        assert_split_content(
3378            &editor,
3379            "
3380            § <no file>
3381            § -----
3382            xxxz
3383            yyy
3384            § <no file>
3385            § -----
3386            aaa
3387            bbb
3388            ccc"
3389            .unindent(),
3390            "
3391            § <no file>
3392            § -----
3393            xxx
3394            yyy
3395            § <no file>
3396            § -----
3397            § spacer
3398            § spacer
3399            § spacer"
3400                .unindent(),
3401            &mut cx,
3402        );
3403    }
3404
3405    #[gpui::test]
3406    async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3407        use rope::Point;
3408        use unindent::Unindent as _;
3409
3410        let (editor, mut cx) =
3411            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3412
3413        let base_text = "
3414            aaa
3415            bbb
3416            ccc
3417        "
3418        .unindent();
3419
3420        let current_text = "
3421            NEW1
3422            NEW2
3423            ccc
3424        "
3425        .unindent();
3426
3427        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3428
3429        editor.update(cx, |editor, cx| {
3430            let path = PathKey::for_buffer(&buffer, cx);
3431            editor.set_excerpts_for_path(
3432                path,
3433                buffer.clone(),
3434                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3435                0,
3436                diff.clone(),
3437                cx,
3438            );
3439        });
3440
3441        cx.run_until_parked();
3442
3443        assert_split_content(
3444            &editor,
3445            "
3446            § <no file>
3447            § -----
3448            NEW1
3449            NEW2
3450            ccc"
3451            .unindent(),
3452            "
3453            § <no file>
3454            § -----
3455            aaa
3456            bbb
3457            ccc"
3458            .unindent(),
3459            &mut cx,
3460        );
3461
3462        buffer.update(cx, |buffer, cx| {
3463            buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3464        });
3465
3466        cx.run_until_parked();
3467
3468        assert_split_content(
3469            &editor,
3470            "
3471            § <no file>
3472            § -----
3473            NEW1
3474            NEW
3475            ccc"
3476            .unindent(),
3477            "
3478            § <no file>
3479            § -----
3480            aaa
3481            bbb
3482            ccc"
3483            .unindent(),
3484            &mut cx,
3485        );
3486    }
3487
3488    #[gpui::test]
3489    async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3490        use rope::Point;
3491        use unindent::Unindent as _;
3492
3493        let (editor, mut cx) =
3494            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3495
3496        let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3497
3498        let current_text = "
3499            aaaa bbbb cccc dddd eeee ffff
3500            added line
3501        "
3502        .unindent();
3503
3504        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3505
3506        editor.update(cx, |editor, cx| {
3507            let path = PathKey::for_buffer(&buffer, cx);
3508            editor.set_excerpts_for_path(
3509                path,
3510                buffer.clone(),
3511                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3512                0,
3513                diff.clone(),
3514                cx,
3515            );
3516        });
3517
3518        cx.run_until_parked();
3519
3520        assert_split_content_with_widths(
3521            &editor,
3522            px(400.0),
3523            px(200.0),
3524            "
3525            § <no file>
3526            § -----
3527            aaaa bbbb cccc dddd eeee ffff
3528            § spacer
3529            § spacer
3530            added line"
3531                .unindent(),
3532            "
3533            § <no file>
3534            § -----
3535            aaaa bbbb\x20
3536            cccc dddd\x20
3537            eeee ffff
3538            § spacer"
3539                .unindent(),
3540            &mut cx,
3541        );
3542
3543        assert_split_content_with_widths(
3544            &editor,
3545            px(200.0),
3546            px(400.0),
3547            "
3548            § <no file>
3549            § -----
3550            aaaa bbbb\x20
3551            cccc dddd\x20
3552            eeee ffff
3553            added line"
3554                .unindent(),
3555            "
3556            § <no file>
3557            § -----
3558            aaaa bbbb cccc dddd eeee ffff
3559            § spacer
3560            § spacer
3561            § spacer"
3562                .unindent(),
3563            &mut cx,
3564        );
3565    }
3566
3567    #[gpui::test]
3568    #[ignore]
3569    async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3570        use rope::Point;
3571        use unindent::Unindent as _;
3572
3573        let (editor, mut cx) =
3574            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3575
3576        let base_text = "
3577            aaa
3578            bbb
3579            ccc
3580            ddd
3581            eee
3582        "
3583        .unindent();
3584
3585        let current_text = "
3586            aaa
3587            NEW
3588            eee
3589        "
3590        .unindent();
3591
3592        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3593
3594        editor.update(cx, |editor, cx| {
3595            let path = PathKey::for_buffer(&buffer, cx);
3596            editor.set_excerpts_for_path(
3597                path,
3598                buffer.clone(),
3599                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3600                0,
3601                diff.clone(),
3602                cx,
3603            );
3604        });
3605
3606        cx.run_until_parked();
3607
3608        assert_split_content(
3609            &editor,
3610            "
3611            § <no file>
3612            § -----
3613            aaa
3614            NEW
3615            § spacer
3616            § spacer
3617            eee"
3618            .unindent(),
3619            "
3620            § <no file>
3621            § -----
3622            aaa
3623            bbb
3624            ccc
3625            ddd
3626            eee"
3627            .unindent(),
3628            &mut cx,
3629        );
3630
3631        buffer.update(cx, |buffer, cx| {
3632            buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3633        });
3634
3635        cx.run_until_parked();
3636
3637        assert_split_content(
3638            &editor,
3639            "
3640            § <no file>
3641            § -----
3642            aaa
3643            § spacer
3644            § spacer
3645            § spacer
3646            NEWeee"
3647                .unindent(),
3648            "
3649            § <no file>
3650            § -----
3651            aaa
3652            bbb
3653            ccc
3654            ddd
3655            eee"
3656            .unindent(),
3657            &mut cx,
3658        );
3659
3660        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3661        diff.update(cx, |diff, cx| {
3662            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3663        });
3664
3665        cx.run_until_parked();
3666
3667        assert_split_content(
3668            &editor,
3669            "
3670            § <no file>
3671            § -----
3672            aaa
3673            NEWeee
3674            § spacer
3675            § spacer
3676            § spacer"
3677                .unindent(),
3678            "
3679            § <no file>
3680            § -----
3681            aaa
3682            bbb
3683            ccc
3684            ddd
3685            eee"
3686            .unindent(),
3687            &mut cx,
3688        );
3689    }
3690
3691    #[gpui::test]
3692    async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3693        use rope::Point;
3694        use unindent::Unindent as _;
3695
3696        let (editor, mut cx) =
3697            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3698
3699        let base_text = "";
3700        let current_text = "
3701            aaaa bbbb cccc dddd eeee ffff
3702            bbb
3703            ccc
3704        "
3705        .unindent();
3706
3707        let (buffer, diff) = buffer_with_diff(base_text, &current_text, &mut cx);
3708
3709        editor.update(cx, |editor, cx| {
3710            let path = PathKey::for_buffer(&buffer, cx);
3711            editor.set_excerpts_for_path(
3712                path,
3713                buffer.clone(),
3714                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3715                0,
3716                diff.clone(),
3717                cx,
3718            );
3719        });
3720
3721        cx.run_until_parked();
3722
3723        assert_split_content(
3724            &editor,
3725            "
3726            § <no file>
3727            § -----
3728            aaaa bbbb cccc dddd eeee ffff
3729            bbb
3730            ccc"
3731            .unindent(),
3732            "
3733            § <no file>
3734            § -----
3735            § spacer
3736            § spacer
3737            § spacer"
3738                .unindent(),
3739            &mut cx,
3740        );
3741
3742        assert_split_content_with_widths(
3743            &editor,
3744            px(200.0),
3745            px(200.0),
3746            "
3747            § <no file>
3748            § -----
3749            aaaa bbbb\x20
3750            cccc dddd\x20
3751            eeee ffff
3752            bbb
3753            ccc"
3754            .unindent(),
3755            "
3756            § <no file>
3757            § -----
3758            § spacer
3759            § spacer
3760            § spacer
3761            § spacer
3762            § spacer"
3763                .unindent(),
3764            &mut cx,
3765        );
3766    }
3767
3768    #[gpui::test]
3769    async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
3770        use rope::Point;
3771        use unindent::Unindent as _;
3772
3773        let (editor, mut cx) =
3774            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3775
3776        let base_text = "
3777            aaa
3778            bbb
3779            ccc
3780        "
3781        .unindent();
3782
3783        let current_text = "
3784            aaa
3785            bbb
3786            xxx
3787            yyy
3788            ccc
3789        "
3790        .unindent();
3791
3792        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3793
3794        editor.update(cx, |editor, cx| {
3795            let path = PathKey::for_buffer(&buffer, cx);
3796            editor.set_excerpts_for_path(
3797                path,
3798                buffer.clone(),
3799                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3800                0,
3801                diff.clone(),
3802                cx,
3803            );
3804        });
3805
3806        cx.run_until_parked();
3807
3808        assert_split_content(
3809            &editor,
3810            "
3811            § <no file>
3812            § -----
3813            aaa
3814            bbb
3815            xxx
3816            yyy
3817            ccc"
3818            .unindent(),
3819            "
3820            § <no file>
3821            § -----
3822            aaa
3823            bbb
3824            § spacer
3825            § spacer
3826            ccc"
3827            .unindent(),
3828            &mut cx,
3829        );
3830
3831        buffer.update(cx, |buffer, cx| {
3832            buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
3833        });
3834
3835        cx.run_until_parked();
3836
3837        assert_split_content(
3838            &editor,
3839            "
3840            § <no file>
3841            § -----
3842            aaa
3843            bbb
3844            xxx
3845            yyy
3846            zzz
3847            ccc"
3848            .unindent(),
3849            "
3850            § <no file>
3851            § -----
3852            aaa
3853            bbb
3854            § spacer
3855            § spacer
3856            § spacer
3857            ccc"
3858            .unindent(),
3859            &mut cx,
3860        );
3861    }
3862
3863    #[gpui::test]
3864    async fn test_scrolling(cx: &mut gpui::TestAppContext) {
3865        use crate::test::editor_content_with_blocks_and_size;
3866        use gpui::size;
3867        use rope::Point;
3868
3869        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::SideBySide).await;
3870
3871        let long_line = "x".repeat(200);
3872        let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
3873        lines[25] = long_line;
3874        let content = lines.join("\n");
3875
3876        let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
3877
3878        editor.update(cx, |editor, cx| {
3879            let path = PathKey::for_buffer(&buffer, cx);
3880            editor.set_excerpts_for_path(
3881                path,
3882                buffer.clone(),
3883                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3884                0,
3885                diff.clone(),
3886                cx,
3887            );
3888        });
3889
3890        cx.run_until_parked();
3891
3892        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
3893            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
3894            (editor.rhs_editor.clone(), lhs.editor.clone())
3895        });
3896
3897        rhs_editor.update_in(cx, |e, window, cx| {
3898            e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
3899        });
3900
3901        let rhs_pos =
3902            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3903        let lhs_pos =
3904            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3905        assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
3906        assert_eq!(
3907            lhs_pos.y, rhs_pos.y,
3908            "LHS should have same scroll position as RHS after set_scroll_position"
3909        );
3910
3911        let draw_size = size(px(300.), px(300.));
3912
3913        rhs_editor.update_in(cx, |e, window, cx| {
3914            e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
3915                s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
3916            });
3917        });
3918
3919        let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
3920        cx.run_until_parked();
3921        let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
3922        cx.run_until_parked();
3923
3924        let rhs_pos =
3925            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3926        let lhs_pos =
3927            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3928
3929        assert!(
3930            rhs_pos.y > 0.,
3931            "RHS should have scrolled vertically to show cursor at row 25"
3932        );
3933        assert!(
3934            rhs_pos.x > 0.,
3935            "RHS should have scrolled horizontally to show cursor at column 150"
3936        );
3937        assert_eq!(
3938            lhs_pos.y, rhs_pos.y,
3939            "LHS should have same vertical scroll position as RHS after autoscroll"
3940        );
3941        assert_eq!(
3942            lhs_pos.x, rhs_pos.x,
3943            "LHS should have same horizontal scroll position as RHS after autoscroll"
3944        )
3945    }
3946
3947    #[gpui::test]
3948    async fn test_edit_line_before_soft_wrapped_line_preceding_hunk(cx: &mut gpui::TestAppContext) {
3949        use rope::Point;
3950        use unindent::Unindent as _;
3951
3952        let (editor, mut cx) =
3953            init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::SideBySide).await;
3954
3955        let base_text = "
3956            first line
3957            aaaa bbbb cccc dddd eeee ffff
3958            original
3959        "
3960        .unindent();
3961
3962        let current_text = "
3963            first line
3964            aaaa bbbb cccc dddd eeee ffff
3965            modified
3966        "
3967        .unindent();
3968
3969        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3970
3971        editor.update(cx, |editor, cx| {
3972            let path = PathKey::for_buffer(&buffer, cx);
3973            editor.set_excerpts_for_path(
3974                path,
3975                buffer.clone(),
3976                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3977                0,
3978                diff.clone(),
3979                cx,
3980            );
3981        });
3982
3983        cx.run_until_parked();
3984
3985        assert_split_content_with_widths(
3986            &editor,
3987            px(400.0),
3988            px(200.0),
3989            "
3990                    § <no file>
3991                    § -----
3992                    first line
3993                    aaaa bbbb cccc dddd eeee ffff
3994                    § spacer
3995                    § spacer
3996                    modified"
3997                .unindent(),
3998            "
3999                    § <no file>
4000                    § -----
4001                    first line
4002                    aaaa bbbb\x20
4003                    cccc dddd\x20
4004                    eeee ffff
4005                    original"
4006                .unindent(),
4007            &mut cx,
4008        );
4009
4010        buffer.update(cx, |buffer, cx| {
4011            buffer.edit(
4012                [(Point::new(0, 0)..Point::new(0, 10), "edited first")],
4013                None,
4014                cx,
4015            );
4016        });
4017
4018        cx.run_until_parked();
4019
4020        assert_split_content_with_widths(
4021            &editor,
4022            px(400.0),
4023            px(200.0),
4024            "
4025                    § <no file>
4026                    § -----
4027                    edited first
4028                    aaaa bbbb cccc dddd eeee ffff
4029                    § spacer
4030                    § spacer
4031                    modified"
4032                .unindent(),
4033            "
4034                    § <no file>
4035                    § -----
4036                    first line
4037                    aaaa bbbb\x20
4038                    cccc dddd\x20
4039                    eeee ffff
4040                    original"
4041                .unindent(),
4042            &mut cx,
4043        );
4044
4045        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4046        diff.update(cx, |diff, cx| {
4047            diff.recalculate_diff_sync(&buffer_snapshot, cx);
4048        });
4049
4050        cx.run_until_parked();
4051
4052        assert_split_content_with_widths(
4053            &editor,
4054            px(400.0),
4055            px(200.0),
4056            "
4057                    § <no file>
4058                    § -----
4059                    edited first
4060                    aaaa bbbb cccc dddd eeee ffff
4061                    § spacer
4062                    § spacer
4063                    modified"
4064                .unindent(),
4065            "
4066                    § <no file>
4067                    § -----
4068                    first line
4069                    aaaa bbbb\x20
4070                    cccc dddd\x20
4071                    eeee ffff
4072                    original"
4073                .unindent(),
4074            &mut cx,
4075        );
4076    }
4077
4078    #[gpui::test]
4079    async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
4080        use rope::Point;
4081        use unindent::Unindent as _;
4082
4083        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::SideBySide).await;
4084
4085        let base_text = "
4086            bbb
4087            ccc
4088        "
4089        .unindent();
4090        let current_text = "
4091            aaa
4092            bbb
4093            ccc
4094        "
4095        .unindent();
4096
4097        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4098
4099        editor.update(cx, |editor, cx| {
4100            let path = PathKey::for_buffer(&buffer, cx);
4101            editor.set_excerpts_for_path(
4102                path,
4103                buffer.clone(),
4104                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4105                0,
4106                diff.clone(),
4107                cx,
4108            );
4109        });
4110
4111        cx.run_until_parked();
4112
4113        assert_split_content(
4114            &editor,
4115            "
4116            § <no file>
4117            § -----
4118            aaa
4119            bbb
4120            ccc"
4121            .unindent(),
4122            "
4123            § <no file>
4124            § -----
4125            § spacer
4126            bbb
4127            ccc"
4128            .unindent(),
4129            &mut cx,
4130        );
4131
4132        let block_ids = editor.update(cx, |splittable_editor, cx| {
4133            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4134                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4135                let anchor = snapshot.anchor_before(Point::new(2, 0));
4136                rhs_editor.insert_blocks(
4137                    [BlockProperties {
4138                        placement: BlockPlacement::Above(anchor),
4139                        height: Some(1),
4140                        style: BlockStyle::Fixed,
4141                        render: Arc::new(|_| div().into_any()),
4142                        priority: 0,
4143                    }],
4144                    None,
4145                    cx,
4146                )
4147            })
4148        });
4149
4150        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4151        let lhs_editor =
4152            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4153
4154        cx.update(|_, cx| {
4155            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4156                "custom block".to_string()
4157            });
4158        });
4159
4160        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
4161            let display_map = lhs_editor.display_map.read(cx);
4162            let companion = display_map.companion().unwrap().read(cx);
4163            let mapping = companion
4164                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4165            *mapping.borrow().get(&block_ids[0]).unwrap()
4166        });
4167
4168        cx.update(|_, cx| {
4169            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
4170                "custom block".to_string()
4171            });
4172        });
4173
4174        cx.run_until_parked();
4175
4176        assert_split_content(
4177            &editor,
4178            "
4179            § <no file>
4180            § -----
4181            aaa
4182            bbb
4183            § custom block
4184            ccc"
4185            .unindent(),
4186            "
4187            § <no file>
4188            § -----
4189            § spacer
4190            bbb
4191            § custom block
4192            ccc"
4193            .unindent(),
4194            &mut cx,
4195        );
4196
4197        editor.update(cx, |splittable_editor, cx| {
4198            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4199                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
4200            });
4201        });
4202
4203        cx.run_until_parked();
4204
4205        assert_split_content(
4206            &editor,
4207            "
4208            § <no file>
4209            § -----
4210            aaa
4211            bbb
4212            ccc"
4213            .unindent(),
4214            "
4215            § <no file>
4216            § -----
4217            § spacer
4218            bbb
4219            ccc"
4220            .unindent(),
4221            &mut cx,
4222        );
4223    }
4224
4225    #[gpui::test]
4226    async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
4227        use rope::Point;
4228        use unindent::Unindent as _;
4229
4230        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::SideBySide).await;
4231
4232        let base_text = "
4233            bbb
4234            ccc
4235        "
4236        .unindent();
4237        let current_text = "
4238            aaa
4239            bbb
4240            ccc
4241        "
4242        .unindent();
4243
4244        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4245
4246        editor.update(cx, |editor, cx| {
4247            let path = PathKey::for_buffer(&buffer, cx);
4248            editor.set_excerpts_for_path(
4249                path,
4250                buffer.clone(),
4251                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4252                0,
4253                diff.clone(),
4254                cx,
4255            );
4256        });
4257
4258        cx.run_until_parked();
4259
4260        assert_split_content(
4261            &editor,
4262            "
4263            § <no file>
4264            § -----
4265            aaa
4266            bbb
4267            ccc"
4268            .unindent(),
4269            "
4270            § <no file>
4271            § -----
4272            § spacer
4273            bbb
4274            ccc"
4275            .unindent(),
4276            &mut cx,
4277        );
4278
4279        let block_ids = editor.update(cx, |splittable_editor, cx| {
4280            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4281                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4282                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4283                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4284                rhs_editor.insert_blocks(
4285                    [
4286                        BlockProperties {
4287                            placement: BlockPlacement::Above(anchor1),
4288                            height: Some(1),
4289                            style: BlockStyle::Fixed,
4290                            render: Arc::new(|_| div().into_any()),
4291                            priority: 0,
4292                        },
4293                        BlockProperties {
4294                            placement: BlockPlacement::Above(anchor2),
4295                            height: Some(1),
4296                            style: BlockStyle::Fixed,
4297                            render: Arc::new(|_| div().into_any()),
4298                            priority: 0,
4299                        },
4300                    ],
4301                    None,
4302                    cx,
4303                )
4304            })
4305        });
4306
4307        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4308        let lhs_editor =
4309            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4310
4311        cx.update(|_, cx| {
4312            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4313                "custom block 1".to_string()
4314            });
4315            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4316                "custom block 2".to_string()
4317            });
4318        });
4319
4320        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4321            let display_map = lhs_editor.display_map.read(cx);
4322            let companion = display_map.companion().unwrap().read(cx);
4323            let mapping = companion
4324                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4325            (
4326                *mapping.borrow().get(&block_ids[0]).unwrap(),
4327                *mapping.borrow().get(&block_ids[1]).unwrap(),
4328            )
4329        });
4330
4331        cx.update(|_, cx| {
4332            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4333                "custom block 1".to_string()
4334            });
4335            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4336                "custom block 2".to_string()
4337            });
4338        });
4339
4340        cx.run_until_parked();
4341
4342        assert_split_content(
4343            &editor,
4344            "
4345            § <no file>
4346            § -----
4347            aaa
4348            bbb
4349            § custom block 1
4350            ccc
4351            § custom block 2"
4352                .unindent(),
4353            "
4354            § <no file>
4355            § -----
4356            § spacer
4357            bbb
4358            § custom block 1
4359            ccc
4360            § custom block 2"
4361                .unindent(),
4362            &mut cx,
4363        );
4364
4365        editor.update(cx, |splittable_editor, cx| {
4366            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4367                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4368            });
4369        });
4370
4371        cx.run_until_parked();
4372
4373        assert_split_content(
4374            &editor,
4375            "
4376            § <no file>
4377            § -----
4378            aaa
4379            bbb
4380            ccc
4381            § custom block 2"
4382                .unindent(),
4383            "
4384            § <no file>
4385            § -----
4386            § spacer
4387            bbb
4388            ccc
4389            § custom block 2"
4390                .unindent(),
4391            &mut cx,
4392        );
4393
4394        editor.update_in(cx, |splittable_editor, window, cx| {
4395            splittable_editor.unsplit(&UnsplitDiff, window, cx);
4396        });
4397
4398        cx.run_until_parked();
4399
4400        editor.update_in(cx, |splittable_editor, window, cx| {
4401            splittable_editor.split(&SplitDiff, window, cx);
4402        });
4403
4404        cx.run_until_parked();
4405
4406        let lhs_editor =
4407            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4408
4409        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4410            let display_map = lhs_editor.display_map.read(cx);
4411            let companion = display_map.companion().unwrap().read(cx);
4412            let mapping = companion
4413                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4414            *mapping.borrow().get(&block_ids[1]).unwrap()
4415        });
4416
4417        cx.update(|_, cx| {
4418            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4419                "custom block 2".to_string()
4420            });
4421        });
4422
4423        cx.run_until_parked();
4424
4425        assert_split_content(
4426            &editor,
4427            "
4428            § <no file>
4429            § -----
4430            aaa
4431            bbb
4432            ccc
4433            § custom block 2"
4434                .unindent(),
4435            "
4436            § <no file>
4437            § -----
4438            § spacer
4439            bbb
4440            ccc
4441            § custom block 2"
4442                .unindent(),
4443            &mut cx,
4444        );
4445    }
4446
4447    #[gpui::test]
4448    async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
4449        use rope::Point;
4450        use unindent::Unindent as _;
4451
4452        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::SideBySide).await;
4453
4454        let base_text = "
4455            bbb
4456            ccc
4457        "
4458        .unindent();
4459        let current_text = "
4460            aaa
4461            bbb
4462            ccc
4463        "
4464        .unindent();
4465
4466        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4467
4468        editor.update(cx, |editor, cx| {
4469            let path = PathKey::for_buffer(&buffer, cx);
4470            editor.set_excerpts_for_path(
4471                path,
4472                buffer.clone(),
4473                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4474                0,
4475                diff.clone(),
4476                cx,
4477            );
4478        });
4479
4480        cx.run_until_parked();
4481
4482        editor.update_in(cx, |splittable_editor, window, cx| {
4483            splittable_editor.unsplit(&UnsplitDiff, window, cx);
4484        });
4485
4486        cx.run_until_parked();
4487
4488        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4489
4490        let block_ids = editor.update(cx, |splittable_editor, cx| {
4491            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4492                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4493                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4494                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4495                rhs_editor.insert_blocks(
4496                    [
4497                        BlockProperties {
4498                            placement: BlockPlacement::Above(anchor1),
4499                            height: Some(1),
4500                            style: BlockStyle::Fixed,
4501                            render: Arc::new(|_| div().into_any()),
4502                            priority: 0,
4503                        },
4504                        BlockProperties {
4505                            placement: BlockPlacement::Above(anchor2),
4506                            height: Some(1),
4507                            style: BlockStyle::Fixed,
4508                            render: Arc::new(|_| div().into_any()),
4509                            priority: 0,
4510                        },
4511                    ],
4512                    None,
4513                    cx,
4514                )
4515            })
4516        });
4517
4518        cx.update(|_, cx| {
4519            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4520                "custom block 1".to_string()
4521            });
4522            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4523                "custom block 2".to_string()
4524            });
4525        });
4526
4527        cx.run_until_parked();
4528
4529        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
4530        assert_eq!(
4531            rhs_content,
4532            "
4533            § <no file>
4534            § -----
4535            aaa
4536            bbb
4537            § custom block 1
4538            ccc
4539            § custom block 2"
4540                .unindent(),
4541            "rhs content before split"
4542        );
4543
4544        editor.update_in(cx, |splittable_editor, window, cx| {
4545            splittable_editor.split(&SplitDiff, window, cx);
4546        });
4547
4548        cx.run_until_parked();
4549
4550        let lhs_editor =
4551            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4552
4553        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4554            let display_map = lhs_editor.display_map.read(cx);
4555            let companion = display_map.companion().unwrap().read(cx);
4556            let mapping = companion
4557                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4558            (
4559                *mapping.borrow().get(&block_ids[0]).unwrap(),
4560                *mapping.borrow().get(&block_ids[1]).unwrap(),
4561            )
4562        });
4563
4564        cx.update(|_, cx| {
4565            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4566                "custom block 1".to_string()
4567            });
4568            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4569                "custom block 2".to_string()
4570            });
4571        });
4572
4573        cx.run_until_parked();
4574
4575        assert_split_content(
4576            &editor,
4577            "
4578            § <no file>
4579            § -----
4580            aaa
4581            bbb
4582            § custom block 1
4583            ccc
4584            § custom block 2"
4585                .unindent(),
4586            "
4587            § <no file>
4588            § -----
4589            § spacer
4590            bbb
4591            § custom block 1
4592            ccc
4593            § custom block 2"
4594                .unindent(),
4595            &mut cx,
4596        );
4597
4598        editor.update(cx, |splittable_editor, cx| {
4599            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4600                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4601            });
4602        });
4603
4604        cx.run_until_parked();
4605
4606        assert_split_content(
4607            &editor,
4608            "
4609            § <no file>
4610            § -----
4611            aaa
4612            bbb
4613            ccc
4614            § custom block 2"
4615                .unindent(),
4616            "
4617            § <no file>
4618            § -----
4619            § spacer
4620            bbb
4621            ccc
4622            § custom block 2"
4623                .unindent(),
4624            &mut cx,
4625        );
4626
4627        editor.update_in(cx, |splittable_editor, window, cx| {
4628            splittable_editor.unsplit(&UnsplitDiff, window, cx);
4629        });
4630
4631        cx.run_until_parked();
4632
4633        editor.update_in(cx, |splittable_editor, window, cx| {
4634            splittable_editor.split(&SplitDiff, window, cx);
4635        });
4636
4637        cx.run_until_parked();
4638
4639        let lhs_editor =
4640            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4641
4642        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4643            let display_map = lhs_editor.display_map.read(cx);
4644            let companion = display_map.companion().unwrap().read(cx);
4645            let mapping = companion
4646                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4647            *mapping.borrow().get(&block_ids[1]).unwrap()
4648        });
4649
4650        cx.update(|_, cx| {
4651            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4652                "custom block 2".to_string()
4653            });
4654        });
4655
4656        cx.run_until_parked();
4657
4658        assert_split_content(
4659            &editor,
4660            "
4661            § <no file>
4662            § -----
4663            aaa
4664            bbb
4665            ccc
4666            § custom block 2"
4667                .unindent(),
4668            "
4669            § <no file>
4670            § -----
4671            § spacer
4672            bbb
4673            ccc
4674            § custom block 2"
4675                .unindent(),
4676            &mut cx,
4677        );
4678
4679        let new_block_ids = editor.update(cx, |splittable_editor, cx| {
4680            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4681                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4682                let anchor = snapshot.anchor_before(Point::new(2, 0));
4683                rhs_editor.insert_blocks(
4684                    [BlockProperties {
4685                        placement: BlockPlacement::Above(anchor),
4686                        height: Some(1),
4687                        style: BlockStyle::Fixed,
4688                        render: Arc::new(|_| div().into_any()),
4689                        priority: 0,
4690                    }],
4691                    None,
4692                    cx,
4693                )
4694            })
4695        });
4696
4697        cx.update(|_, cx| {
4698            set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
4699                "custom block 3".to_string()
4700            });
4701        });
4702
4703        let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4704            let display_map = lhs_editor.display_map.read(cx);
4705            let companion = display_map.companion().unwrap().read(cx);
4706            let mapping = companion
4707                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4708            *mapping.borrow().get(&new_block_ids[0]).unwrap()
4709        });
4710
4711        cx.update(|_, cx| {
4712            set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
4713                "custom block 3".to_string()
4714            });
4715        });
4716
4717        cx.run_until_parked();
4718
4719        assert_split_content(
4720            &editor,
4721            "
4722            § <no file>
4723            § -----
4724            aaa
4725            bbb
4726            § custom block 3
4727            ccc
4728            § custom block 2"
4729                .unindent(),
4730            "
4731            § <no file>
4732            § -----
4733            § spacer
4734            bbb
4735            § custom block 3
4736            ccc
4737            § custom block 2"
4738                .unindent(),
4739            &mut cx,
4740        );
4741
4742        editor.update(cx, |splittable_editor, cx| {
4743            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4744                rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
4745            });
4746        });
4747
4748        cx.run_until_parked();
4749
4750        assert_split_content(
4751            &editor,
4752            "
4753            § <no file>
4754            § -----
4755            aaa
4756            bbb
4757            ccc
4758            § custom block 2"
4759                .unindent(),
4760            "
4761            § <no file>
4762            § -----
4763            § spacer
4764            bbb
4765            ccc
4766            § custom block 2"
4767                .unindent(),
4768            &mut cx,
4769        );
4770    }
4771
4772    #[gpui::test]
4773    async fn test_buffer_folding_sync(cx: &mut gpui::TestAppContext) {
4774        use rope::Point;
4775        use unindent::Unindent as _;
4776
4777        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Stacked).await;
4778
4779        let base_text1 = "
4780            aaa
4781            bbb
4782            ccc"
4783        .unindent();
4784        let current_text1 = "
4785            aaa
4786            bbb
4787            ccc"
4788        .unindent();
4789
4790        let base_text2 = "
4791            ddd
4792            eee
4793            fff"
4794        .unindent();
4795        let current_text2 = "
4796            ddd
4797            eee
4798            fff"
4799        .unindent();
4800
4801        let (buffer1, diff1) = buffer_with_diff(&base_text1, &current_text1, &mut cx);
4802        let (buffer2, diff2) = buffer_with_diff(&base_text2, &current_text2, &mut cx);
4803
4804        let buffer1_id = buffer1.read_with(cx, |buffer, _| buffer.remote_id());
4805        let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
4806
4807        editor.update(cx, |editor, cx| {
4808            let path1 = PathKey::for_buffer(&buffer1, cx);
4809            editor.set_excerpts_for_path(
4810                path1,
4811                buffer1.clone(),
4812                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
4813                0,
4814                diff1.clone(),
4815                cx,
4816            );
4817            let path2 = PathKey::for_buffer(&buffer2, cx);
4818            editor.set_excerpts_for_path(
4819                path2,
4820                buffer2.clone(),
4821                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
4822                1,
4823                diff2.clone(),
4824                cx,
4825            );
4826        });
4827
4828        cx.run_until_parked();
4829
4830        editor.update(cx, |editor, cx| {
4831            editor.rhs_editor.update(cx, |rhs_editor, cx| {
4832                rhs_editor.fold_buffer(buffer1_id, cx);
4833            });
4834        });
4835
4836        cx.run_until_parked();
4837
4838        let rhs_buffer1_folded = editor.read_with(cx, |editor, cx| {
4839            editor.rhs_editor.read(cx).is_buffer_folded(buffer1_id, cx)
4840        });
4841        assert!(
4842            rhs_buffer1_folded,
4843            "buffer1 should be folded in rhs before split"
4844        );
4845
4846        editor.update_in(cx, |editor, window, cx| {
4847            editor.split(&Default::default(), window, cx);
4848        });
4849
4850        cx.run_until_parked();
4851
4852        let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
4853            (
4854                editor.rhs_editor.clone(),
4855                editor.lhs.as_ref().unwrap().editor.clone(),
4856            )
4857        });
4858
4859        let rhs_buffer1_folded =
4860            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
4861        assert!(
4862            rhs_buffer1_folded,
4863            "buffer1 should be folded in rhs after split"
4864        );
4865
4866        let base_buffer1_id = diff1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
4867        let lhs_buffer1_folded = lhs_editor.read_with(cx, |editor, cx| {
4868            editor.is_buffer_folded(base_buffer1_id, cx)
4869        });
4870        assert!(
4871            lhs_buffer1_folded,
4872            "buffer1 should be folded in lhs after split"
4873        );
4874
4875        assert_split_content(
4876            &editor,
4877            "
4878            § <no file>
4879            § -----
4880            § <no file>
4881            § -----
4882            ddd
4883            eee
4884            fff"
4885            .unindent(),
4886            "
4887            § <no file>
4888            § -----
4889            § <no file>
4890            § -----
4891            ddd
4892            eee
4893            fff"
4894            .unindent(),
4895            &mut cx,
4896        );
4897
4898        editor.update(cx, |editor, cx| {
4899            editor.rhs_editor.update(cx, |rhs_editor, cx| {
4900                rhs_editor.fold_buffer(buffer2_id, cx);
4901            });
4902        });
4903
4904        cx.run_until_parked();
4905
4906        let rhs_buffer2_folded =
4907            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer2_id, cx));
4908        assert!(rhs_buffer2_folded, "buffer2 should be folded in rhs");
4909
4910        let base_buffer2_id = diff2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
4911        let lhs_buffer2_folded = lhs_editor.read_with(cx, |editor, cx| {
4912            editor.is_buffer_folded(base_buffer2_id, cx)
4913        });
4914        assert!(lhs_buffer2_folded, "buffer2 should be folded in lhs");
4915
4916        let rhs_buffer1_still_folded =
4917            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
4918        assert!(
4919            rhs_buffer1_still_folded,
4920            "buffer1 should still be folded in rhs"
4921        );
4922
4923        let lhs_buffer1_still_folded = lhs_editor.read_with(cx, |editor, cx| {
4924            editor.is_buffer_folded(base_buffer1_id, cx)
4925        });
4926        assert!(
4927            lhs_buffer1_still_folded,
4928            "buffer1 should still be folded in lhs"
4929        );
4930
4931        assert_split_content(
4932            &editor,
4933            "
4934            § <no file>
4935            § -----
4936            § <no file>
4937            § -----"
4938                .unindent(),
4939            "
4940            § <no file>
4941            § -----
4942            § <no file>
4943            § -----"
4944                .unindent(),
4945            &mut cx,
4946        );
4947    }
4948
4949    #[gpui::test]
4950    async fn test_custom_block_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
4951        use rope::Point;
4952        use unindent::Unindent as _;
4953
4954        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::SideBySide).await;
4955
4956        let base_text = "
4957            ddd
4958            eee
4959        "
4960        .unindent();
4961        let current_text = "
4962            aaa
4963            bbb
4964            ccc
4965            ddd
4966            eee
4967        "
4968        .unindent();
4969
4970        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4971
4972        editor.update(cx, |editor, cx| {
4973            let path = PathKey::for_buffer(&buffer, cx);
4974            editor.set_excerpts_for_path(
4975                path,
4976                buffer.clone(),
4977                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4978                0,
4979                diff.clone(),
4980                cx,
4981            );
4982        });
4983
4984        cx.run_until_parked();
4985
4986        assert_split_content(
4987            &editor,
4988            "
4989            § <no file>
4990            § -----
4991            aaa
4992            bbb
4993            ccc
4994            ddd
4995            eee"
4996            .unindent(),
4997            "
4998            § <no file>
4999            § -----
5000            § spacer
5001            § spacer
5002            § spacer
5003            ddd
5004            eee"
5005            .unindent(),
5006            &mut cx,
5007        );
5008
5009        let block_ids = editor.update(cx, |splittable_editor, cx| {
5010            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5011                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5012                let anchor = snapshot.anchor_before(Point::new(2, 0));
5013                rhs_editor.insert_blocks(
5014                    [BlockProperties {
5015                        placement: BlockPlacement::Above(anchor),
5016                        height: Some(1),
5017                        style: BlockStyle::Fixed,
5018                        render: Arc::new(|_| div().into_any()),
5019                        priority: 0,
5020                    }],
5021                    None,
5022                    cx,
5023                )
5024            })
5025        });
5026
5027        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5028        let lhs_editor =
5029            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5030
5031        cx.update(|_, cx| {
5032            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5033                "custom block".to_string()
5034            });
5035        });
5036
5037        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5038            let display_map = lhs_editor.display_map.read(cx);
5039            let companion = display_map.companion().unwrap().read(cx);
5040            let mapping = companion
5041                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5042            *mapping.borrow().get(&block_ids[0]).unwrap()
5043        });
5044
5045        cx.update(|_, cx| {
5046            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5047                "custom block".to_string()
5048            });
5049        });
5050
5051        cx.run_until_parked();
5052
5053        assert_split_content(
5054            &editor,
5055            "
5056            § <no file>
5057            § -----
5058            aaa
5059            bbb
5060            § custom block
5061            ccc
5062            ddd
5063            eee"
5064            .unindent(),
5065            "
5066            § <no file>
5067            § -----
5068            § spacer
5069            § spacer
5070            § spacer
5071            § custom block
5072            ddd
5073            eee"
5074            .unindent(),
5075            &mut cx,
5076        );
5077
5078        editor.update(cx, |splittable_editor, cx| {
5079            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5080                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5081            });
5082        });
5083
5084        cx.run_until_parked();
5085
5086        assert_split_content(
5087            &editor,
5088            "
5089            § <no file>
5090            § -----
5091            aaa
5092            bbb
5093            ccc
5094            ddd
5095            eee"
5096            .unindent(),
5097            "
5098            § <no file>
5099            § -----
5100            § spacer
5101            § spacer
5102            § spacer
5103            ddd
5104            eee"
5105            .unindent(),
5106            &mut cx,
5107        );
5108    }
5109
5110    #[gpui::test]
5111    async fn test_custom_block_below_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5112        use rope::Point;
5113        use unindent::Unindent as _;
5114
5115        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::SideBySide).await;
5116
5117        let base_text = "
5118            ddd
5119            eee
5120        "
5121        .unindent();
5122        let current_text = "
5123            aaa
5124            bbb
5125            ccc
5126            ddd
5127            eee
5128        "
5129        .unindent();
5130
5131        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5132
5133        editor.update(cx, |editor, cx| {
5134            let path = PathKey::for_buffer(&buffer, cx);
5135            editor.set_excerpts_for_path(
5136                path,
5137                buffer.clone(),
5138                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5139                0,
5140                diff.clone(),
5141                cx,
5142            );
5143        });
5144
5145        cx.run_until_parked();
5146
5147        assert_split_content(
5148            &editor,
5149            "
5150            § <no file>
5151            § -----
5152            aaa
5153            bbb
5154            ccc
5155            ddd
5156            eee"
5157            .unindent(),
5158            "
5159            § <no file>
5160            § -----
5161            § spacer
5162            § spacer
5163            § spacer
5164            ddd
5165            eee"
5166            .unindent(),
5167            &mut cx,
5168        );
5169
5170        let block_ids = editor.update(cx, |splittable_editor, cx| {
5171            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5172                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5173                let anchor = snapshot.anchor_after(Point::new(1, 3));
5174                rhs_editor.insert_blocks(
5175                    [BlockProperties {
5176                        placement: BlockPlacement::Below(anchor),
5177                        height: Some(1),
5178                        style: BlockStyle::Fixed,
5179                        render: Arc::new(|_| div().into_any()),
5180                        priority: 0,
5181                    }],
5182                    None,
5183                    cx,
5184                )
5185            })
5186        });
5187
5188        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5189        let lhs_editor =
5190            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5191
5192        cx.update(|_, cx| {
5193            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5194                "custom block".to_string()
5195            });
5196        });
5197
5198        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5199            let display_map = lhs_editor.display_map.read(cx);
5200            let companion = display_map.companion().unwrap().read(cx);
5201            let mapping = companion
5202                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5203            *mapping.borrow().get(&block_ids[0]).unwrap()
5204        });
5205
5206        cx.update(|_, cx| {
5207            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5208                "custom block".to_string()
5209            });
5210        });
5211
5212        cx.run_until_parked();
5213
5214        assert_split_content(
5215            &editor,
5216            "
5217            § <no file>
5218            § -----
5219            aaa
5220            bbb
5221            § custom block
5222            ccc
5223            ddd
5224            eee"
5225            .unindent(),
5226            "
5227            § <no file>
5228            § -----
5229            § spacer
5230            § spacer
5231            § spacer
5232            § custom block
5233            ddd
5234            eee"
5235            .unindent(),
5236            &mut cx,
5237        );
5238
5239        editor.update(cx, |splittable_editor, cx| {
5240            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5241                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5242            });
5243        });
5244
5245        cx.run_until_parked();
5246
5247        assert_split_content(
5248            &editor,
5249            "
5250            § <no file>
5251            § -----
5252            aaa
5253            bbb
5254            ccc
5255            ddd
5256            eee"
5257            .unindent(),
5258            "
5259            § <no file>
5260            § -----
5261            § spacer
5262            § spacer
5263            § spacer
5264            ddd
5265            eee"
5266            .unindent(),
5267            &mut cx,
5268        );
5269    }
5270
5271    #[gpui::test]
5272    async fn test_custom_block_resize_syncs_balancing_block(cx: &mut gpui::TestAppContext) {
5273        use rope::Point;
5274        use unindent::Unindent as _;
5275
5276        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::SideBySide).await;
5277
5278        let base_text = "
5279            bbb
5280            ccc
5281        "
5282        .unindent();
5283        let current_text = "
5284            aaa
5285            bbb
5286            ccc
5287        "
5288        .unindent();
5289
5290        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5291
5292        editor.update(cx, |editor, cx| {
5293            let path = PathKey::for_buffer(&buffer, cx);
5294            editor.set_excerpts_for_path(
5295                path,
5296                buffer.clone(),
5297                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5298                0,
5299                diff.clone(),
5300                cx,
5301            );
5302        });
5303
5304        cx.run_until_parked();
5305
5306        let block_ids = editor.update(cx, |splittable_editor, cx| {
5307            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5308                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5309                let anchor = snapshot.anchor_before(Point::new(2, 0));
5310                rhs_editor.insert_blocks(
5311                    [BlockProperties {
5312                        placement: BlockPlacement::Above(anchor),
5313                        height: Some(1),
5314                        style: BlockStyle::Fixed,
5315                        render: Arc::new(|_| div().into_any()),
5316                        priority: 0,
5317                    }],
5318                    None,
5319                    cx,
5320                )
5321            })
5322        });
5323
5324        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5325        let lhs_editor =
5326            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5327
5328        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5329            let display_map = lhs_editor.display_map.read(cx);
5330            let companion = display_map.companion().unwrap().read(cx);
5331            let mapping = companion
5332                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5333            *mapping.borrow().get(&block_ids[0]).unwrap()
5334        });
5335
5336        cx.run_until_parked();
5337
5338        let get_block_height = |editor: &Entity<crate::Editor>,
5339                                block_id: crate::CustomBlockId,
5340                                cx: &mut VisualTestContext| {
5341            editor.update_in(cx, |editor, window, cx| {
5342                let snapshot = editor.snapshot(window, cx);
5343                snapshot
5344                    .block_for_id(crate::BlockId::Custom(block_id))
5345                    .map(|block| block.height())
5346            })
5347        };
5348
5349        assert_eq!(
5350            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5351            Some(1)
5352        );
5353        assert_eq!(
5354            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5355            Some(1)
5356        );
5357
5358        editor.update(cx, |splittable_editor, cx| {
5359            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5360                let mut heights = HashMap::default();
5361                heights.insert(block_ids[0], 3);
5362                rhs_editor.resize_blocks(heights, None, cx);
5363            });
5364        });
5365
5366        cx.run_until_parked();
5367
5368        assert_eq!(
5369            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5370            Some(3)
5371        );
5372        assert_eq!(
5373            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5374            Some(3)
5375        );
5376
5377        editor.update(cx, |splittable_editor, cx| {
5378            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5379                let mut heights = HashMap::default();
5380                heights.insert(block_ids[0], 5);
5381                rhs_editor.resize_blocks(heights, None, cx);
5382            });
5383        });
5384
5385        cx.run_until_parked();
5386
5387        assert_eq!(
5388            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5389            Some(5)
5390        );
5391        assert_eq!(
5392            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5393            Some(5)
5394        );
5395    }
5396}