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