split.rs

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