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