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