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