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