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