split.rs

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