split.rs

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