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