split.rs

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