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