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::MultiWorkspace;
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 (multi_workspace, cx) =
2109            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2110        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2111        let rhs_multibuffer = cx.new(|cx| {
2112            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2113            multibuffer.set_all_diff_hunks_expanded(cx);
2114            multibuffer
2115        });
2116        let editor = cx.new_window_entity(|window, cx| {
2117            let editor = SplittableEditor::new(
2118                style,
2119                rhs_multibuffer.clone(),
2120                project.clone(),
2121                workspace,
2122                window,
2123                cx,
2124            );
2125            editor.rhs_editor.update(cx, |editor, cx| {
2126                editor.set_soft_wrap_mode(soft_wrap, cx);
2127            });
2128            if let Some(lhs) = &editor.lhs {
2129                lhs.editor.update(cx, |editor, cx| {
2130                    editor.set_soft_wrap_mode(soft_wrap, cx);
2131                });
2132            }
2133            editor
2134        });
2135        (editor, cx)
2136    }
2137
2138    fn buffer_with_diff(
2139        base_text: &str,
2140        current_text: &str,
2141        cx: &mut VisualTestContext,
2142    ) -> (Entity<Buffer>, Entity<BufferDiff>) {
2143        let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2144        let diff = cx.new(|cx| {
2145            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
2146        });
2147        (buffer, diff)
2148    }
2149
2150    #[track_caller]
2151    fn assert_split_content(
2152        editor: &Entity<SplittableEditor>,
2153        expected_rhs: String,
2154        expected_lhs: String,
2155        cx: &mut VisualTestContext,
2156    ) {
2157        assert_split_content_with_widths(
2158            editor,
2159            px(3000.0),
2160            px(3000.0),
2161            expected_rhs,
2162            expected_lhs,
2163            cx,
2164        );
2165    }
2166
2167    #[track_caller]
2168    fn assert_split_content_with_widths(
2169        editor: &Entity<SplittableEditor>,
2170        rhs_width: Pixels,
2171        lhs_width: Pixels,
2172        expected_rhs: String,
2173        expected_lhs: String,
2174        cx: &mut VisualTestContext,
2175    ) {
2176        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2177            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2178            (editor.rhs_editor.clone(), lhs.editor.clone())
2179        });
2180
2181        // Make sure both sides learn if the other has soft-wrapped
2182        let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2183        cx.run_until_parked();
2184        let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2185        cx.run_until_parked();
2186
2187        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2188        let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2189
2190        if rhs_content != expected_rhs || lhs_content != expected_lhs {
2191            editor.update(cx, |editor, cx| editor.debug_print(cx));
2192        }
2193
2194        assert_eq!(rhs_content, expected_rhs, "rhs");
2195        assert_eq!(lhs_content, expected_lhs, "lhs");
2196    }
2197
2198    #[gpui::test(iterations = 100)]
2199    async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2200        use rand::prelude::*;
2201
2202        let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2203        let operations = std::env::var("OPERATIONS")
2204            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2205            .unwrap_or(10);
2206        let rng = &mut rng;
2207        for _ in 0..operations {
2208            let buffers = editor.update(cx, |editor, cx| {
2209                editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2210            });
2211
2212            if buffers.is_empty() {
2213                log::info!("adding excerpts to empty multibuffer");
2214                editor.update(cx, |editor, cx| {
2215                    editor.randomly_edit_excerpts(rng, 2, cx);
2216                    editor.check_invariants(true, cx);
2217                });
2218                continue;
2219            }
2220
2221            let mut quiesced = false;
2222
2223            match rng.random_range(0..100) {
2224                0..=44 => {
2225                    log::info!("randomly editing multibuffer");
2226                    editor.update(cx, |editor, cx| {
2227                        editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2228                            multibuffer.randomly_edit(rng, 5, cx);
2229                        })
2230                    })
2231                }
2232                45..=64 => {
2233                    log::info!("randomly undoing/redoing in single buffer");
2234                    let buffer = buffers.iter().choose(rng).unwrap();
2235                    buffer.update(cx, |buffer, cx| {
2236                        buffer.randomly_undo_redo(rng, cx);
2237                    });
2238                }
2239                65..=79 => {
2240                    log::info!("mutating excerpts");
2241                    editor.update(cx, |editor, cx| {
2242                        editor.randomly_edit_excerpts(rng, 2, cx);
2243                    });
2244                }
2245                _ => {
2246                    log::info!("quiescing");
2247                    for buffer in buffers {
2248                        let buffer_snapshot =
2249                            buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2250                        let diff = editor.update(cx, |editor, cx| {
2251                            editor
2252                                .rhs_multibuffer
2253                                .read(cx)
2254                                .diff_for(buffer.read(cx).remote_id())
2255                                .unwrap()
2256                        });
2257                        diff.update(cx, |diff, cx| {
2258                            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2259                        });
2260                        cx.run_until_parked();
2261                        let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2262                        let ranges = diff_snapshot
2263                            .hunks(&buffer_snapshot)
2264                            .map(|hunk| hunk.range)
2265                            .collect::<Vec<_>>();
2266                        editor.update(cx, |editor, cx| {
2267                            let path = PathKey::for_buffer(&buffer, cx);
2268                            editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2269                        });
2270                    }
2271                    quiesced = true;
2272                }
2273            }
2274
2275            editor.update(cx, |editor, cx| {
2276                editor.check_invariants(quiesced, cx);
2277            });
2278        }
2279    }
2280
2281    #[gpui::test]
2282    async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2283        use rope::Point;
2284        use unindent::Unindent as _;
2285
2286        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2287
2288        let base_text = "
2289            aaa
2290            bbb
2291            ccc
2292            ddd
2293            eee
2294            fff
2295        "
2296        .unindent();
2297        let current_text = "
2298            aaa
2299            ddd
2300            eee
2301            fff
2302        "
2303        .unindent();
2304
2305        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2306
2307        editor.update(cx, |editor, cx| {
2308            let path = PathKey::for_buffer(&buffer, cx);
2309            editor.set_excerpts_for_path(
2310                path,
2311                buffer.clone(),
2312                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2313                0,
2314                diff.clone(),
2315                cx,
2316            );
2317        });
2318
2319        cx.run_until_parked();
2320
2321        assert_split_content(
2322            &editor,
2323            "
2324            § <no file>
2325            § -----
2326            aaa
2327            § spacer
2328            § spacer
2329            ddd
2330            eee
2331            fff"
2332            .unindent(),
2333            "
2334            § <no file>
2335            § -----
2336            aaa
2337            bbb
2338            ccc
2339            ddd
2340            eee
2341            fff"
2342            .unindent(),
2343            &mut cx,
2344        );
2345
2346        buffer.update(cx, |buffer, cx| {
2347            buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2348        });
2349
2350        cx.run_until_parked();
2351
2352        assert_split_content(
2353            &editor,
2354            "
2355            § <no file>
2356            § -----
2357            aaa
2358            § spacer
2359            § spacer
2360            ddd
2361            eee
2362            FFF"
2363            .unindent(),
2364            "
2365            § <no file>
2366            § -----
2367            aaa
2368            bbb
2369            ccc
2370            ddd
2371            eee
2372            fff"
2373            .unindent(),
2374            &mut cx,
2375        );
2376
2377        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2378        diff.update(cx, |diff, cx| {
2379            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2380        });
2381
2382        cx.run_until_parked();
2383
2384        assert_split_content(
2385            &editor,
2386            "
2387            § <no file>
2388            § -----
2389            aaa
2390            § spacer
2391            § spacer
2392            ddd
2393            eee
2394            FFF"
2395            .unindent(),
2396            "
2397            § <no file>
2398            § -----
2399            aaa
2400            bbb
2401            ccc
2402            ddd
2403            eee
2404            fff"
2405            .unindent(),
2406            &mut cx,
2407        );
2408    }
2409
2410    #[gpui::test]
2411    async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2412        use rope::Point;
2413        use unindent::Unindent as _;
2414
2415        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2416
2417        let base_text1 = "
2418            aaa
2419            bbb
2420            ccc
2421            ddd
2422            eee"
2423        .unindent();
2424
2425        let base_text2 = "
2426            fff
2427            ggg
2428            hhh
2429            iii
2430            jjj"
2431        .unindent();
2432
2433        let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2434        let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2435
2436        editor.update(cx, |editor, cx| {
2437            let path1 = PathKey::for_buffer(&buffer1, cx);
2438            editor.set_excerpts_for_path(
2439                path1,
2440                buffer1.clone(),
2441                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2442                0,
2443                diff1.clone(),
2444                cx,
2445            );
2446            let path2 = PathKey::for_buffer(&buffer2, cx);
2447            editor.set_excerpts_for_path(
2448                path2,
2449                buffer2.clone(),
2450                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2451                1,
2452                diff2.clone(),
2453                cx,
2454            );
2455        });
2456
2457        cx.run_until_parked();
2458
2459        buffer1.update(cx, |buffer, cx| {
2460            buffer.edit(
2461                [
2462                    (Point::new(0, 0)..Point::new(1, 0), ""),
2463                    (Point::new(3, 0)..Point::new(4, 0), ""),
2464                ],
2465                None,
2466                cx,
2467            );
2468        });
2469        buffer2.update(cx, |buffer, cx| {
2470            buffer.edit(
2471                [
2472                    (Point::new(0, 0)..Point::new(1, 0), ""),
2473                    (Point::new(3, 0)..Point::new(4, 0), ""),
2474                ],
2475                None,
2476                cx,
2477            );
2478        });
2479
2480        cx.run_until_parked();
2481
2482        assert_split_content(
2483            &editor,
2484            "
2485            § <no file>
2486            § -----
2487            § spacer
2488            bbb
2489            ccc
2490            § spacer
2491            eee
2492            § <no file>
2493            § -----
2494            § spacer
2495            ggg
2496            hhh
2497            § spacer
2498            jjj"
2499            .unindent(),
2500            "
2501            § <no file>
2502            § -----
2503            aaa
2504            bbb
2505            ccc
2506            ddd
2507            eee
2508            § <no file>
2509            § -----
2510            fff
2511            ggg
2512            hhh
2513            iii
2514            jjj"
2515            .unindent(),
2516            &mut cx,
2517        );
2518
2519        let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2520        diff1.update(cx, |diff, cx| {
2521            diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2522        });
2523        let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2524        diff2.update(cx, |diff, cx| {
2525            diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2526        });
2527
2528        cx.run_until_parked();
2529
2530        assert_split_content(
2531            &editor,
2532            "
2533            § <no file>
2534            § -----
2535            § spacer
2536            bbb
2537            ccc
2538            § spacer
2539            eee
2540            § <no file>
2541            § -----
2542            § spacer
2543            ggg
2544            hhh
2545            § spacer
2546            jjj"
2547            .unindent(),
2548            "
2549            § <no file>
2550            § -----
2551            aaa
2552            bbb
2553            ccc
2554            ddd
2555            eee
2556            § <no file>
2557            § -----
2558            fff
2559            ggg
2560            hhh
2561            iii
2562            jjj"
2563            .unindent(),
2564            &mut cx,
2565        );
2566    }
2567
2568    #[gpui::test]
2569    async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2570        use rope::Point;
2571        use unindent::Unindent as _;
2572
2573        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2574
2575        let base_text = "
2576            aaa
2577            bbb
2578            ccc
2579            ddd
2580        "
2581        .unindent();
2582
2583        let current_text = "
2584            aaa
2585            NEW1
2586            NEW2
2587            ccc
2588            ddd
2589        "
2590        .unindent();
2591
2592        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2593
2594        editor.update(cx, |editor, cx| {
2595            let path = PathKey::for_buffer(&buffer, cx);
2596            editor.set_excerpts_for_path(
2597                path,
2598                buffer.clone(),
2599                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2600                0,
2601                diff.clone(),
2602                cx,
2603            );
2604        });
2605
2606        cx.run_until_parked();
2607
2608        assert_split_content(
2609            &editor,
2610            "
2611            § <no file>
2612            § -----
2613            aaa
2614            NEW1
2615            NEW2
2616            ccc
2617            ddd"
2618            .unindent(),
2619            "
2620            § <no file>
2621            § -----
2622            aaa
2623            bbb
2624            § spacer
2625            ccc
2626            ddd"
2627            .unindent(),
2628            &mut cx,
2629        );
2630
2631        buffer.update(cx, |buffer, cx| {
2632            buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2633        });
2634
2635        cx.run_until_parked();
2636
2637        assert_split_content(
2638            &editor,
2639            "
2640            § <no file>
2641            § -----
2642            aaa
2643            NEW1
2644            ccc
2645            ddd"
2646            .unindent(),
2647            "
2648            § <no file>
2649            § -----
2650            aaa
2651            bbb
2652            ccc
2653            ddd"
2654            .unindent(),
2655            &mut cx,
2656        );
2657
2658        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2659        diff.update(cx, |diff, cx| {
2660            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2661        });
2662
2663        cx.run_until_parked();
2664
2665        assert_split_content(
2666            &editor,
2667            "
2668            § <no file>
2669            § -----
2670            aaa
2671            NEW1
2672            ccc
2673            ddd"
2674            .unindent(),
2675            "
2676            § <no file>
2677            § -----
2678            aaa
2679            bbb
2680            ccc
2681            ddd"
2682            .unindent(),
2683            &mut cx,
2684        );
2685    }
2686
2687    #[gpui::test]
2688    async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2689        use rope::Point;
2690        use unindent::Unindent as _;
2691
2692        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2693
2694        let base_text = "
2695            aaa
2696            bbb
2697
2698
2699
2700
2701
2702            ccc
2703            ddd
2704        "
2705        .unindent();
2706        let current_text = "
2707            aaa
2708            bbb
2709
2710
2711
2712
2713
2714            CCC
2715            ddd
2716        "
2717        .unindent();
2718
2719        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2720
2721        editor.update(cx, |editor, cx| {
2722            let path = PathKey::for_buffer(&buffer, cx);
2723            editor.set_excerpts_for_path(
2724                path,
2725                buffer.clone(),
2726                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2727                0,
2728                diff.clone(),
2729                cx,
2730            );
2731        });
2732
2733        cx.run_until_parked();
2734
2735        buffer.update(cx, |buffer, cx| {
2736            buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2737        });
2738
2739        cx.run_until_parked();
2740
2741        assert_split_content(
2742            &editor,
2743            "
2744            § <no file>
2745            § -----
2746            aaa
2747            bbb
2748
2749
2750
2751
2752
2753
2754            CCC
2755            ddd"
2756            .unindent(),
2757            "
2758            § <no file>
2759            § -----
2760            aaa
2761            bbb
2762            § spacer
2763
2764
2765
2766
2767
2768            ccc
2769            ddd"
2770            .unindent(),
2771            &mut cx,
2772        );
2773
2774        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2775        diff.update(cx, |diff, cx| {
2776            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2777        });
2778
2779        cx.run_until_parked();
2780
2781        assert_split_content(
2782            &editor,
2783            "
2784            § <no file>
2785            § -----
2786            aaa
2787            bbb
2788
2789
2790
2791
2792
2793
2794            CCC
2795            ddd"
2796            .unindent(),
2797            "
2798            § <no file>
2799            § -----
2800            aaa
2801            bbb
2802
2803
2804
2805
2806
2807            ccc
2808            § spacer
2809            ddd"
2810            .unindent(),
2811            &mut cx,
2812        );
2813    }
2814
2815    #[gpui::test]
2816    async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2817        use git::Restore;
2818        use rope::Point;
2819        use unindent::Unindent as _;
2820
2821        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2822
2823        let base_text = "
2824            aaa
2825            bbb
2826            ccc
2827            ddd
2828            eee
2829        "
2830        .unindent();
2831        let current_text = "
2832            aaa
2833            ddd
2834            eee
2835        "
2836        .unindent();
2837
2838        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2839
2840        editor.update(cx, |editor, cx| {
2841            let path = PathKey::for_buffer(&buffer, cx);
2842            editor.set_excerpts_for_path(
2843                path,
2844                buffer.clone(),
2845                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2846                0,
2847                diff.clone(),
2848                cx,
2849            );
2850        });
2851
2852        cx.run_until_parked();
2853
2854        assert_split_content(
2855            &editor,
2856            "
2857            § <no file>
2858            § -----
2859            aaa
2860            § spacer
2861            § spacer
2862            ddd
2863            eee"
2864            .unindent(),
2865            "
2866            § <no file>
2867            § -----
2868            aaa
2869            bbb
2870            ccc
2871            ddd
2872            eee"
2873            .unindent(),
2874            &mut cx,
2875        );
2876
2877        let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
2878        cx.update_window_entity(&rhs_editor, |editor, window, cx| {
2879            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2880                s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2881            });
2882            editor.git_restore(&Restore, window, cx);
2883        });
2884
2885        cx.run_until_parked();
2886
2887        assert_split_content(
2888            &editor,
2889            "
2890            § <no file>
2891            § -----
2892            aaa
2893            bbb
2894            ccc
2895            ddd
2896            eee"
2897            .unindent(),
2898            "
2899            § <no file>
2900            § -----
2901            aaa
2902            bbb
2903            ccc
2904            ddd
2905            eee"
2906            .unindent(),
2907            &mut cx,
2908        );
2909
2910        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2911        diff.update(cx, |diff, cx| {
2912            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2913        });
2914
2915        cx.run_until_parked();
2916
2917        assert_split_content(
2918            &editor,
2919            "
2920            § <no file>
2921            § -----
2922            aaa
2923            bbb
2924            ccc
2925            ddd
2926            eee"
2927            .unindent(),
2928            "
2929            § <no file>
2930            § -----
2931            aaa
2932            bbb
2933            ccc
2934            ddd
2935            eee"
2936            .unindent(),
2937            &mut cx,
2938        );
2939    }
2940
2941    #[gpui::test]
2942    async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
2943        use rope::Point;
2944        use unindent::Unindent as _;
2945
2946        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2947
2948        let base_text = "
2949            aaa
2950            old1
2951            old2
2952            old3
2953            old4
2954            zzz
2955        "
2956        .unindent();
2957
2958        let current_text = "
2959            aaa
2960            new1
2961            new2
2962            new3
2963            new4
2964            zzz
2965        "
2966        .unindent();
2967
2968        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2969
2970        editor.update(cx, |editor, cx| {
2971            let path = PathKey::for_buffer(&buffer, cx);
2972            editor.set_excerpts_for_path(
2973                path,
2974                buffer.clone(),
2975                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2976                0,
2977                diff.clone(),
2978                cx,
2979            );
2980        });
2981
2982        cx.run_until_parked();
2983
2984        buffer.update(cx, |buffer, cx| {
2985            buffer.edit(
2986                [
2987                    (Point::new(2, 0)..Point::new(3, 0), ""),
2988                    (Point::new(4, 0)..Point::new(5, 0), ""),
2989                ],
2990                None,
2991                cx,
2992            );
2993        });
2994        cx.run_until_parked();
2995
2996        assert_split_content(
2997            &editor,
2998            "
2999            § <no file>
3000            § -----
3001            aaa
3002            new1
3003            new3
3004            § spacer
3005            § spacer
3006            zzz"
3007            .unindent(),
3008            "
3009            § <no file>
3010            § -----
3011            aaa
3012            old1
3013            old2
3014            old3
3015            old4
3016            zzz"
3017            .unindent(),
3018            &mut cx,
3019        );
3020
3021        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3022        diff.update(cx, |diff, cx| {
3023            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3024        });
3025
3026        cx.run_until_parked();
3027
3028        assert_split_content(
3029            &editor,
3030            "
3031            § <no file>
3032            § -----
3033            aaa
3034            new1
3035            new3
3036            § spacer
3037            § spacer
3038            zzz"
3039            .unindent(),
3040            "
3041            § <no file>
3042            § -----
3043            aaa
3044            old1
3045            old2
3046            old3
3047            old4
3048            zzz"
3049            .unindent(),
3050            &mut cx,
3051        );
3052    }
3053
3054    #[gpui::test]
3055    async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
3056        use rope::Point;
3057        use unindent::Unindent as _;
3058
3059        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3060
3061        let text = "aaaa bbbb cccc dddd eeee ffff";
3062
3063        let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
3064        let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
3065
3066        editor.update(cx, |editor, cx| {
3067            let end = Point::new(0, text.len() as u32);
3068            let path1 = PathKey::for_buffer(&buffer1, cx);
3069            editor.set_excerpts_for_path(
3070                path1,
3071                buffer1.clone(),
3072                vec![Point::new(0, 0)..end],
3073                0,
3074                diff1.clone(),
3075                cx,
3076            );
3077            let path2 = PathKey::for_buffer(&buffer2, cx);
3078            editor.set_excerpts_for_path(
3079                path2,
3080                buffer2.clone(),
3081                vec![Point::new(0, 0)..end],
3082                0,
3083                diff2.clone(),
3084                cx,
3085            );
3086        });
3087
3088        cx.run_until_parked();
3089
3090        assert_split_content_with_widths(
3091            &editor,
3092            px(200.0),
3093            px(400.0),
3094            "
3095            § <no file>
3096            § -----
3097            aaaa bbbb\x20
3098            cccc dddd\x20
3099            eeee ffff
3100            § <no file>
3101            § -----
3102            aaaa bbbb\x20
3103            cccc dddd\x20
3104            eeee ffff"
3105                .unindent(),
3106            "
3107            § <no file>
3108            § -----
3109            aaaa bbbb cccc dddd eeee ffff
3110            § spacer
3111            § spacer
3112            § <no file>
3113            § -----
3114            aaaa bbbb cccc dddd eeee ffff
3115            § spacer
3116            § spacer"
3117                .unindent(),
3118            &mut cx,
3119        );
3120    }
3121
3122    #[gpui::test]
3123    async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
3124        use rope::Point;
3125        use unindent::Unindent as _;
3126
3127        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3128
3129        let base_text = "
3130            aaaa bbbb cccc dddd eeee ffff
3131            old line one
3132            old line two
3133        "
3134        .unindent();
3135
3136        let current_text = "
3137            aaaa bbbb cccc dddd eeee ffff
3138            new line
3139        "
3140        .unindent();
3141
3142        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3143
3144        editor.update(cx, |editor, cx| {
3145            let path = PathKey::for_buffer(&buffer, cx);
3146            editor.set_excerpts_for_path(
3147                path,
3148                buffer.clone(),
3149                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3150                0,
3151                diff.clone(),
3152                cx,
3153            );
3154        });
3155
3156        cx.run_until_parked();
3157
3158        assert_split_content_with_widths(
3159            &editor,
3160            px(200.0),
3161            px(400.0),
3162            "
3163            § <no file>
3164            § -----
3165            aaaa bbbb\x20
3166            cccc dddd\x20
3167            eeee ffff
3168            new line
3169            § spacer"
3170                .unindent(),
3171            "
3172            § <no file>
3173            § -----
3174            aaaa bbbb cccc dddd eeee ffff
3175            § spacer
3176            § spacer
3177            old line one
3178            old line two"
3179                .unindent(),
3180            &mut cx,
3181        );
3182    }
3183
3184    #[gpui::test]
3185    async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3186        use rope::Point;
3187        use unindent::Unindent as _;
3188
3189        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3190
3191        let base_text = "
3192            aaaa bbbb cccc dddd eeee ffff
3193            deleted line one
3194            deleted line two
3195            after
3196        "
3197        .unindent();
3198
3199        let current_text = "
3200            aaaa bbbb cccc dddd eeee ffff
3201            after
3202        "
3203        .unindent();
3204
3205        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3206
3207        editor.update(cx, |editor, cx| {
3208            let path = PathKey::for_buffer(&buffer, cx);
3209            editor.set_excerpts_for_path(
3210                path,
3211                buffer.clone(),
3212                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3213                0,
3214                diff.clone(),
3215                cx,
3216            );
3217        });
3218
3219        cx.run_until_parked();
3220
3221        assert_split_content_with_widths(
3222            &editor,
3223            px(400.0),
3224            px(200.0),
3225            "
3226            § <no file>
3227            § -----
3228            aaaa bbbb cccc dddd eeee ffff
3229            § spacer
3230            § spacer
3231            § spacer
3232            § spacer
3233            § spacer
3234            § spacer
3235            after"
3236                .unindent(),
3237            "
3238            § <no file>
3239            § -----
3240            aaaa bbbb\x20
3241            cccc dddd\x20
3242            eeee ffff
3243            deleted line\x20
3244            one
3245            deleted line\x20
3246            two
3247            after"
3248                .unindent(),
3249            &mut cx,
3250        );
3251    }
3252
3253    #[gpui::test]
3254    async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3255        use rope::Point;
3256        use unindent::Unindent as _;
3257
3258        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3259
3260        let text = "
3261            aaaa bbbb cccc dddd eeee ffff
3262            short
3263        "
3264        .unindent();
3265
3266        let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3267
3268        editor.update(cx, |editor, cx| {
3269            let path = PathKey::for_buffer(&buffer, cx);
3270            editor.set_excerpts_for_path(
3271                path,
3272                buffer.clone(),
3273                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3274                0,
3275                diff.clone(),
3276                cx,
3277            );
3278        });
3279
3280        cx.run_until_parked();
3281
3282        assert_split_content_with_widths(
3283            &editor,
3284            px(400.0),
3285            px(200.0),
3286            "
3287            § <no file>
3288            § -----
3289            aaaa bbbb cccc dddd eeee ffff
3290            § spacer
3291            § spacer
3292            short"
3293                .unindent(),
3294            "
3295            § <no file>
3296            § -----
3297            aaaa bbbb\x20
3298            cccc dddd\x20
3299            eeee ffff
3300            short"
3301                .unindent(),
3302            &mut cx,
3303        );
3304
3305        buffer.update(cx, |buffer, cx| {
3306            buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3307        });
3308
3309        cx.run_until_parked();
3310
3311        assert_split_content_with_widths(
3312            &editor,
3313            px(400.0),
3314            px(200.0),
3315            "
3316            § <no file>
3317            § -----
3318            aaaa bbbb cccc dddd eeee ffff
3319            § spacer
3320            § spacer
3321            modified"
3322                .unindent(),
3323            "
3324            § <no file>
3325            § -----
3326            aaaa bbbb\x20
3327            cccc dddd\x20
3328            eeee ffff
3329            short"
3330                .unindent(),
3331            &mut cx,
3332        );
3333
3334        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3335        diff.update(cx, |diff, cx| {
3336            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3337        });
3338
3339        cx.run_until_parked();
3340
3341        assert_split_content_with_widths(
3342            &editor,
3343            px(400.0),
3344            px(200.0),
3345            "
3346            § <no file>
3347            § -----
3348            aaaa bbbb cccc dddd eeee ffff
3349            § spacer
3350            § spacer
3351            modified"
3352                .unindent(),
3353            "
3354            § <no file>
3355            § -----
3356            aaaa bbbb\x20
3357            cccc dddd\x20
3358            eeee ffff
3359            short"
3360                .unindent(),
3361            &mut cx,
3362        );
3363    }
3364
3365    #[gpui::test]
3366    async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3367        use rope::Point;
3368        use unindent::Unindent as _;
3369
3370        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3371
3372        let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3373
3374        let current_text = "
3375            aaa
3376            bbb
3377            ccc
3378        "
3379        .unindent();
3380
3381        let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3382        let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3383
3384        editor.update(cx, |editor, cx| {
3385            let path1 = PathKey::for_buffer(&buffer1, cx);
3386            editor.set_excerpts_for_path(
3387                path1,
3388                buffer1.clone(),
3389                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3390                0,
3391                diff1.clone(),
3392                cx,
3393            );
3394
3395            let path2 = PathKey::for_buffer(&buffer2, cx);
3396            editor.set_excerpts_for_path(
3397                path2,
3398                buffer2.clone(),
3399                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3400                1,
3401                diff2.clone(),
3402                cx,
3403            );
3404        });
3405
3406        cx.run_until_parked();
3407
3408        assert_split_content(
3409            &editor,
3410            "
3411            § <no file>
3412            § -----
3413            xxx
3414            yyy
3415            § <no file>
3416            § -----
3417            aaa
3418            bbb
3419            ccc"
3420            .unindent(),
3421            "
3422            § <no file>
3423            § -----
3424            xxx
3425            yyy
3426            § <no file>
3427            § -----
3428            § spacer
3429            § spacer
3430            § spacer"
3431                .unindent(),
3432            &mut cx,
3433        );
3434
3435        buffer1.update(cx, |buffer, cx| {
3436            buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3437        });
3438
3439        cx.run_until_parked();
3440
3441        assert_split_content(
3442            &editor,
3443            "
3444            § <no file>
3445            § -----
3446            xxxz
3447            yyy
3448            § <no file>
3449            § -----
3450            aaa
3451            bbb
3452            ccc"
3453            .unindent(),
3454            "
3455            § <no file>
3456            § -----
3457            xxx
3458            yyy
3459            § <no file>
3460            § -----
3461            § spacer
3462            § spacer
3463            § spacer"
3464                .unindent(),
3465            &mut cx,
3466        );
3467    }
3468
3469    #[gpui::test]
3470    async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3471        use rope::Point;
3472        use unindent::Unindent as _;
3473
3474        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3475
3476        let base_text = "
3477            aaa
3478            bbb
3479            ccc
3480        "
3481        .unindent();
3482
3483        let current_text = "
3484            NEW1
3485            NEW2
3486            ccc
3487        "
3488        .unindent();
3489
3490        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3491
3492        editor.update(cx, |editor, cx| {
3493            let path = PathKey::for_buffer(&buffer, cx);
3494            editor.set_excerpts_for_path(
3495                path,
3496                buffer.clone(),
3497                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3498                0,
3499                diff.clone(),
3500                cx,
3501            );
3502        });
3503
3504        cx.run_until_parked();
3505
3506        assert_split_content(
3507            &editor,
3508            "
3509            § <no file>
3510            § -----
3511            NEW1
3512            NEW2
3513            ccc"
3514            .unindent(),
3515            "
3516            § <no file>
3517            § -----
3518            aaa
3519            bbb
3520            ccc"
3521            .unindent(),
3522            &mut cx,
3523        );
3524
3525        buffer.update(cx, |buffer, cx| {
3526            buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3527        });
3528
3529        cx.run_until_parked();
3530
3531        assert_split_content(
3532            &editor,
3533            "
3534            § <no file>
3535            § -----
3536            NEW1
3537            NEW
3538            ccc"
3539            .unindent(),
3540            "
3541            § <no file>
3542            § -----
3543            aaa
3544            bbb
3545            ccc"
3546            .unindent(),
3547            &mut cx,
3548        );
3549    }
3550
3551    #[gpui::test]
3552    async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3553        use rope::Point;
3554        use unindent::Unindent as _;
3555
3556        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3557
3558        let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3559
3560        let current_text = "
3561            aaaa bbbb cccc dddd eeee ffff
3562            added line
3563        "
3564        .unindent();
3565
3566        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3567
3568        editor.update(cx, |editor, cx| {
3569            let path = PathKey::for_buffer(&buffer, cx);
3570            editor.set_excerpts_for_path(
3571                path,
3572                buffer.clone(),
3573                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3574                0,
3575                diff.clone(),
3576                cx,
3577            );
3578        });
3579
3580        cx.run_until_parked();
3581
3582        assert_split_content_with_widths(
3583            &editor,
3584            px(400.0),
3585            px(200.0),
3586            "
3587            § <no file>
3588            § -----
3589            aaaa bbbb cccc dddd eeee ffff
3590            § spacer
3591            § spacer
3592            added line"
3593                .unindent(),
3594            "
3595            § <no file>
3596            § -----
3597            aaaa bbbb\x20
3598            cccc dddd\x20
3599            eeee ffff
3600            § spacer"
3601                .unindent(),
3602            &mut cx,
3603        );
3604
3605        assert_split_content_with_widths(
3606            &editor,
3607            px(200.0),
3608            px(400.0),
3609            "
3610            § <no file>
3611            § -----
3612            aaaa bbbb\x20
3613            cccc dddd\x20
3614            eeee ffff
3615            added line"
3616                .unindent(),
3617            "
3618            § <no file>
3619            § -----
3620            aaaa bbbb cccc dddd eeee ffff
3621            § spacer
3622            § spacer
3623            § spacer"
3624                .unindent(),
3625            &mut cx,
3626        );
3627    }
3628
3629    #[gpui::test]
3630    #[ignore]
3631    async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3632        use rope::Point;
3633        use unindent::Unindent as _;
3634
3635        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3636
3637        let base_text = "
3638            aaa
3639            bbb
3640            ccc
3641            ddd
3642            eee
3643        "
3644        .unindent();
3645
3646        let current_text = "
3647            aaa
3648            NEW
3649            eee
3650        "
3651        .unindent();
3652
3653        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3654
3655        editor.update(cx, |editor, cx| {
3656            let path = PathKey::for_buffer(&buffer, cx);
3657            editor.set_excerpts_for_path(
3658                path,
3659                buffer.clone(),
3660                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3661                0,
3662                diff.clone(),
3663                cx,
3664            );
3665        });
3666
3667        cx.run_until_parked();
3668
3669        assert_split_content(
3670            &editor,
3671            "
3672            § <no file>
3673            § -----
3674            aaa
3675            NEW
3676            § spacer
3677            § spacer
3678            eee"
3679            .unindent(),
3680            "
3681            § <no file>
3682            § -----
3683            aaa
3684            bbb
3685            ccc
3686            ddd
3687            eee"
3688            .unindent(),
3689            &mut cx,
3690        );
3691
3692        buffer.update(cx, |buffer, cx| {
3693            buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3694        });
3695
3696        cx.run_until_parked();
3697
3698        assert_split_content(
3699            &editor,
3700            "
3701            § <no file>
3702            § -----
3703            aaa
3704            § spacer
3705            § spacer
3706            § spacer
3707            NEWeee"
3708                .unindent(),
3709            "
3710            § <no file>
3711            § -----
3712            aaa
3713            bbb
3714            ccc
3715            ddd
3716            eee"
3717            .unindent(),
3718            &mut cx,
3719        );
3720
3721        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3722        diff.update(cx, |diff, cx| {
3723            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3724        });
3725
3726        cx.run_until_parked();
3727
3728        assert_split_content(
3729            &editor,
3730            "
3731            § <no file>
3732            § -----
3733            aaa
3734            NEWeee
3735            § spacer
3736            § spacer
3737            § spacer"
3738                .unindent(),
3739            "
3740            § <no file>
3741            § -----
3742            aaa
3743            bbb
3744            ccc
3745            ddd
3746            eee"
3747            .unindent(),
3748            &mut cx,
3749        );
3750    }
3751
3752    #[gpui::test]
3753    async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3754        use rope::Point;
3755        use unindent::Unindent as _;
3756
3757        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3758
3759        let base_text = "";
3760        let current_text = "
3761            aaaa bbbb cccc dddd eeee ffff
3762            bbb
3763            ccc
3764        "
3765        .unindent();
3766
3767        let (buffer, diff) = buffer_with_diff(base_text, &current_text, &mut cx);
3768
3769        editor.update(cx, |editor, cx| {
3770            let path = PathKey::for_buffer(&buffer, cx);
3771            editor.set_excerpts_for_path(
3772                path,
3773                buffer.clone(),
3774                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3775                0,
3776                diff.clone(),
3777                cx,
3778            );
3779        });
3780
3781        cx.run_until_parked();
3782
3783        assert_split_content(
3784            &editor,
3785            "
3786            § <no file>
3787            § -----
3788            aaaa bbbb cccc dddd eeee ffff
3789            bbb
3790            ccc"
3791            .unindent(),
3792            "
3793            § <no file>
3794            § -----
3795            § spacer
3796            § spacer
3797            § spacer"
3798                .unindent(),
3799            &mut cx,
3800        );
3801
3802        assert_split_content_with_widths(
3803            &editor,
3804            px(200.0),
3805            px(200.0),
3806            "
3807            § <no file>
3808            § -----
3809            aaaa bbbb\x20
3810            cccc dddd\x20
3811            eeee ffff
3812            bbb
3813            ccc"
3814            .unindent(),
3815            "
3816            § <no file>
3817            § -----
3818            § spacer
3819            § spacer
3820            § spacer
3821            § spacer
3822            § spacer"
3823                .unindent(),
3824            &mut cx,
3825        );
3826    }
3827
3828    #[gpui::test]
3829    async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
3830        use rope::Point;
3831        use unindent::Unindent as _;
3832
3833        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3834
3835        let base_text = "
3836            aaa
3837            bbb
3838            ccc
3839        "
3840        .unindent();
3841
3842        let current_text = "
3843            aaa
3844            bbb
3845            xxx
3846            yyy
3847            ccc
3848        "
3849        .unindent();
3850
3851        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3852
3853        editor.update(cx, |editor, cx| {
3854            let path = PathKey::for_buffer(&buffer, cx);
3855            editor.set_excerpts_for_path(
3856                path,
3857                buffer.clone(),
3858                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3859                0,
3860                diff.clone(),
3861                cx,
3862            );
3863        });
3864
3865        cx.run_until_parked();
3866
3867        assert_split_content(
3868            &editor,
3869            "
3870            § <no file>
3871            § -----
3872            aaa
3873            bbb
3874            xxx
3875            yyy
3876            ccc"
3877            .unindent(),
3878            "
3879            § <no file>
3880            § -----
3881            aaa
3882            bbb
3883            § spacer
3884            § spacer
3885            ccc"
3886            .unindent(),
3887            &mut cx,
3888        );
3889
3890        buffer.update(cx, |buffer, cx| {
3891            buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
3892        });
3893
3894        cx.run_until_parked();
3895
3896        assert_split_content(
3897            &editor,
3898            "
3899            § <no file>
3900            § -----
3901            aaa
3902            bbb
3903            xxx
3904            yyy
3905            zzz
3906            ccc"
3907            .unindent(),
3908            "
3909            § <no file>
3910            § -----
3911            aaa
3912            bbb
3913            § spacer
3914            § spacer
3915            § spacer
3916            ccc"
3917            .unindent(),
3918            &mut cx,
3919        );
3920    }
3921
3922    #[gpui::test]
3923    async fn test_scrolling(cx: &mut gpui::TestAppContext) {
3924        use crate::test::editor_content_with_blocks_and_size;
3925        use gpui::size;
3926        use rope::Point;
3927
3928        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
3929
3930        let long_line = "x".repeat(200);
3931        let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
3932        lines[25] = long_line;
3933        let content = lines.join("\n");
3934
3935        let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
3936
3937        editor.update(cx, |editor, cx| {
3938            let path = PathKey::for_buffer(&buffer, cx);
3939            editor.set_excerpts_for_path(
3940                path,
3941                buffer.clone(),
3942                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3943                0,
3944                diff.clone(),
3945                cx,
3946            );
3947        });
3948
3949        cx.run_until_parked();
3950
3951        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
3952            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
3953            (editor.rhs_editor.clone(), lhs.editor.clone())
3954        });
3955
3956        rhs_editor.update_in(cx, |e, window, cx| {
3957            e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
3958        });
3959
3960        let rhs_pos =
3961            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3962        let lhs_pos =
3963            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3964        assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
3965        assert_eq!(
3966            lhs_pos.y, rhs_pos.y,
3967            "LHS should have same scroll position as RHS after set_scroll_position"
3968        );
3969
3970        let draw_size = size(px(300.), px(300.));
3971
3972        rhs_editor.update_in(cx, |e, window, cx| {
3973            e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
3974                s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
3975            });
3976        });
3977
3978        let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
3979        cx.run_until_parked();
3980        let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
3981        cx.run_until_parked();
3982
3983        let rhs_pos =
3984            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3985        let lhs_pos =
3986            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3987
3988        assert!(
3989            rhs_pos.y > 0.,
3990            "RHS should have scrolled vertically to show cursor at row 25"
3991        );
3992        assert!(
3993            rhs_pos.x > 0.,
3994            "RHS should have scrolled horizontally to show cursor at column 150"
3995        );
3996        assert_eq!(
3997            lhs_pos.y, rhs_pos.y,
3998            "LHS should have same vertical scroll position as RHS after autoscroll"
3999        );
4000        assert_eq!(
4001            lhs_pos.x, rhs_pos.x,
4002            "LHS should have same horizontal scroll position as RHS after autoscroll"
4003        )
4004    }
4005
4006    #[gpui::test]
4007    async fn test_edit_line_before_soft_wrapped_line_preceding_hunk(cx: &mut gpui::TestAppContext) {
4008        use rope::Point;
4009        use unindent::Unindent as _;
4010
4011        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
4012
4013        let base_text = "
4014            first line
4015            aaaa bbbb cccc dddd eeee ffff
4016            original
4017        "
4018        .unindent();
4019
4020        let current_text = "
4021            first line
4022            aaaa bbbb cccc dddd eeee ffff
4023            modified
4024        "
4025        .unindent();
4026
4027        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4028
4029        editor.update(cx, |editor, cx| {
4030            let path = PathKey::for_buffer(&buffer, cx);
4031            editor.set_excerpts_for_path(
4032                path,
4033                buffer.clone(),
4034                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4035                0,
4036                diff.clone(),
4037                cx,
4038            );
4039        });
4040
4041        cx.run_until_parked();
4042
4043        assert_split_content_with_widths(
4044            &editor,
4045            px(400.0),
4046            px(200.0),
4047            "
4048                    § <no file>
4049                    § -----
4050                    first line
4051                    aaaa bbbb cccc dddd eeee ffff
4052                    § spacer
4053                    § spacer
4054                    modified"
4055                .unindent(),
4056            "
4057                    § <no file>
4058                    § -----
4059                    first line
4060                    aaaa bbbb\x20
4061                    cccc dddd\x20
4062                    eeee ffff
4063                    original"
4064                .unindent(),
4065            &mut cx,
4066        );
4067
4068        buffer.update(cx, |buffer, cx| {
4069            buffer.edit(
4070                [(Point::new(0, 0)..Point::new(0, 10), "edited first")],
4071                None,
4072                cx,
4073            );
4074        });
4075
4076        cx.run_until_parked();
4077
4078        assert_split_content_with_widths(
4079            &editor,
4080            px(400.0),
4081            px(200.0),
4082            "
4083                    § <no file>
4084                    § -----
4085                    edited first
4086                    aaaa bbbb cccc dddd eeee ffff
4087                    § spacer
4088                    § spacer
4089                    modified"
4090                .unindent(),
4091            "
4092                    § <no file>
4093                    § -----
4094                    first line
4095                    aaaa bbbb\x20
4096                    cccc dddd\x20
4097                    eeee ffff
4098                    original"
4099                .unindent(),
4100            &mut cx,
4101        );
4102
4103        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4104        diff.update(cx, |diff, cx| {
4105            diff.recalculate_diff_sync(&buffer_snapshot, cx);
4106        });
4107
4108        cx.run_until_parked();
4109
4110        assert_split_content_with_widths(
4111            &editor,
4112            px(400.0),
4113            px(200.0),
4114            "
4115                    § <no file>
4116                    § -----
4117                    edited first
4118                    aaaa bbbb cccc dddd eeee ffff
4119                    § spacer
4120                    § spacer
4121                    modified"
4122                .unindent(),
4123            "
4124                    § <no file>
4125                    § -----
4126                    first line
4127                    aaaa bbbb\x20
4128                    cccc dddd\x20
4129                    eeee ffff
4130                    original"
4131                .unindent(),
4132            &mut cx,
4133        );
4134    }
4135
4136    #[gpui::test]
4137    async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
4138        use rope::Point;
4139        use unindent::Unindent as _;
4140
4141        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4142
4143        let base_text = "
4144            bbb
4145            ccc
4146        "
4147        .unindent();
4148        let current_text = "
4149            aaa
4150            bbb
4151            ccc
4152        "
4153        .unindent();
4154
4155        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4156
4157        editor.update(cx, |editor, cx| {
4158            let path = PathKey::for_buffer(&buffer, cx);
4159            editor.set_excerpts_for_path(
4160                path,
4161                buffer.clone(),
4162                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4163                0,
4164                diff.clone(),
4165                cx,
4166            );
4167        });
4168
4169        cx.run_until_parked();
4170
4171        assert_split_content(
4172            &editor,
4173            "
4174            § <no file>
4175            § -----
4176            aaa
4177            bbb
4178            ccc"
4179            .unindent(),
4180            "
4181            § <no file>
4182            § -----
4183            § spacer
4184            bbb
4185            ccc"
4186            .unindent(),
4187            &mut cx,
4188        );
4189
4190        let block_ids = editor.update(cx, |splittable_editor, cx| {
4191            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4192                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4193                let anchor = snapshot.anchor_before(Point::new(2, 0));
4194                rhs_editor.insert_blocks(
4195                    [BlockProperties {
4196                        placement: BlockPlacement::Above(anchor),
4197                        height: Some(1),
4198                        style: BlockStyle::Fixed,
4199                        render: Arc::new(|_| div().into_any()),
4200                        priority: 0,
4201                    }],
4202                    None,
4203                    cx,
4204                )
4205            })
4206        });
4207
4208        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4209        let lhs_editor =
4210            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4211
4212        cx.update(|_, cx| {
4213            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4214                "custom block".to_string()
4215            });
4216        });
4217
4218        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
4219            let display_map = lhs_editor.display_map.read(cx);
4220            let companion = display_map.companion().unwrap().read(cx);
4221            let mapping = companion
4222                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4223            *mapping.borrow().get(&block_ids[0]).unwrap()
4224        });
4225
4226        cx.update(|_, cx| {
4227            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
4228                "custom block".to_string()
4229            });
4230        });
4231
4232        cx.run_until_parked();
4233
4234        assert_split_content(
4235            &editor,
4236            "
4237            § <no file>
4238            § -----
4239            aaa
4240            bbb
4241            § custom block
4242            ccc"
4243            .unindent(),
4244            "
4245            § <no file>
4246            § -----
4247            § spacer
4248            bbb
4249            § custom block
4250            ccc"
4251            .unindent(),
4252            &mut cx,
4253        );
4254
4255        editor.update(cx, |splittable_editor, cx| {
4256            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4257                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
4258            });
4259        });
4260
4261        cx.run_until_parked();
4262
4263        assert_split_content(
4264            &editor,
4265            "
4266            § <no file>
4267            § -----
4268            aaa
4269            bbb
4270            ccc"
4271            .unindent(),
4272            "
4273            § <no file>
4274            § -----
4275            § spacer
4276            bbb
4277            ccc"
4278            .unindent(),
4279            &mut cx,
4280        );
4281    }
4282
4283    #[gpui::test]
4284    async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
4285        use rope::Point;
4286        use unindent::Unindent as _;
4287
4288        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4289
4290        let base_text = "
4291            bbb
4292            ccc
4293        "
4294        .unindent();
4295        let current_text = "
4296            aaa
4297            bbb
4298            ccc
4299        "
4300        .unindent();
4301
4302        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4303
4304        editor.update(cx, |editor, cx| {
4305            let path = PathKey::for_buffer(&buffer, cx);
4306            editor.set_excerpts_for_path(
4307                path,
4308                buffer.clone(),
4309                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4310                0,
4311                diff.clone(),
4312                cx,
4313            );
4314        });
4315
4316        cx.run_until_parked();
4317
4318        assert_split_content(
4319            &editor,
4320            "
4321            § <no file>
4322            § -----
4323            aaa
4324            bbb
4325            ccc"
4326            .unindent(),
4327            "
4328            § <no file>
4329            § -----
4330            § spacer
4331            bbb
4332            ccc"
4333            .unindent(),
4334            &mut cx,
4335        );
4336
4337        let block_ids = editor.update(cx, |splittable_editor, cx| {
4338            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4339                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4340                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4341                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4342                rhs_editor.insert_blocks(
4343                    [
4344                        BlockProperties {
4345                            placement: BlockPlacement::Above(anchor1),
4346                            height: Some(1),
4347                            style: BlockStyle::Fixed,
4348                            render: Arc::new(|_| div().into_any()),
4349                            priority: 0,
4350                        },
4351                        BlockProperties {
4352                            placement: BlockPlacement::Above(anchor2),
4353                            height: Some(1),
4354                            style: BlockStyle::Fixed,
4355                            render: Arc::new(|_| div().into_any()),
4356                            priority: 0,
4357                        },
4358                    ],
4359                    None,
4360                    cx,
4361                )
4362            })
4363        });
4364
4365        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4366        let lhs_editor =
4367            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4368
4369        cx.update(|_, cx| {
4370            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4371                "custom block 1".to_string()
4372            });
4373            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4374                "custom block 2".to_string()
4375            });
4376        });
4377
4378        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4379            let display_map = lhs_editor.display_map.read(cx);
4380            let companion = display_map.companion().unwrap().read(cx);
4381            let mapping = companion
4382                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4383            (
4384                *mapping.borrow().get(&block_ids[0]).unwrap(),
4385                *mapping.borrow().get(&block_ids[1]).unwrap(),
4386            )
4387        });
4388
4389        cx.update(|_, cx| {
4390            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4391                "custom block 1".to_string()
4392            });
4393            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4394                "custom block 2".to_string()
4395            });
4396        });
4397
4398        cx.run_until_parked();
4399
4400        assert_split_content(
4401            &editor,
4402            "
4403            § <no file>
4404            § -----
4405            aaa
4406            bbb
4407            § custom block 1
4408            ccc
4409            § custom block 2"
4410                .unindent(),
4411            "
4412            § <no file>
4413            § -----
4414            § spacer
4415            bbb
4416            § custom block 1
4417            ccc
4418            § custom block 2"
4419                .unindent(),
4420            &mut cx,
4421        );
4422
4423        editor.update(cx, |splittable_editor, cx| {
4424            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4425                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4426            });
4427        });
4428
4429        cx.run_until_parked();
4430
4431        assert_split_content(
4432            &editor,
4433            "
4434            § <no file>
4435            § -----
4436            aaa
4437            bbb
4438            ccc
4439            § custom block 2"
4440                .unindent(),
4441            "
4442            § <no file>
4443            § -----
4444            § spacer
4445            bbb
4446            ccc
4447            § custom block 2"
4448                .unindent(),
4449            &mut cx,
4450        );
4451
4452        editor.update_in(cx, |splittable_editor, window, cx| {
4453            splittable_editor.unsplit(window, cx);
4454        });
4455
4456        cx.run_until_parked();
4457
4458        editor.update_in(cx, |splittable_editor, window, cx| {
4459            splittable_editor.split(window, cx);
4460        });
4461
4462        cx.run_until_parked();
4463
4464        let lhs_editor =
4465            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4466
4467        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4468            let display_map = lhs_editor.display_map.read(cx);
4469            let companion = display_map.companion().unwrap().read(cx);
4470            let mapping = companion
4471                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4472            *mapping.borrow().get(&block_ids[1]).unwrap()
4473        });
4474
4475        cx.update(|_, cx| {
4476            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4477                "custom block 2".to_string()
4478            });
4479        });
4480
4481        cx.run_until_parked();
4482
4483        assert_split_content(
4484            &editor,
4485            "
4486            § <no file>
4487            § -----
4488            aaa
4489            bbb
4490            ccc
4491            § custom block 2"
4492                .unindent(),
4493            "
4494            § <no file>
4495            § -----
4496            § spacer
4497            bbb
4498            ccc
4499            § custom block 2"
4500                .unindent(),
4501            &mut cx,
4502        );
4503    }
4504
4505    #[gpui::test]
4506    async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
4507        use rope::Point;
4508        use unindent::Unindent as _;
4509
4510        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4511
4512        let base_text = "
4513            bbb
4514            ccc
4515        "
4516        .unindent();
4517        let current_text = "
4518            aaa
4519            bbb
4520            ccc
4521        "
4522        .unindent();
4523
4524        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4525
4526        editor.update(cx, |editor, cx| {
4527            let path = PathKey::for_buffer(&buffer, cx);
4528            editor.set_excerpts_for_path(
4529                path,
4530                buffer.clone(),
4531                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4532                0,
4533                diff.clone(),
4534                cx,
4535            );
4536        });
4537
4538        cx.run_until_parked();
4539
4540        editor.update_in(cx, |splittable_editor, window, cx| {
4541            splittable_editor.unsplit(window, cx);
4542        });
4543
4544        cx.run_until_parked();
4545
4546        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4547
4548        let block_ids = editor.update(cx, |splittable_editor, cx| {
4549            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4550                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4551                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4552                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4553                rhs_editor.insert_blocks(
4554                    [
4555                        BlockProperties {
4556                            placement: BlockPlacement::Above(anchor1),
4557                            height: Some(1),
4558                            style: BlockStyle::Fixed,
4559                            render: Arc::new(|_| div().into_any()),
4560                            priority: 0,
4561                        },
4562                        BlockProperties {
4563                            placement: BlockPlacement::Above(anchor2),
4564                            height: Some(1),
4565                            style: BlockStyle::Fixed,
4566                            render: Arc::new(|_| div().into_any()),
4567                            priority: 0,
4568                        },
4569                    ],
4570                    None,
4571                    cx,
4572                )
4573            })
4574        });
4575
4576        cx.update(|_, cx| {
4577            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4578                "custom block 1".to_string()
4579            });
4580            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4581                "custom block 2".to_string()
4582            });
4583        });
4584
4585        cx.run_until_parked();
4586
4587        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
4588        assert_eq!(
4589            rhs_content,
4590            "
4591            § <no file>
4592            § -----
4593            aaa
4594            bbb
4595            § custom block 1
4596            ccc
4597            § custom block 2"
4598                .unindent(),
4599            "rhs content before split"
4600        );
4601
4602        editor.update_in(cx, |splittable_editor, window, cx| {
4603            splittable_editor.split(window, cx);
4604        });
4605
4606        cx.run_until_parked();
4607
4608        let lhs_editor =
4609            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4610
4611        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4612            let display_map = lhs_editor.display_map.read(cx);
4613            let companion = display_map.companion().unwrap().read(cx);
4614            let mapping = companion
4615                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4616            (
4617                *mapping.borrow().get(&block_ids[0]).unwrap(),
4618                *mapping.borrow().get(&block_ids[1]).unwrap(),
4619            )
4620        });
4621
4622        cx.update(|_, cx| {
4623            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4624                "custom block 1".to_string()
4625            });
4626            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4627                "custom block 2".to_string()
4628            });
4629        });
4630
4631        cx.run_until_parked();
4632
4633        assert_split_content(
4634            &editor,
4635            "
4636            § <no file>
4637            § -----
4638            aaa
4639            bbb
4640            § custom block 1
4641            ccc
4642            § custom block 2"
4643                .unindent(),
4644            "
4645            § <no file>
4646            § -----
4647            § spacer
4648            bbb
4649            § custom block 1
4650            ccc
4651            § custom block 2"
4652                .unindent(),
4653            &mut cx,
4654        );
4655
4656        editor.update(cx, |splittable_editor, cx| {
4657            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4658                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4659            });
4660        });
4661
4662        cx.run_until_parked();
4663
4664        assert_split_content(
4665            &editor,
4666            "
4667            § <no file>
4668            § -----
4669            aaa
4670            bbb
4671            ccc
4672            § custom block 2"
4673                .unindent(),
4674            "
4675            § <no file>
4676            § -----
4677            § spacer
4678            bbb
4679            ccc
4680            § custom block 2"
4681                .unindent(),
4682            &mut cx,
4683        );
4684
4685        editor.update_in(cx, |splittable_editor, window, cx| {
4686            splittable_editor.unsplit(window, cx);
4687        });
4688
4689        cx.run_until_parked();
4690
4691        editor.update_in(cx, |splittable_editor, window, cx| {
4692            splittable_editor.split(window, cx);
4693        });
4694
4695        cx.run_until_parked();
4696
4697        let lhs_editor =
4698            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4699
4700        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4701            let display_map = lhs_editor.display_map.read(cx);
4702            let companion = display_map.companion().unwrap().read(cx);
4703            let mapping = companion
4704                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4705            *mapping.borrow().get(&block_ids[1]).unwrap()
4706        });
4707
4708        cx.update(|_, cx| {
4709            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4710                "custom block 2".to_string()
4711            });
4712        });
4713
4714        cx.run_until_parked();
4715
4716        assert_split_content(
4717            &editor,
4718            "
4719            § <no file>
4720            § -----
4721            aaa
4722            bbb
4723            ccc
4724            § custom block 2"
4725                .unindent(),
4726            "
4727            § <no file>
4728            § -----
4729            § spacer
4730            bbb
4731            ccc
4732            § custom block 2"
4733                .unindent(),
4734            &mut cx,
4735        );
4736
4737        let new_block_ids = editor.update(cx, |splittable_editor, cx| {
4738            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4739                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4740                let anchor = snapshot.anchor_before(Point::new(2, 0));
4741                rhs_editor.insert_blocks(
4742                    [BlockProperties {
4743                        placement: BlockPlacement::Above(anchor),
4744                        height: Some(1),
4745                        style: BlockStyle::Fixed,
4746                        render: Arc::new(|_| div().into_any()),
4747                        priority: 0,
4748                    }],
4749                    None,
4750                    cx,
4751                )
4752            })
4753        });
4754
4755        cx.update(|_, cx| {
4756            set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
4757                "custom block 3".to_string()
4758            });
4759        });
4760
4761        let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4762            let display_map = lhs_editor.display_map.read(cx);
4763            let companion = display_map.companion().unwrap().read(cx);
4764            let mapping = companion
4765                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4766            *mapping.borrow().get(&new_block_ids[0]).unwrap()
4767        });
4768
4769        cx.update(|_, cx| {
4770            set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
4771                "custom block 3".to_string()
4772            });
4773        });
4774
4775        cx.run_until_parked();
4776
4777        assert_split_content(
4778            &editor,
4779            "
4780            § <no file>
4781            § -----
4782            aaa
4783            bbb
4784            § custom block 3
4785            ccc
4786            § custom block 2"
4787                .unindent(),
4788            "
4789            § <no file>
4790            § -----
4791            § spacer
4792            bbb
4793            § custom block 3
4794            ccc
4795            § custom block 2"
4796                .unindent(),
4797            &mut cx,
4798        );
4799
4800        editor.update(cx, |splittable_editor, cx| {
4801            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4802                rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
4803            });
4804        });
4805
4806        cx.run_until_parked();
4807
4808        assert_split_content(
4809            &editor,
4810            "
4811            § <no file>
4812            § -----
4813            aaa
4814            bbb
4815            ccc
4816            § custom block 2"
4817                .unindent(),
4818            "
4819            § <no file>
4820            § -----
4821            § spacer
4822            bbb
4823            ccc
4824            § custom block 2"
4825                .unindent(),
4826            &mut cx,
4827        );
4828    }
4829
4830    #[gpui::test]
4831    async fn test_buffer_folding_sync(cx: &mut gpui::TestAppContext) {
4832        use rope::Point;
4833        use unindent::Unindent as _;
4834
4835        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
4836
4837        let base_text1 = "
4838            aaa
4839            bbb
4840            ccc"
4841        .unindent();
4842        let current_text1 = "
4843            aaa
4844            bbb
4845            ccc"
4846        .unindent();
4847
4848        let base_text2 = "
4849            ddd
4850            eee
4851            fff"
4852        .unindent();
4853        let current_text2 = "
4854            ddd
4855            eee
4856            fff"
4857        .unindent();
4858
4859        let (buffer1, diff1) = buffer_with_diff(&base_text1, &current_text1, &mut cx);
4860        let (buffer2, diff2) = buffer_with_diff(&base_text2, &current_text2, &mut cx);
4861
4862        let buffer1_id = buffer1.read_with(cx, |buffer, _| buffer.remote_id());
4863        let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
4864
4865        editor.update(cx, |editor, cx| {
4866            let path1 = PathKey::for_buffer(&buffer1, cx);
4867            editor.set_excerpts_for_path(
4868                path1,
4869                buffer1.clone(),
4870                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
4871                0,
4872                diff1.clone(),
4873                cx,
4874            );
4875            let path2 = PathKey::for_buffer(&buffer2, cx);
4876            editor.set_excerpts_for_path(
4877                path2,
4878                buffer2.clone(),
4879                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
4880                1,
4881                diff2.clone(),
4882                cx,
4883            );
4884        });
4885
4886        cx.run_until_parked();
4887
4888        editor.update(cx, |editor, cx| {
4889            editor.rhs_editor.update(cx, |rhs_editor, cx| {
4890                rhs_editor.fold_buffer(buffer1_id, cx);
4891            });
4892        });
4893
4894        cx.run_until_parked();
4895
4896        let rhs_buffer1_folded = editor.read_with(cx, |editor, cx| {
4897            editor.rhs_editor.read(cx).is_buffer_folded(buffer1_id, cx)
4898        });
4899        assert!(
4900            rhs_buffer1_folded,
4901            "buffer1 should be folded in rhs before split"
4902        );
4903
4904        editor.update_in(cx, |editor, window, cx| {
4905            editor.split(window, cx);
4906        });
4907
4908        cx.run_until_parked();
4909
4910        let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
4911            (
4912                editor.rhs_editor.clone(),
4913                editor.lhs.as_ref().unwrap().editor.clone(),
4914            )
4915        });
4916
4917        let rhs_buffer1_folded =
4918            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
4919        assert!(
4920            rhs_buffer1_folded,
4921            "buffer1 should be folded in rhs after split"
4922        );
4923
4924        let base_buffer1_id = diff1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
4925        let lhs_buffer1_folded = lhs_editor.read_with(cx, |editor, cx| {
4926            editor.is_buffer_folded(base_buffer1_id, cx)
4927        });
4928        assert!(
4929            lhs_buffer1_folded,
4930            "buffer1 should be folded in lhs after split"
4931        );
4932
4933        assert_split_content(
4934            &editor,
4935            "
4936            § <no file>
4937            § -----
4938            § <no file>
4939            § -----
4940            ddd
4941            eee
4942            fff"
4943            .unindent(),
4944            "
4945            § <no file>
4946            § -----
4947            § <no file>
4948            § -----
4949            ddd
4950            eee
4951            fff"
4952            .unindent(),
4953            &mut cx,
4954        );
4955
4956        editor.update(cx, |editor, cx| {
4957            editor.rhs_editor.update(cx, |rhs_editor, cx| {
4958                rhs_editor.fold_buffer(buffer2_id, cx);
4959            });
4960        });
4961
4962        cx.run_until_parked();
4963
4964        let rhs_buffer2_folded =
4965            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer2_id, cx));
4966        assert!(rhs_buffer2_folded, "buffer2 should be folded in rhs");
4967
4968        let base_buffer2_id = diff2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
4969        let lhs_buffer2_folded = lhs_editor.read_with(cx, |editor, cx| {
4970            editor.is_buffer_folded(base_buffer2_id, cx)
4971        });
4972        assert!(lhs_buffer2_folded, "buffer2 should be folded in lhs");
4973
4974        let rhs_buffer1_still_folded =
4975            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
4976        assert!(
4977            rhs_buffer1_still_folded,
4978            "buffer1 should still be folded in rhs"
4979        );
4980
4981        let lhs_buffer1_still_folded = lhs_editor.read_with(cx, |editor, cx| {
4982            editor.is_buffer_folded(base_buffer1_id, cx)
4983        });
4984        assert!(
4985            lhs_buffer1_still_folded,
4986            "buffer1 should still be folded in lhs"
4987        );
4988
4989        assert_split_content(
4990            &editor,
4991            "
4992            § <no file>
4993            § -----
4994            § <no file>
4995            § -----"
4996                .unindent(),
4997            "
4998            § <no file>
4999            § -----
5000            § <no file>
5001            § -----"
5002                .unindent(),
5003            &mut cx,
5004        );
5005    }
5006
5007    #[gpui::test]
5008    async fn test_custom_block_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5009        use rope::Point;
5010        use unindent::Unindent as _;
5011
5012        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5013
5014        let base_text = "
5015            ddd
5016            eee
5017        "
5018        .unindent();
5019        let current_text = "
5020            aaa
5021            bbb
5022            ccc
5023            ddd
5024            eee
5025        "
5026        .unindent();
5027
5028        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5029
5030        editor.update(cx, |editor, cx| {
5031            let path = PathKey::for_buffer(&buffer, cx);
5032            editor.set_excerpts_for_path(
5033                path,
5034                buffer.clone(),
5035                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5036                0,
5037                diff.clone(),
5038                cx,
5039            );
5040        });
5041
5042        cx.run_until_parked();
5043
5044        assert_split_content(
5045            &editor,
5046            "
5047            § <no file>
5048            § -----
5049            aaa
5050            bbb
5051            ccc
5052            ddd
5053            eee"
5054            .unindent(),
5055            "
5056            § <no file>
5057            § -----
5058            § spacer
5059            § spacer
5060            § spacer
5061            ddd
5062            eee"
5063            .unindent(),
5064            &mut cx,
5065        );
5066
5067        let block_ids = editor.update(cx, |splittable_editor, cx| {
5068            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5069                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5070                let anchor = snapshot.anchor_before(Point::new(2, 0));
5071                rhs_editor.insert_blocks(
5072                    [BlockProperties {
5073                        placement: BlockPlacement::Above(anchor),
5074                        height: Some(1),
5075                        style: BlockStyle::Fixed,
5076                        render: Arc::new(|_| div().into_any()),
5077                        priority: 0,
5078                    }],
5079                    None,
5080                    cx,
5081                )
5082            })
5083        });
5084
5085        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5086        let lhs_editor =
5087            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5088
5089        cx.update(|_, cx| {
5090            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5091                "custom block".to_string()
5092            });
5093        });
5094
5095        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5096            let display_map = lhs_editor.display_map.read(cx);
5097            let companion = display_map.companion().unwrap().read(cx);
5098            let mapping = companion
5099                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5100            *mapping.borrow().get(&block_ids[0]).unwrap()
5101        });
5102
5103        cx.update(|_, cx| {
5104            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5105                "custom block".to_string()
5106            });
5107        });
5108
5109        cx.run_until_parked();
5110
5111        assert_split_content(
5112            &editor,
5113            "
5114            § <no file>
5115            § -----
5116            aaa
5117            bbb
5118            § custom block
5119            ccc
5120            ddd
5121            eee"
5122            .unindent(),
5123            "
5124            § <no file>
5125            § -----
5126            § spacer
5127            § spacer
5128            § spacer
5129            § custom block
5130            ddd
5131            eee"
5132            .unindent(),
5133            &mut cx,
5134        );
5135
5136        editor.update(cx, |splittable_editor, cx| {
5137            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5138                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5139            });
5140        });
5141
5142        cx.run_until_parked();
5143
5144        assert_split_content(
5145            &editor,
5146            "
5147            § <no file>
5148            § -----
5149            aaa
5150            bbb
5151            ccc
5152            ddd
5153            eee"
5154            .unindent(),
5155            "
5156            § <no file>
5157            § -----
5158            § spacer
5159            § spacer
5160            § spacer
5161            ddd
5162            eee"
5163            .unindent(),
5164            &mut cx,
5165        );
5166    }
5167
5168    #[gpui::test]
5169    async fn test_custom_block_below_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5170        use rope::Point;
5171        use unindent::Unindent as _;
5172
5173        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5174
5175        let base_text = "
5176            ddd
5177            eee
5178        "
5179        .unindent();
5180        let current_text = "
5181            aaa
5182            bbb
5183            ccc
5184            ddd
5185            eee
5186        "
5187        .unindent();
5188
5189        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5190
5191        editor.update(cx, |editor, cx| {
5192            let path = PathKey::for_buffer(&buffer, cx);
5193            editor.set_excerpts_for_path(
5194                path,
5195                buffer.clone(),
5196                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5197                0,
5198                diff.clone(),
5199                cx,
5200            );
5201        });
5202
5203        cx.run_until_parked();
5204
5205        assert_split_content(
5206            &editor,
5207            "
5208            § <no file>
5209            § -----
5210            aaa
5211            bbb
5212            ccc
5213            ddd
5214            eee"
5215            .unindent(),
5216            "
5217            § <no file>
5218            § -----
5219            § spacer
5220            § spacer
5221            § spacer
5222            ddd
5223            eee"
5224            .unindent(),
5225            &mut cx,
5226        );
5227
5228        let block_ids = editor.update(cx, |splittable_editor, cx| {
5229            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5230                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5231                let anchor = snapshot.anchor_after(Point::new(1, 3));
5232                rhs_editor.insert_blocks(
5233                    [BlockProperties {
5234                        placement: BlockPlacement::Below(anchor),
5235                        height: Some(1),
5236                        style: BlockStyle::Fixed,
5237                        render: Arc::new(|_| div().into_any()),
5238                        priority: 0,
5239                    }],
5240                    None,
5241                    cx,
5242                )
5243            })
5244        });
5245
5246        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5247        let lhs_editor =
5248            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5249
5250        cx.update(|_, cx| {
5251            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5252                "custom block".to_string()
5253            });
5254        });
5255
5256        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5257            let display_map = lhs_editor.display_map.read(cx);
5258            let companion = display_map.companion().unwrap().read(cx);
5259            let mapping = companion
5260                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5261            *mapping.borrow().get(&block_ids[0]).unwrap()
5262        });
5263
5264        cx.update(|_, cx| {
5265            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5266                "custom block".to_string()
5267            });
5268        });
5269
5270        cx.run_until_parked();
5271
5272        assert_split_content(
5273            &editor,
5274            "
5275            § <no file>
5276            § -----
5277            aaa
5278            bbb
5279            § custom block
5280            ccc
5281            ddd
5282            eee"
5283            .unindent(),
5284            "
5285            § <no file>
5286            § -----
5287            § spacer
5288            § spacer
5289            § spacer
5290            § custom block
5291            ddd
5292            eee"
5293            .unindent(),
5294            &mut cx,
5295        );
5296
5297        editor.update(cx, |splittable_editor, cx| {
5298            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5299                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5300            });
5301        });
5302
5303        cx.run_until_parked();
5304
5305        assert_split_content(
5306            &editor,
5307            "
5308            § <no file>
5309            § -----
5310            aaa
5311            bbb
5312            ccc
5313            ddd
5314            eee"
5315            .unindent(),
5316            "
5317            § <no file>
5318            § -----
5319            § spacer
5320            § spacer
5321            § spacer
5322            ddd
5323            eee"
5324            .unindent(),
5325            &mut cx,
5326        );
5327    }
5328
5329    #[gpui::test]
5330    async fn test_custom_block_resize_syncs_balancing_block(cx: &mut gpui::TestAppContext) {
5331        use rope::Point;
5332        use unindent::Unindent as _;
5333
5334        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5335
5336        let base_text = "
5337            bbb
5338            ccc
5339        "
5340        .unindent();
5341        let current_text = "
5342            aaa
5343            bbb
5344            ccc
5345        "
5346        .unindent();
5347
5348        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5349
5350        editor.update(cx, |editor, cx| {
5351            let path = PathKey::for_buffer(&buffer, cx);
5352            editor.set_excerpts_for_path(
5353                path,
5354                buffer.clone(),
5355                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5356                0,
5357                diff.clone(),
5358                cx,
5359            );
5360        });
5361
5362        cx.run_until_parked();
5363
5364        let block_ids = editor.update(cx, |splittable_editor, cx| {
5365            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5366                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5367                let anchor = snapshot.anchor_before(Point::new(2, 0));
5368                rhs_editor.insert_blocks(
5369                    [BlockProperties {
5370                        placement: BlockPlacement::Above(anchor),
5371                        height: Some(1),
5372                        style: BlockStyle::Fixed,
5373                        render: Arc::new(|_| div().into_any()),
5374                        priority: 0,
5375                    }],
5376                    None,
5377                    cx,
5378                )
5379            })
5380        });
5381
5382        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5383        let lhs_editor =
5384            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5385
5386        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5387            let display_map = lhs_editor.display_map.read(cx);
5388            let companion = display_map.companion().unwrap().read(cx);
5389            let mapping = companion
5390                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5391            *mapping.borrow().get(&block_ids[0]).unwrap()
5392        });
5393
5394        cx.run_until_parked();
5395
5396        let get_block_height = |editor: &Entity<crate::Editor>,
5397                                block_id: crate::CustomBlockId,
5398                                cx: &mut VisualTestContext| {
5399            editor.update_in(cx, |editor, window, cx| {
5400                let snapshot = editor.snapshot(window, cx);
5401                snapshot
5402                    .block_for_id(crate::BlockId::Custom(block_id))
5403                    .map(|block| block.height())
5404            })
5405        };
5406
5407        assert_eq!(
5408            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5409            Some(1)
5410        );
5411        assert_eq!(
5412            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5413            Some(1)
5414        );
5415
5416        editor.update(cx, |splittable_editor, cx| {
5417            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5418                let mut heights = HashMap::default();
5419                heights.insert(block_ids[0], 3);
5420                rhs_editor.resize_blocks(heights, None, cx);
5421            });
5422        });
5423
5424        cx.run_until_parked();
5425
5426        assert_eq!(
5427            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5428            Some(3)
5429        );
5430        assert_eq!(
5431            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5432            Some(3)
5433        );
5434
5435        editor.update(cx, |splittable_editor, cx| {
5436            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5437                let mut heights = HashMap::default();
5438                heights.insert(block_ids[0], 5);
5439                rhs_editor.resize_blocks(heights, None, cx);
5440            });
5441        });
5442
5443        cx.run_until_parked();
5444
5445        assert_eq!(
5446            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5447            Some(5)
5448        );
5449        assert_eq!(
5450            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5451            Some(5)
5452        );
5453    }
5454}