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