split.rs

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