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