split.rs

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