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