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