split.rs

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