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