split.rs

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