split.rs

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