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