split.rs

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