split.rs

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