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