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: &mut dyn 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            let main_buffer_entity = rhs_multibuffer
2073                .buffer(main_buffer.remote_id())
2074                .expect("main buffer should exist in rhs_multibuffer");
2075            lhs_multibuffer.add_inverted_diff(diff, main_buffer_entity, lhs_cx);
2076        }
2077
2078        let rhs_merge_groups: Vec<Vec<ExcerptId>> = {
2079            let mut groups = Vec::new();
2080            let mut current_group = Vec::new();
2081            let mut last_id = None;
2082
2083            for (lhs_id, rhs_id) in lhs_result.excerpt_ids.iter().zip(rhs_excerpt_ids) {
2084                if last_id == Some(lhs_id) {
2085                    current_group.push(rhs_id);
2086                } else {
2087                    if !current_group.is_empty() {
2088                        groups.push(current_group);
2089                    }
2090                    current_group = vec![rhs_id];
2091                    last_id = Some(lhs_id);
2092                }
2093            }
2094            if !current_group.is_empty() {
2095                groups.push(current_group);
2096            }
2097            groups
2098        };
2099
2100        let deduplicated_lhs_ids: Vec<ExcerptId> =
2101            lhs_result.excerpt_ids.iter().dedup().copied().collect();
2102
2103        Some((deduplicated_lhs_ids, rhs_merge_groups))
2104    }
2105
2106    fn sync_path_excerpts(
2107        path_key: PathKey,
2108        old_rhs_excerpt_ids: Vec<ExcerptId>,
2109        rhs_multibuffer: &MultiBuffer,
2110        lhs_multibuffer: &mut MultiBuffer,
2111        diff: Entity<BufferDiff>,
2112        rhs_display_map: &Entity<DisplayMap>,
2113        lhs_cx: &mut Context<MultiBuffer>,
2114    ) -> Option<(Vec<ExcerptId>, Vec<Vec<ExcerptId>>)> {
2115        let old_lhs_excerpt_ids: Vec<ExcerptId> =
2116            lhs_multibuffer.excerpts_for_path(&path_key).collect();
2117
2118        if let Some(companion) = rhs_display_map.read(lhs_cx).companion().cloned() {
2119            companion.update(lhs_cx, |c, _| {
2120                c.remove_excerpt_mappings(old_lhs_excerpt_ids, old_rhs_excerpt_ids);
2121            });
2122        }
2123
2124        Self::update_path_excerpts_from_rhs(
2125            path_key,
2126            rhs_multibuffer,
2127            lhs_multibuffer,
2128            diff,
2129            lhs_cx,
2130        )
2131    }
2132}
2133
2134#[cfg(test)]
2135mod tests {
2136    use std::sync::Arc;
2137
2138    use buffer_diff::BufferDiff;
2139    use collections::{HashMap, HashSet};
2140    use fs::FakeFs;
2141    use gpui::Element as _;
2142    use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
2143    use language::language_settings::SoftWrap;
2144    use language::{Buffer, Capability};
2145    use multi_buffer::{MultiBuffer, PathKey};
2146    use pretty_assertions::assert_eq;
2147    use project::Project;
2148    use rand::rngs::StdRng;
2149    use settings::{DiffViewStyle, SettingsStore};
2150    use ui::{VisualContext as _, div, px};
2151    use util::rel_path::rel_path;
2152    use workspace::MultiWorkspace;
2153
2154    use crate::SplittableEditor;
2155    use crate::display_map::{
2156        BlockPlacement, BlockProperties, BlockStyle, Crease, FoldPlaceholder,
2157    };
2158    use crate::inlays::Inlay;
2159    use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
2160    use multi_buffer::MultiBufferOffset;
2161
2162    async fn init_test(
2163        cx: &mut gpui::TestAppContext,
2164        soft_wrap: SoftWrap,
2165        style: DiffViewStyle,
2166    ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
2167        cx.update(|cx| {
2168            let store = SettingsStore::test(cx);
2169            cx.set_global(store);
2170            theme::init(theme::LoadThemes::JustBase, cx);
2171            crate::init(cx);
2172        });
2173        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
2174        let (multi_workspace, cx) =
2175            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2176        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2177        let rhs_multibuffer = cx.new(|cx| {
2178            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2179            multibuffer.set_all_diff_hunks_expanded(cx);
2180            multibuffer
2181        });
2182        let editor = cx.new_window_entity(|window, cx| {
2183            let editor = SplittableEditor::new(
2184                style,
2185                rhs_multibuffer.clone(),
2186                project.clone(),
2187                workspace,
2188                window,
2189                cx,
2190            );
2191            editor.rhs_editor.update(cx, |editor, cx| {
2192                editor.set_soft_wrap_mode(soft_wrap, cx);
2193            });
2194            if let Some(lhs) = &editor.lhs {
2195                lhs.editor.update(cx, |editor, cx| {
2196                    editor.set_soft_wrap_mode(soft_wrap, cx);
2197                });
2198            }
2199            editor
2200        });
2201        (editor, cx)
2202    }
2203
2204    fn buffer_with_diff(
2205        base_text: &str,
2206        current_text: &str,
2207        cx: &mut VisualTestContext,
2208    ) -> (Entity<Buffer>, Entity<BufferDiff>) {
2209        let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2210        let diff = cx.new(|cx| {
2211            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
2212        });
2213        (buffer, diff)
2214    }
2215
2216    #[track_caller]
2217    fn assert_split_content(
2218        editor: &Entity<SplittableEditor>,
2219        expected_rhs: String,
2220        expected_lhs: String,
2221        cx: &mut VisualTestContext,
2222    ) {
2223        assert_split_content_with_widths(
2224            editor,
2225            px(3000.0),
2226            px(3000.0),
2227            expected_rhs,
2228            expected_lhs,
2229            cx,
2230        );
2231    }
2232
2233    #[track_caller]
2234    fn assert_split_content_with_widths(
2235        editor: &Entity<SplittableEditor>,
2236        rhs_width: Pixels,
2237        lhs_width: Pixels,
2238        expected_rhs: String,
2239        expected_lhs: String,
2240        cx: &mut VisualTestContext,
2241    ) {
2242        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2243            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2244            (editor.rhs_editor.clone(), lhs.editor.clone())
2245        });
2246
2247        // Make sure both sides learn if the other has soft-wrapped
2248        let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2249        cx.run_until_parked();
2250        let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2251        cx.run_until_parked();
2252
2253        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2254        let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2255
2256        if rhs_content != expected_rhs || lhs_content != expected_lhs {
2257            editor.update(cx, |editor, cx| editor.debug_print(cx));
2258        }
2259
2260        assert_eq!(rhs_content, expected_rhs, "rhs");
2261        assert_eq!(lhs_content, expected_lhs, "lhs");
2262    }
2263
2264    #[gpui::test(iterations = 25)]
2265    async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2266        use rand::prelude::*;
2267
2268        let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2269        let operations = std::env::var("OPERATIONS")
2270            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2271            .unwrap_or(10);
2272        let rng = &mut rng;
2273        for _ in 0..operations {
2274            let buffers = editor.update(cx, |editor, cx| {
2275                editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2276            });
2277
2278            if buffers.is_empty() {
2279                log::info!("adding excerpts to empty multibuffer");
2280                editor.update(cx, |editor, cx| {
2281                    editor.randomly_edit_excerpts(rng, 2, cx);
2282                    editor.check_invariants(true, cx);
2283                });
2284                continue;
2285            }
2286
2287            let mut quiesced = false;
2288
2289            match rng.random_range(0..100) {
2290                0..=44 => {
2291                    log::info!("randomly editing multibuffer");
2292                    editor.update(cx, |editor, cx| {
2293                        editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2294                            multibuffer.randomly_edit(rng, 5, cx);
2295                        })
2296                    })
2297                }
2298                45..=64 => {
2299                    log::info!("randomly undoing/redoing in single buffer");
2300                    let buffer = buffers.iter().choose(rng).unwrap();
2301                    buffer.update(cx, |buffer, cx| {
2302                        buffer.randomly_undo_redo(rng, cx);
2303                    });
2304                }
2305                65..=79 => {
2306                    log::info!("mutating excerpts");
2307                    editor.update(cx, |editor, cx| {
2308                        editor.randomly_edit_excerpts(rng, 2, cx);
2309                    });
2310                }
2311                _ => {
2312                    log::info!("quiescing");
2313                    for buffer in buffers {
2314                        let buffer_snapshot =
2315                            buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2316                        let diff = editor.update(cx, |editor, cx| {
2317                            editor
2318                                .rhs_multibuffer
2319                                .read(cx)
2320                                .diff_for(buffer.read(cx).remote_id())
2321                                .unwrap()
2322                        });
2323                        diff.update(cx, |diff, cx| {
2324                            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2325                        });
2326                        cx.run_until_parked();
2327                        let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2328                        let ranges = diff_snapshot
2329                            .hunks(&buffer_snapshot)
2330                            .map(|hunk| hunk.range)
2331                            .collect::<Vec<_>>();
2332                        editor.update(cx, |editor, cx| {
2333                            let path = PathKey::for_buffer(&buffer, cx);
2334                            editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2335                        });
2336                    }
2337                    quiesced = true;
2338                }
2339            }
2340
2341            editor.update(cx, |editor, cx| {
2342                editor.check_invariants(quiesced, cx);
2343            });
2344        }
2345    }
2346
2347    #[gpui::test]
2348    async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2349        use rope::Point;
2350        use unindent::Unindent as _;
2351
2352        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2353
2354        let base_text = "
2355            aaa
2356            bbb
2357            ccc
2358            ddd
2359            eee
2360            fff
2361        "
2362        .unindent();
2363        let current_text = "
2364            aaa
2365            ddd
2366            eee
2367            fff
2368        "
2369        .unindent();
2370
2371        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2372
2373        editor.update(cx, |editor, cx| {
2374            let path = PathKey::for_buffer(&buffer, cx);
2375            editor.set_excerpts_for_path(
2376                path,
2377                buffer.clone(),
2378                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2379                0,
2380                diff.clone(),
2381                cx,
2382            );
2383        });
2384
2385        cx.run_until_parked();
2386
2387        assert_split_content(
2388            &editor,
2389            "
2390            § <no file>
2391            § -----
2392            aaa
2393            § spacer
2394            § spacer
2395            ddd
2396            eee
2397            fff"
2398            .unindent(),
2399            "
2400            § <no file>
2401            § -----
2402            aaa
2403            bbb
2404            ccc
2405            ddd
2406            eee
2407            fff"
2408            .unindent(),
2409            &mut cx,
2410        );
2411
2412        buffer.update(cx, |buffer, cx| {
2413            buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2414        });
2415
2416        cx.run_until_parked();
2417
2418        assert_split_content(
2419            &editor,
2420            "
2421            § <no file>
2422            § -----
2423            aaa
2424            § spacer
2425            § spacer
2426            ddd
2427            eee
2428            FFF"
2429            .unindent(),
2430            "
2431            § <no file>
2432            § -----
2433            aaa
2434            bbb
2435            ccc
2436            ddd
2437            eee
2438            fff"
2439            .unindent(),
2440            &mut cx,
2441        );
2442
2443        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2444        diff.update(cx, |diff, cx| {
2445            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2446        });
2447
2448        cx.run_until_parked();
2449
2450        assert_split_content(
2451            &editor,
2452            "
2453            § <no file>
2454            § -----
2455            aaa
2456            § spacer
2457            § spacer
2458            ddd
2459            eee
2460            FFF"
2461            .unindent(),
2462            "
2463            § <no file>
2464            § -----
2465            aaa
2466            bbb
2467            ccc
2468            ddd
2469            eee
2470            fff"
2471            .unindent(),
2472            &mut cx,
2473        );
2474    }
2475
2476    #[gpui::test]
2477    async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2478        use rope::Point;
2479        use unindent::Unindent as _;
2480
2481        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2482
2483        let base_text1 = "
2484            aaa
2485            bbb
2486            ccc
2487            ddd
2488            eee"
2489        .unindent();
2490
2491        let base_text2 = "
2492            fff
2493            ggg
2494            hhh
2495            iii
2496            jjj"
2497        .unindent();
2498
2499        let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2500        let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2501
2502        editor.update(cx, |editor, cx| {
2503            let path1 = PathKey::for_buffer(&buffer1, cx);
2504            editor.set_excerpts_for_path(
2505                path1,
2506                buffer1.clone(),
2507                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2508                0,
2509                diff1.clone(),
2510                cx,
2511            );
2512            let path2 = PathKey::for_buffer(&buffer2, cx);
2513            editor.set_excerpts_for_path(
2514                path2,
2515                buffer2.clone(),
2516                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2517                1,
2518                diff2.clone(),
2519                cx,
2520            );
2521        });
2522
2523        cx.run_until_parked();
2524
2525        buffer1.update(cx, |buffer, cx| {
2526            buffer.edit(
2527                [
2528                    (Point::new(0, 0)..Point::new(1, 0), ""),
2529                    (Point::new(3, 0)..Point::new(4, 0), ""),
2530                ],
2531                None,
2532                cx,
2533            );
2534        });
2535        buffer2.update(cx, |buffer, cx| {
2536            buffer.edit(
2537                [
2538                    (Point::new(0, 0)..Point::new(1, 0), ""),
2539                    (Point::new(3, 0)..Point::new(4, 0), ""),
2540                ],
2541                None,
2542                cx,
2543            );
2544        });
2545
2546        cx.run_until_parked();
2547
2548        assert_split_content(
2549            &editor,
2550            "
2551            § <no file>
2552            § -----
2553            § spacer
2554            bbb
2555            ccc
2556            § spacer
2557            eee
2558            § <no file>
2559            § -----
2560            § spacer
2561            ggg
2562            hhh
2563            § spacer
2564            jjj"
2565            .unindent(),
2566            "
2567            § <no file>
2568            § -----
2569            aaa
2570            bbb
2571            ccc
2572            ddd
2573            eee
2574            § <no file>
2575            § -----
2576            fff
2577            ggg
2578            hhh
2579            iii
2580            jjj"
2581            .unindent(),
2582            &mut cx,
2583        );
2584
2585        let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2586        diff1.update(cx, |diff, cx| {
2587            diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2588        });
2589        let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2590        diff2.update(cx, |diff, cx| {
2591            diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2592        });
2593
2594        cx.run_until_parked();
2595
2596        assert_split_content(
2597            &editor,
2598            "
2599            § <no file>
2600            § -----
2601            § spacer
2602            bbb
2603            ccc
2604            § spacer
2605            eee
2606            § <no file>
2607            § -----
2608            § spacer
2609            ggg
2610            hhh
2611            § spacer
2612            jjj"
2613            .unindent(),
2614            "
2615            § <no file>
2616            § -----
2617            aaa
2618            bbb
2619            ccc
2620            ddd
2621            eee
2622            § <no file>
2623            § -----
2624            fff
2625            ggg
2626            hhh
2627            iii
2628            jjj"
2629            .unindent(),
2630            &mut cx,
2631        );
2632    }
2633
2634    #[gpui::test]
2635    async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2636        use rope::Point;
2637        use unindent::Unindent as _;
2638
2639        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2640
2641        let base_text = "
2642            aaa
2643            bbb
2644            ccc
2645            ddd
2646        "
2647        .unindent();
2648
2649        let current_text = "
2650            aaa
2651            NEW1
2652            NEW2
2653            ccc
2654            ddd
2655        "
2656        .unindent();
2657
2658        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2659
2660        editor.update(cx, |editor, cx| {
2661            let path = PathKey::for_buffer(&buffer, cx);
2662            editor.set_excerpts_for_path(
2663                path,
2664                buffer.clone(),
2665                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2666                0,
2667                diff.clone(),
2668                cx,
2669            );
2670        });
2671
2672        cx.run_until_parked();
2673
2674        assert_split_content(
2675            &editor,
2676            "
2677            § <no file>
2678            § -----
2679            aaa
2680            NEW1
2681            NEW2
2682            ccc
2683            ddd"
2684            .unindent(),
2685            "
2686            § <no file>
2687            § -----
2688            aaa
2689            bbb
2690            § spacer
2691            ccc
2692            ddd"
2693            .unindent(),
2694            &mut cx,
2695        );
2696
2697        buffer.update(cx, |buffer, cx| {
2698            buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2699        });
2700
2701        cx.run_until_parked();
2702
2703        assert_split_content(
2704            &editor,
2705            "
2706            § <no file>
2707            § -----
2708            aaa
2709            NEW1
2710            ccc
2711            ddd"
2712            .unindent(),
2713            "
2714            § <no file>
2715            § -----
2716            aaa
2717            bbb
2718            ccc
2719            ddd"
2720            .unindent(),
2721            &mut cx,
2722        );
2723
2724        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2725        diff.update(cx, |diff, cx| {
2726            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2727        });
2728
2729        cx.run_until_parked();
2730
2731        assert_split_content(
2732            &editor,
2733            "
2734            § <no file>
2735            § -----
2736            aaa
2737            NEW1
2738            ccc
2739            ddd"
2740            .unindent(),
2741            "
2742            § <no file>
2743            § -----
2744            aaa
2745            bbb
2746            ccc
2747            ddd"
2748            .unindent(),
2749            &mut cx,
2750        );
2751    }
2752
2753    #[gpui::test]
2754    async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2755        use rope::Point;
2756        use unindent::Unindent as _;
2757
2758        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2759
2760        let base_text = "
2761            aaa
2762            bbb
2763
2764
2765
2766
2767
2768            ccc
2769            ddd
2770        "
2771        .unindent();
2772        let current_text = "
2773            aaa
2774            bbb
2775
2776
2777
2778
2779
2780            CCC
2781            ddd
2782        "
2783        .unindent();
2784
2785        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2786
2787        editor.update(cx, |editor, cx| {
2788            let path = PathKey::for_buffer(&buffer, cx);
2789            editor.set_excerpts_for_path(
2790                path,
2791                buffer.clone(),
2792                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2793                0,
2794                diff.clone(),
2795                cx,
2796            );
2797        });
2798
2799        cx.run_until_parked();
2800
2801        buffer.update(cx, |buffer, cx| {
2802            buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2803        });
2804
2805        cx.run_until_parked();
2806
2807        assert_split_content(
2808            &editor,
2809            "
2810            § <no file>
2811            § -----
2812            aaa
2813            bbb
2814
2815
2816
2817
2818
2819
2820            CCC
2821            ddd"
2822            .unindent(),
2823            "
2824            § <no file>
2825            § -----
2826            aaa
2827            bbb
2828            § spacer
2829
2830
2831
2832
2833
2834            ccc
2835            ddd"
2836            .unindent(),
2837            &mut cx,
2838        );
2839
2840        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2841        diff.update(cx, |diff, cx| {
2842            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2843        });
2844
2845        cx.run_until_parked();
2846
2847        assert_split_content(
2848            &editor,
2849            "
2850            § <no file>
2851            § -----
2852            aaa
2853            bbb
2854
2855
2856
2857
2858
2859
2860            CCC
2861            ddd"
2862            .unindent(),
2863            "
2864            § <no file>
2865            § -----
2866            aaa
2867            bbb
2868
2869
2870
2871
2872
2873            ccc
2874            § spacer
2875            ddd"
2876            .unindent(),
2877            &mut cx,
2878        );
2879    }
2880
2881    #[gpui::test]
2882    async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2883        use git::Restore;
2884        use rope::Point;
2885        use unindent::Unindent as _;
2886
2887        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2888
2889        let base_text = "
2890            aaa
2891            bbb
2892            ccc
2893            ddd
2894            eee
2895        "
2896        .unindent();
2897        let current_text = "
2898            aaa
2899            ddd
2900            eee
2901        "
2902        .unindent();
2903
2904        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2905
2906        editor.update(cx, |editor, cx| {
2907            let path = PathKey::for_buffer(&buffer, cx);
2908            editor.set_excerpts_for_path(
2909                path,
2910                buffer.clone(),
2911                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2912                0,
2913                diff.clone(),
2914                cx,
2915            );
2916        });
2917
2918        cx.run_until_parked();
2919
2920        assert_split_content(
2921            &editor,
2922            "
2923            § <no file>
2924            § -----
2925            aaa
2926            § spacer
2927            § spacer
2928            ddd
2929            eee"
2930            .unindent(),
2931            "
2932            § <no file>
2933            § -----
2934            aaa
2935            bbb
2936            ccc
2937            ddd
2938            eee"
2939            .unindent(),
2940            &mut cx,
2941        );
2942
2943        let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
2944        cx.update_window_entity(&rhs_editor, |editor, window, cx| {
2945            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2946                s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2947            });
2948            editor.git_restore(&Restore, window, cx);
2949        });
2950
2951        cx.run_until_parked();
2952
2953        assert_split_content(
2954            &editor,
2955            "
2956            § <no file>
2957            § -----
2958            aaa
2959            bbb
2960            ccc
2961            ddd
2962            eee"
2963            .unindent(),
2964            "
2965            § <no file>
2966            § -----
2967            aaa
2968            bbb
2969            ccc
2970            ddd
2971            eee"
2972            .unindent(),
2973            &mut cx,
2974        );
2975
2976        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2977        diff.update(cx, |diff, cx| {
2978            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2979        });
2980
2981        cx.run_until_parked();
2982
2983        assert_split_content(
2984            &editor,
2985            "
2986            § <no file>
2987            § -----
2988            aaa
2989            bbb
2990            ccc
2991            ddd
2992            eee"
2993            .unindent(),
2994            "
2995            § <no file>
2996            § -----
2997            aaa
2998            bbb
2999            ccc
3000            ddd
3001            eee"
3002            .unindent(),
3003            &mut cx,
3004        );
3005    }
3006
3007    #[gpui::test]
3008    async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
3009        use rope::Point;
3010        use unindent::Unindent as _;
3011
3012        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3013
3014        let base_text = "
3015            aaa
3016            old1
3017            old2
3018            old3
3019            old4
3020            zzz
3021        "
3022        .unindent();
3023
3024        let current_text = "
3025            aaa
3026            new1
3027            new2
3028            new3
3029            new4
3030            zzz
3031        "
3032        .unindent();
3033
3034        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3035
3036        editor.update(cx, |editor, cx| {
3037            let path = PathKey::for_buffer(&buffer, cx);
3038            editor.set_excerpts_for_path(
3039                path,
3040                buffer.clone(),
3041                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3042                0,
3043                diff.clone(),
3044                cx,
3045            );
3046        });
3047
3048        cx.run_until_parked();
3049
3050        buffer.update(cx, |buffer, cx| {
3051            buffer.edit(
3052                [
3053                    (Point::new(2, 0)..Point::new(3, 0), ""),
3054                    (Point::new(4, 0)..Point::new(5, 0), ""),
3055                ],
3056                None,
3057                cx,
3058            );
3059        });
3060        cx.run_until_parked();
3061
3062        assert_split_content(
3063            &editor,
3064            "
3065            § <no file>
3066            § -----
3067            aaa
3068            new1
3069            new3
3070            § spacer
3071            § spacer
3072            zzz"
3073            .unindent(),
3074            "
3075            § <no file>
3076            § -----
3077            aaa
3078            old1
3079            old2
3080            old3
3081            old4
3082            zzz"
3083            .unindent(),
3084            &mut cx,
3085        );
3086
3087        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3088        diff.update(cx, |diff, cx| {
3089            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3090        });
3091
3092        cx.run_until_parked();
3093
3094        assert_split_content(
3095            &editor,
3096            "
3097            § <no file>
3098            § -----
3099            aaa
3100            new1
3101            new3
3102            § spacer
3103            § spacer
3104            zzz"
3105            .unindent(),
3106            "
3107            § <no file>
3108            § -----
3109            aaa
3110            old1
3111            old2
3112            old3
3113            old4
3114            zzz"
3115            .unindent(),
3116            &mut cx,
3117        );
3118    }
3119
3120    #[gpui::test]
3121    async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
3122        use rope::Point;
3123        use unindent::Unindent as _;
3124
3125        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3126
3127        let text = "aaaa bbbb cccc dddd eeee ffff";
3128
3129        let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
3130        let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
3131
3132        editor.update(cx, |editor, cx| {
3133            let end = Point::new(0, text.len() as u32);
3134            let path1 = PathKey::for_buffer(&buffer1, cx);
3135            editor.set_excerpts_for_path(
3136                path1,
3137                buffer1.clone(),
3138                vec![Point::new(0, 0)..end],
3139                0,
3140                diff1.clone(),
3141                cx,
3142            );
3143            let path2 = PathKey::for_buffer(&buffer2, cx);
3144            editor.set_excerpts_for_path(
3145                path2,
3146                buffer2.clone(),
3147                vec![Point::new(0, 0)..end],
3148                0,
3149                diff2.clone(),
3150                cx,
3151            );
3152        });
3153
3154        cx.run_until_parked();
3155
3156        assert_split_content_with_widths(
3157            &editor,
3158            px(200.0),
3159            px(400.0),
3160            "
3161            § <no file>
3162            § -----
3163            aaaa bbbb\x20
3164            cccc dddd\x20
3165            eeee ffff
3166            § <no file>
3167            § -----
3168            aaaa bbbb\x20
3169            cccc dddd\x20
3170            eeee ffff"
3171                .unindent(),
3172            "
3173            § <no file>
3174            § -----
3175            aaaa bbbb cccc dddd eeee ffff
3176            § spacer
3177            § spacer
3178            § <no file>
3179            § -----
3180            aaaa bbbb cccc dddd eeee ffff
3181            § spacer
3182            § spacer"
3183                .unindent(),
3184            &mut cx,
3185        );
3186    }
3187
3188    #[gpui::test]
3189    async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
3190        use rope::Point;
3191        use unindent::Unindent as _;
3192
3193        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3194
3195        let base_text = "
3196            aaaa bbbb cccc dddd eeee ffff
3197            old line one
3198            old line two
3199        "
3200        .unindent();
3201
3202        let current_text = "
3203            aaaa bbbb cccc dddd eeee ffff
3204            new line
3205        "
3206        .unindent();
3207
3208        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3209
3210        editor.update(cx, |editor, cx| {
3211            let path = PathKey::for_buffer(&buffer, cx);
3212            editor.set_excerpts_for_path(
3213                path,
3214                buffer.clone(),
3215                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3216                0,
3217                diff.clone(),
3218                cx,
3219            );
3220        });
3221
3222        cx.run_until_parked();
3223
3224        assert_split_content_with_widths(
3225            &editor,
3226            px(200.0),
3227            px(400.0),
3228            "
3229            § <no file>
3230            § -----
3231            aaaa bbbb\x20
3232            cccc dddd\x20
3233            eeee ffff
3234            new line
3235            § spacer"
3236                .unindent(),
3237            "
3238            § <no file>
3239            § -----
3240            aaaa bbbb cccc dddd eeee ffff
3241            § spacer
3242            § spacer
3243            old line one
3244            old line two"
3245                .unindent(),
3246            &mut cx,
3247        );
3248    }
3249
3250    #[gpui::test]
3251    async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3252        use rope::Point;
3253        use unindent::Unindent as _;
3254
3255        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3256
3257        let base_text = "
3258            aaaa bbbb cccc dddd eeee ffff
3259            deleted line one
3260            deleted line two
3261            after
3262        "
3263        .unindent();
3264
3265        let current_text = "
3266            aaaa bbbb cccc dddd eeee ffff
3267            after
3268        "
3269        .unindent();
3270
3271        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3272
3273        editor.update(cx, |editor, cx| {
3274            let path = PathKey::for_buffer(&buffer, cx);
3275            editor.set_excerpts_for_path(
3276                path,
3277                buffer.clone(),
3278                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3279                0,
3280                diff.clone(),
3281                cx,
3282            );
3283        });
3284
3285        cx.run_until_parked();
3286
3287        assert_split_content_with_widths(
3288            &editor,
3289            px(400.0),
3290            px(200.0),
3291            "
3292            § <no file>
3293            § -----
3294            aaaa bbbb cccc dddd eeee ffff
3295            § spacer
3296            § spacer
3297            § spacer
3298            § spacer
3299            § spacer
3300            § spacer
3301            after"
3302                .unindent(),
3303            "
3304            § <no file>
3305            § -----
3306            aaaa bbbb\x20
3307            cccc dddd\x20
3308            eeee ffff
3309            deleted line\x20
3310            one
3311            deleted line\x20
3312            two
3313            after"
3314                .unindent(),
3315            &mut cx,
3316        );
3317    }
3318
3319    #[gpui::test]
3320    async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3321        use rope::Point;
3322        use unindent::Unindent as _;
3323
3324        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3325
3326        let text = "
3327            aaaa bbbb cccc dddd eeee ffff
3328            short
3329        "
3330        .unindent();
3331
3332        let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3333
3334        editor.update(cx, |editor, cx| {
3335            let path = PathKey::for_buffer(&buffer, cx);
3336            editor.set_excerpts_for_path(
3337                path,
3338                buffer.clone(),
3339                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3340                0,
3341                diff.clone(),
3342                cx,
3343            );
3344        });
3345
3346        cx.run_until_parked();
3347
3348        assert_split_content_with_widths(
3349            &editor,
3350            px(400.0),
3351            px(200.0),
3352            "
3353            § <no file>
3354            § -----
3355            aaaa bbbb cccc dddd eeee ffff
3356            § spacer
3357            § spacer
3358            short"
3359                .unindent(),
3360            "
3361            § <no file>
3362            § -----
3363            aaaa bbbb\x20
3364            cccc dddd\x20
3365            eeee ffff
3366            short"
3367                .unindent(),
3368            &mut cx,
3369        );
3370
3371        buffer.update(cx, |buffer, cx| {
3372            buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3373        });
3374
3375        cx.run_until_parked();
3376
3377        assert_split_content_with_widths(
3378            &editor,
3379            px(400.0),
3380            px(200.0),
3381            "
3382            § <no file>
3383            § -----
3384            aaaa bbbb cccc dddd eeee ffff
3385            § spacer
3386            § spacer
3387            modified"
3388                .unindent(),
3389            "
3390            § <no file>
3391            § -----
3392            aaaa bbbb\x20
3393            cccc dddd\x20
3394            eeee ffff
3395            short"
3396                .unindent(),
3397            &mut cx,
3398        );
3399
3400        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3401        diff.update(cx, |diff, cx| {
3402            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3403        });
3404
3405        cx.run_until_parked();
3406
3407        assert_split_content_with_widths(
3408            &editor,
3409            px(400.0),
3410            px(200.0),
3411            "
3412            § <no file>
3413            § -----
3414            aaaa bbbb cccc dddd eeee ffff
3415            § spacer
3416            § spacer
3417            modified"
3418                .unindent(),
3419            "
3420            § <no file>
3421            § -----
3422            aaaa bbbb\x20
3423            cccc dddd\x20
3424            eeee ffff
3425            short"
3426                .unindent(),
3427            &mut cx,
3428        );
3429    }
3430
3431    #[gpui::test]
3432    async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3433        use rope::Point;
3434        use unindent::Unindent as _;
3435
3436        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3437
3438        let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3439
3440        let current_text = "
3441            aaa
3442            bbb
3443            ccc
3444        "
3445        .unindent();
3446
3447        let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3448        let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3449
3450        editor.update(cx, |editor, cx| {
3451            let path1 = PathKey::for_buffer(&buffer1, cx);
3452            editor.set_excerpts_for_path(
3453                path1,
3454                buffer1.clone(),
3455                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3456                0,
3457                diff1.clone(),
3458                cx,
3459            );
3460
3461            let path2 = PathKey::for_buffer(&buffer2, cx);
3462            editor.set_excerpts_for_path(
3463                path2,
3464                buffer2.clone(),
3465                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3466                1,
3467                diff2.clone(),
3468                cx,
3469            );
3470        });
3471
3472        cx.run_until_parked();
3473
3474        assert_split_content(
3475            &editor,
3476            "
3477            § <no file>
3478            § -----
3479            xxx
3480            yyy
3481            § <no file>
3482            § -----
3483            aaa
3484            bbb
3485            ccc"
3486            .unindent(),
3487            "
3488            § <no file>
3489            § -----
3490            xxx
3491            yyy
3492            § <no file>
3493            § -----
3494            § spacer
3495            § spacer
3496            § spacer"
3497                .unindent(),
3498            &mut cx,
3499        );
3500
3501        buffer1.update(cx, |buffer, cx| {
3502            buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3503        });
3504
3505        cx.run_until_parked();
3506
3507        assert_split_content(
3508            &editor,
3509            "
3510            § <no file>
3511            § -----
3512            xxxz
3513            yyy
3514            § <no file>
3515            § -----
3516            aaa
3517            bbb
3518            ccc"
3519            .unindent(),
3520            "
3521            § <no file>
3522            § -----
3523            xxx
3524            yyy
3525            § <no file>
3526            § -----
3527            § spacer
3528            § spacer
3529            § spacer"
3530                .unindent(),
3531            &mut cx,
3532        );
3533    }
3534
3535    #[gpui::test]
3536    async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3537        use rope::Point;
3538        use unindent::Unindent as _;
3539
3540        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3541
3542        let base_text = "
3543            aaa
3544            bbb
3545            ccc
3546        "
3547        .unindent();
3548
3549        let current_text = "
3550            NEW1
3551            NEW2
3552            ccc
3553        "
3554        .unindent();
3555
3556        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3557
3558        editor.update(cx, |editor, cx| {
3559            let path = PathKey::for_buffer(&buffer, cx);
3560            editor.set_excerpts_for_path(
3561                path,
3562                buffer.clone(),
3563                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3564                0,
3565                diff.clone(),
3566                cx,
3567            );
3568        });
3569
3570        cx.run_until_parked();
3571
3572        assert_split_content(
3573            &editor,
3574            "
3575            § <no file>
3576            § -----
3577            NEW1
3578            NEW2
3579            ccc"
3580            .unindent(),
3581            "
3582            § <no file>
3583            § -----
3584            aaa
3585            bbb
3586            ccc"
3587            .unindent(),
3588            &mut cx,
3589        );
3590
3591        buffer.update(cx, |buffer, cx| {
3592            buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3593        });
3594
3595        cx.run_until_parked();
3596
3597        assert_split_content(
3598            &editor,
3599            "
3600            § <no file>
3601            § -----
3602            NEW1
3603            NEW
3604            ccc"
3605            .unindent(),
3606            "
3607            § <no file>
3608            § -----
3609            aaa
3610            bbb
3611            ccc"
3612            .unindent(),
3613            &mut cx,
3614        );
3615    }
3616
3617    #[gpui::test]
3618    async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3619        use rope::Point;
3620        use unindent::Unindent as _;
3621
3622        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3623
3624        let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3625
3626        let current_text = "
3627            aaaa bbbb cccc dddd eeee ffff
3628            added line
3629        "
3630        .unindent();
3631
3632        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3633
3634        editor.update(cx, |editor, cx| {
3635            let path = PathKey::for_buffer(&buffer, cx);
3636            editor.set_excerpts_for_path(
3637                path,
3638                buffer.clone(),
3639                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3640                0,
3641                diff.clone(),
3642                cx,
3643            );
3644        });
3645
3646        cx.run_until_parked();
3647
3648        assert_split_content_with_widths(
3649            &editor,
3650            px(400.0),
3651            px(200.0),
3652            "
3653            § <no file>
3654            § -----
3655            aaaa bbbb cccc dddd eeee ffff
3656            § spacer
3657            § spacer
3658            added line"
3659                .unindent(),
3660            "
3661            § <no file>
3662            § -----
3663            aaaa bbbb\x20
3664            cccc dddd\x20
3665            eeee ffff
3666            § spacer"
3667                .unindent(),
3668            &mut cx,
3669        );
3670
3671        assert_split_content_with_widths(
3672            &editor,
3673            px(200.0),
3674            px(400.0),
3675            "
3676            § <no file>
3677            § -----
3678            aaaa bbbb\x20
3679            cccc dddd\x20
3680            eeee ffff
3681            added line"
3682                .unindent(),
3683            "
3684            § <no file>
3685            § -----
3686            aaaa bbbb cccc dddd eeee ffff
3687            § spacer
3688            § spacer
3689            § spacer"
3690                .unindent(),
3691            &mut cx,
3692        );
3693    }
3694
3695    #[gpui::test]
3696    #[ignore]
3697    async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3698        use rope::Point;
3699        use unindent::Unindent as _;
3700
3701        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3702
3703        let base_text = "
3704            aaa
3705            bbb
3706            ccc
3707            ddd
3708            eee
3709        "
3710        .unindent();
3711
3712        let current_text = "
3713            aaa
3714            NEW
3715            eee
3716        "
3717        .unindent();
3718
3719        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3720
3721        editor.update(cx, |editor, cx| {
3722            let path = PathKey::for_buffer(&buffer, cx);
3723            editor.set_excerpts_for_path(
3724                path,
3725                buffer.clone(),
3726                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3727                0,
3728                diff.clone(),
3729                cx,
3730            );
3731        });
3732
3733        cx.run_until_parked();
3734
3735        assert_split_content(
3736            &editor,
3737            "
3738            § <no file>
3739            § -----
3740            aaa
3741            NEW
3742            § spacer
3743            § spacer
3744            eee"
3745            .unindent(),
3746            "
3747            § <no file>
3748            § -----
3749            aaa
3750            bbb
3751            ccc
3752            ddd
3753            eee"
3754            .unindent(),
3755            &mut cx,
3756        );
3757
3758        buffer.update(cx, |buffer, cx| {
3759            buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3760        });
3761
3762        cx.run_until_parked();
3763
3764        assert_split_content(
3765            &editor,
3766            "
3767            § <no file>
3768            § -----
3769            aaa
3770            § spacer
3771            § spacer
3772            § spacer
3773            NEWeee"
3774                .unindent(),
3775            "
3776            § <no file>
3777            § -----
3778            aaa
3779            bbb
3780            ccc
3781            ddd
3782            eee"
3783            .unindent(),
3784            &mut cx,
3785        );
3786
3787        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3788        diff.update(cx, |diff, cx| {
3789            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3790        });
3791
3792        cx.run_until_parked();
3793
3794        assert_split_content(
3795            &editor,
3796            "
3797            § <no file>
3798            § -----
3799            aaa
3800            NEWeee
3801            § spacer
3802            § spacer
3803            § spacer"
3804                .unindent(),
3805            "
3806            § <no file>
3807            § -----
3808            aaa
3809            bbb
3810            ccc
3811            ddd
3812            eee"
3813            .unindent(),
3814            &mut cx,
3815        );
3816    }
3817
3818    #[gpui::test]
3819    async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3820        use rope::Point;
3821        use unindent::Unindent as _;
3822
3823        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3824
3825        let base_text = "";
3826        let current_text = "
3827            aaaa bbbb cccc dddd eeee ffff
3828            bbb
3829            ccc
3830        "
3831        .unindent();
3832
3833        let (buffer, diff) = buffer_with_diff(base_text, &current_text, &mut cx);
3834
3835        editor.update(cx, |editor, cx| {
3836            let path = PathKey::for_buffer(&buffer, cx);
3837            editor.set_excerpts_for_path(
3838                path,
3839                buffer.clone(),
3840                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3841                0,
3842                diff.clone(),
3843                cx,
3844            );
3845        });
3846
3847        cx.run_until_parked();
3848
3849        assert_split_content(
3850            &editor,
3851            "
3852            § <no file>
3853            § -----
3854            aaaa bbbb cccc dddd eeee ffff
3855            bbb
3856            ccc"
3857            .unindent(),
3858            "
3859            § <no file>
3860            § -----
3861            § spacer
3862            § spacer
3863            § spacer"
3864                .unindent(),
3865            &mut cx,
3866        );
3867
3868        assert_split_content_with_widths(
3869            &editor,
3870            px(200.0),
3871            px(200.0),
3872            "
3873            § <no file>
3874            § -----
3875            aaaa bbbb\x20
3876            cccc dddd\x20
3877            eeee ffff
3878            bbb
3879            ccc"
3880            .unindent(),
3881            "
3882            § <no file>
3883            § -----
3884            § spacer
3885            § spacer
3886            § spacer
3887            § spacer
3888            § spacer"
3889                .unindent(),
3890            &mut cx,
3891        );
3892    }
3893
3894    #[gpui::test]
3895    async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
3896        use rope::Point;
3897        use unindent::Unindent as _;
3898
3899        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3900
3901        let base_text = "
3902            aaa
3903            bbb
3904            ccc
3905        "
3906        .unindent();
3907
3908        let current_text = "
3909            aaa
3910            bbb
3911            xxx
3912            yyy
3913            ccc
3914        "
3915        .unindent();
3916
3917        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3918
3919        editor.update(cx, |editor, cx| {
3920            let path = PathKey::for_buffer(&buffer, cx);
3921            editor.set_excerpts_for_path(
3922                path,
3923                buffer.clone(),
3924                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3925                0,
3926                diff.clone(),
3927                cx,
3928            );
3929        });
3930
3931        cx.run_until_parked();
3932
3933        assert_split_content(
3934            &editor,
3935            "
3936            § <no file>
3937            § -----
3938            aaa
3939            bbb
3940            xxx
3941            yyy
3942            ccc"
3943            .unindent(),
3944            "
3945            § <no file>
3946            § -----
3947            aaa
3948            bbb
3949            § spacer
3950            § spacer
3951            ccc"
3952            .unindent(),
3953            &mut cx,
3954        );
3955
3956        buffer.update(cx, |buffer, cx| {
3957            buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
3958        });
3959
3960        cx.run_until_parked();
3961
3962        assert_split_content(
3963            &editor,
3964            "
3965            § <no file>
3966            § -----
3967            aaa
3968            bbb
3969            xxx
3970            yyy
3971            zzz
3972            ccc"
3973            .unindent(),
3974            "
3975            § <no file>
3976            § -----
3977            aaa
3978            bbb
3979            § spacer
3980            § spacer
3981            § spacer
3982            ccc"
3983            .unindent(),
3984            &mut cx,
3985        );
3986    }
3987
3988    #[gpui::test]
3989    async fn test_scrolling(cx: &mut gpui::TestAppContext) {
3990        use crate::test::editor_content_with_blocks_and_size;
3991        use gpui::size;
3992        use rope::Point;
3993
3994        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
3995
3996        let long_line = "x".repeat(200);
3997        let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
3998        lines[25] = long_line;
3999        let content = lines.join("\n");
4000
4001        let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
4002
4003        editor.update(cx, |editor, cx| {
4004            let path = PathKey::for_buffer(&buffer, cx);
4005            editor.set_excerpts_for_path(
4006                path,
4007                buffer.clone(),
4008                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4009                0,
4010                diff.clone(),
4011                cx,
4012            );
4013        });
4014
4015        cx.run_until_parked();
4016
4017        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
4018            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
4019            (editor.rhs_editor.clone(), lhs.editor.clone())
4020        });
4021
4022        rhs_editor.update_in(cx, |e, window, cx| {
4023            e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
4024        });
4025
4026        let rhs_pos =
4027            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4028        let lhs_pos =
4029            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4030        assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
4031        assert_eq!(
4032            lhs_pos.y, rhs_pos.y,
4033            "LHS should have same scroll position as RHS after set_scroll_position"
4034        );
4035
4036        let draw_size = size(px(300.), px(300.));
4037
4038        rhs_editor.update_in(cx, |e, window, cx| {
4039            e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
4040                s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
4041            });
4042        });
4043
4044        let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
4045        cx.run_until_parked();
4046        let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
4047        cx.run_until_parked();
4048
4049        let rhs_pos =
4050            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4051        let lhs_pos =
4052            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4053
4054        assert!(
4055            rhs_pos.y > 0.,
4056            "RHS should have scrolled vertically to show cursor at row 25"
4057        );
4058        assert!(
4059            rhs_pos.x > 0.,
4060            "RHS should have scrolled horizontally to show cursor at column 150"
4061        );
4062        assert_eq!(
4063            lhs_pos.y, rhs_pos.y,
4064            "LHS should have same vertical scroll position as RHS after autoscroll"
4065        );
4066        assert_eq!(
4067            lhs_pos.x, rhs_pos.x,
4068            "LHS should have same horizontal scroll position as RHS after autoscroll"
4069        )
4070    }
4071
4072    #[gpui::test]
4073    async fn test_edit_line_before_soft_wrapped_line_preceding_hunk(cx: &mut gpui::TestAppContext) {
4074        use rope::Point;
4075        use unindent::Unindent as _;
4076
4077        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
4078
4079        let base_text = "
4080            first line
4081            aaaa bbbb cccc dddd eeee ffff
4082            original
4083        "
4084        .unindent();
4085
4086        let current_text = "
4087            first line
4088            aaaa bbbb cccc dddd eeee ffff
4089            modified
4090        "
4091        .unindent();
4092
4093        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4094
4095        editor.update(cx, |editor, cx| {
4096            let path = PathKey::for_buffer(&buffer, cx);
4097            editor.set_excerpts_for_path(
4098                path,
4099                buffer.clone(),
4100                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4101                0,
4102                diff.clone(),
4103                cx,
4104            );
4105        });
4106
4107        cx.run_until_parked();
4108
4109        assert_split_content_with_widths(
4110            &editor,
4111            px(400.0),
4112            px(200.0),
4113            "
4114                    § <no file>
4115                    § -----
4116                    first line
4117                    aaaa bbbb cccc dddd eeee ffff
4118                    § spacer
4119                    § spacer
4120                    modified"
4121                .unindent(),
4122            "
4123                    § <no file>
4124                    § -----
4125                    first line
4126                    aaaa bbbb\x20
4127                    cccc dddd\x20
4128                    eeee ffff
4129                    original"
4130                .unindent(),
4131            &mut cx,
4132        );
4133
4134        buffer.update(cx, |buffer, cx| {
4135            buffer.edit(
4136                [(Point::new(0, 0)..Point::new(0, 10), "edited first")],
4137                None,
4138                cx,
4139            );
4140        });
4141
4142        cx.run_until_parked();
4143
4144        assert_split_content_with_widths(
4145            &editor,
4146            px(400.0),
4147            px(200.0),
4148            "
4149                    § <no file>
4150                    § -----
4151                    edited first
4152                    aaaa bbbb cccc dddd eeee ffff
4153                    § spacer
4154                    § spacer
4155                    modified"
4156                .unindent(),
4157            "
4158                    § <no file>
4159                    § -----
4160                    first line
4161                    aaaa bbbb\x20
4162                    cccc dddd\x20
4163                    eeee ffff
4164                    original"
4165                .unindent(),
4166            &mut cx,
4167        );
4168
4169        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4170        diff.update(cx, |diff, cx| {
4171            diff.recalculate_diff_sync(&buffer_snapshot, cx);
4172        });
4173
4174        cx.run_until_parked();
4175
4176        assert_split_content_with_widths(
4177            &editor,
4178            px(400.0),
4179            px(200.0),
4180            "
4181                    § <no file>
4182                    § -----
4183                    edited first
4184                    aaaa bbbb cccc dddd eeee ffff
4185                    § spacer
4186                    § spacer
4187                    modified"
4188                .unindent(),
4189            "
4190                    § <no file>
4191                    § -----
4192                    first line
4193                    aaaa bbbb\x20
4194                    cccc dddd\x20
4195                    eeee ffff
4196                    original"
4197                .unindent(),
4198            &mut cx,
4199        );
4200    }
4201
4202    #[gpui::test]
4203    async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
4204        use rope::Point;
4205        use unindent::Unindent as _;
4206
4207        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4208
4209        let base_text = "
4210            bbb
4211            ccc
4212        "
4213        .unindent();
4214        let current_text = "
4215            aaa
4216            bbb
4217            ccc
4218        "
4219        .unindent();
4220
4221        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4222
4223        editor.update(cx, |editor, cx| {
4224            let path = PathKey::for_buffer(&buffer, cx);
4225            editor.set_excerpts_for_path(
4226                path,
4227                buffer.clone(),
4228                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4229                0,
4230                diff.clone(),
4231                cx,
4232            );
4233        });
4234
4235        cx.run_until_parked();
4236
4237        assert_split_content(
4238            &editor,
4239            "
4240            § <no file>
4241            § -----
4242            aaa
4243            bbb
4244            ccc"
4245            .unindent(),
4246            "
4247            § <no file>
4248            § -----
4249            § spacer
4250            bbb
4251            ccc"
4252            .unindent(),
4253            &mut cx,
4254        );
4255
4256        let block_ids = editor.update(cx, |splittable_editor, cx| {
4257            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4258                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4259                let anchor = snapshot.anchor_before(Point::new(2, 0));
4260                rhs_editor.insert_blocks(
4261                    [BlockProperties {
4262                        placement: BlockPlacement::Above(anchor),
4263                        height: Some(1),
4264                        style: BlockStyle::Fixed,
4265                        render: Arc::new(|_| div().into_any()),
4266                        priority: 0,
4267                    }],
4268                    None,
4269                    cx,
4270                )
4271            })
4272        });
4273
4274        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4275        let lhs_editor =
4276            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4277
4278        cx.update(|_, cx| {
4279            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4280                "custom block".to_string()
4281            });
4282        });
4283
4284        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
4285            let display_map = lhs_editor.display_map.read(cx);
4286            let companion = display_map.companion().unwrap().read(cx);
4287            let mapping = companion
4288                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4289            *mapping.borrow().get(&block_ids[0]).unwrap()
4290        });
4291
4292        cx.update(|_, cx| {
4293            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
4294                "custom block".to_string()
4295            });
4296        });
4297
4298        cx.run_until_parked();
4299
4300        assert_split_content(
4301            &editor,
4302            "
4303            § <no file>
4304            § -----
4305            aaa
4306            bbb
4307            § custom block
4308            ccc"
4309            .unindent(),
4310            "
4311            § <no file>
4312            § -----
4313            § spacer
4314            bbb
4315            § custom block
4316            ccc"
4317            .unindent(),
4318            &mut cx,
4319        );
4320
4321        editor.update(cx, |splittable_editor, cx| {
4322            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4323                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
4324            });
4325        });
4326
4327        cx.run_until_parked();
4328
4329        assert_split_content(
4330            &editor,
4331            "
4332            § <no file>
4333            § -----
4334            aaa
4335            bbb
4336            ccc"
4337            .unindent(),
4338            "
4339            § <no file>
4340            § -----
4341            § spacer
4342            bbb
4343            ccc"
4344            .unindent(),
4345            &mut cx,
4346        );
4347    }
4348
4349    #[gpui::test]
4350    async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
4351        use rope::Point;
4352        use unindent::Unindent as _;
4353
4354        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4355
4356        let base_text = "
4357            bbb
4358            ccc
4359        "
4360        .unindent();
4361        let current_text = "
4362            aaa
4363            bbb
4364            ccc
4365        "
4366        .unindent();
4367
4368        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4369
4370        editor.update(cx, |editor, cx| {
4371            let path = PathKey::for_buffer(&buffer, cx);
4372            editor.set_excerpts_for_path(
4373                path,
4374                buffer.clone(),
4375                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4376                0,
4377                diff.clone(),
4378                cx,
4379            );
4380        });
4381
4382        cx.run_until_parked();
4383
4384        assert_split_content(
4385            &editor,
4386            "
4387            § <no file>
4388            § -----
4389            aaa
4390            bbb
4391            ccc"
4392            .unindent(),
4393            "
4394            § <no file>
4395            § -----
4396            § spacer
4397            bbb
4398            ccc"
4399            .unindent(),
4400            &mut cx,
4401        );
4402
4403        let block_ids = editor.update(cx, |splittable_editor, cx| {
4404            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4405                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4406                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4407                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4408                rhs_editor.insert_blocks(
4409                    [
4410                        BlockProperties {
4411                            placement: BlockPlacement::Above(anchor1),
4412                            height: Some(1),
4413                            style: BlockStyle::Fixed,
4414                            render: Arc::new(|_| div().into_any()),
4415                            priority: 0,
4416                        },
4417                        BlockProperties {
4418                            placement: BlockPlacement::Above(anchor2),
4419                            height: Some(1),
4420                            style: BlockStyle::Fixed,
4421                            render: Arc::new(|_| div().into_any()),
4422                            priority: 0,
4423                        },
4424                    ],
4425                    None,
4426                    cx,
4427                )
4428            })
4429        });
4430
4431        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4432        let lhs_editor =
4433            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4434
4435        cx.update(|_, cx| {
4436            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4437                "custom block 1".to_string()
4438            });
4439            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4440                "custom block 2".to_string()
4441            });
4442        });
4443
4444        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4445            let display_map = lhs_editor.display_map.read(cx);
4446            let companion = display_map.companion().unwrap().read(cx);
4447            let mapping = companion
4448                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4449            (
4450                *mapping.borrow().get(&block_ids[0]).unwrap(),
4451                *mapping.borrow().get(&block_ids[1]).unwrap(),
4452            )
4453        });
4454
4455        cx.update(|_, cx| {
4456            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4457                "custom block 1".to_string()
4458            });
4459            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4460                "custom block 2".to_string()
4461            });
4462        });
4463
4464        cx.run_until_parked();
4465
4466        assert_split_content(
4467            &editor,
4468            "
4469            § <no file>
4470            § -----
4471            aaa
4472            bbb
4473            § custom block 1
4474            ccc
4475            § custom block 2"
4476                .unindent(),
4477            "
4478            § <no file>
4479            § -----
4480            § spacer
4481            bbb
4482            § custom block 1
4483            ccc
4484            § custom block 2"
4485                .unindent(),
4486            &mut cx,
4487        );
4488
4489        editor.update(cx, |splittable_editor, cx| {
4490            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4491                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4492            });
4493        });
4494
4495        cx.run_until_parked();
4496
4497        assert_split_content(
4498            &editor,
4499            "
4500            § <no file>
4501            § -----
4502            aaa
4503            bbb
4504            ccc
4505            § custom block 2"
4506                .unindent(),
4507            "
4508            § <no file>
4509            § -----
4510            § spacer
4511            bbb
4512            ccc
4513            § custom block 2"
4514                .unindent(),
4515            &mut cx,
4516        );
4517
4518        editor.update_in(cx, |splittable_editor, window, cx| {
4519            splittable_editor.unsplit(window, cx);
4520        });
4521
4522        cx.run_until_parked();
4523
4524        editor.update_in(cx, |splittable_editor, window, cx| {
4525            splittable_editor.split(window, cx);
4526        });
4527
4528        cx.run_until_parked();
4529
4530        let lhs_editor =
4531            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4532
4533        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4534            let display_map = lhs_editor.display_map.read(cx);
4535            let companion = display_map.companion().unwrap().read(cx);
4536            let mapping = companion
4537                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4538            *mapping.borrow().get(&block_ids[1]).unwrap()
4539        });
4540
4541        cx.update(|_, cx| {
4542            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4543                "custom block 2".to_string()
4544            });
4545        });
4546
4547        cx.run_until_parked();
4548
4549        assert_split_content(
4550            &editor,
4551            "
4552            § <no file>
4553            § -----
4554            aaa
4555            bbb
4556            ccc
4557            § custom block 2"
4558                .unindent(),
4559            "
4560            § <no file>
4561            § -----
4562            § spacer
4563            bbb
4564            ccc
4565            § custom block 2"
4566                .unindent(),
4567            &mut cx,
4568        );
4569    }
4570
4571    #[gpui::test]
4572    async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
4573        use rope::Point;
4574        use unindent::Unindent as _;
4575
4576        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4577
4578        let base_text = "
4579            bbb
4580            ccc
4581        "
4582        .unindent();
4583        let current_text = "
4584            aaa
4585            bbb
4586            ccc
4587        "
4588        .unindent();
4589
4590        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4591
4592        editor.update(cx, |editor, cx| {
4593            let path = PathKey::for_buffer(&buffer, cx);
4594            editor.set_excerpts_for_path(
4595                path,
4596                buffer.clone(),
4597                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4598                0,
4599                diff.clone(),
4600                cx,
4601            );
4602        });
4603
4604        cx.run_until_parked();
4605
4606        editor.update_in(cx, |splittable_editor, window, cx| {
4607            splittable_editor.unsplit(window, cx);
4608        });
4609
4610        cx.run_until_parked();
4611
4612        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4613
4614        let block_ids = editor.update(cx, |splittable_editor, cx| {
4615            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4616                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4617                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4618                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4619                rhs_editor.insert_blocks(
4620                    [
4621                        BlockProperties {
4622                            placement: BlockPlacement::Above(anchor1),
4623                            height: Some(1),
4624                            style: BlockStyle::Fixed,
4625                            render: Arc::new(|_| div().into_any()),
4626                            priority: 0,
4627                        },
4628                        BlockProperties {
4629                            placement: BlockPlacement::Above(anchor2),
4630                            height: Some(1),
4631                            style: BlockStyle::Fixed,
4632                            render: Arc::new(|_| div().into_any()),
4633                            priority: 0,
4634                        },
4635                    ],
4636                    None,
4637                    cx,
4638                )
4639            })
4640        });
4641
4642        cx.update(|_, cx| {
4643            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4644                "custom block 1".to_string()
4645            });
4646            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4647                "custom block 2".to_string()
4648            });
4649        });
4650
4651        cx.run_until_parked();
4652
4653        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
4654        assert_eq!(
4655            rhs_content,
4656            "
4657            § <no file>
4658            § -----
4659            aaa
4660            bbb
4661            § custom block 1
4662            ccc
4663            § custom block 2"
4664                .unindent(),
4665            "rhs content before split"
4666        );
4667
4668        editor.update_in(cx, |splittable_editor, window, cx| {
4669            splittable_editor.split(window, cx);
4670        });
4671
4672        cx.run_until_parked();
4673
4674        let lhs_editor =
4675            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4676
4677        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4678            let display_map = lhs_editor.display_map.read(cx);
4679            let companion = display_map.companion().unwrap().read(cx);
4680            let mapping = companion
4681                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4682            (
4683                *mapping.borrow().get(&block_ids[0]).unwrap(),
4684                *mapping.borrow().get(&block_ids[1]).unwrap(),
4685            )
4686        });
4687
4688        cx.update(|_, cx| {
4689            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4690                "custom block 1".to_string()
4691            });
4692            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4693                "custom block 2".to_string()
4694            });
4695        });
4696
4697        cx.run_until_parked();
4698
4699        assert_split_content(
4700            &editor,
4701            "
4702            § <no file>
4703            § -----
4704            aaa
4705            bbb
4706            § custom block 1
4707            ccc
4708            § custom block 2"
4709                .unindent(),
4710            "
4711            § <no file>
4712            § -----
4713            § spacer
4714            bbb
4715            § custom block 1
4716            ccc
4717            § custom block 2"
4718                .unindent(),
4719            &mut cx,
4720        );
4721
4722        editor.update(cx, |splittable_editor, cx| {
4723            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4724                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4725            });
4726        });
4727
4728        cx.run_until_parked();
4729
4730        assert_split_content(
4731            &editor,
4732            "
4733            § <no file>
4734            § -----
4735            aaa
4736            bbb
4737            ccc
4738            § custom block 2"
4739                .unindent(),
4740            "
4741            § <no file>
4742            § -----
4743            § spacer
4744            bbb
4745            ccc
4746            § custom block 2"
4747                .unindent(),
4748            &mut cx,
4749        );
4750
4751        editor.update_in(cx, |splittable_editor, window, cx| {
4752            splittable_editor.unsplit(window, cx);
4753        });
4754
4755        cx.run_until_parked();
4756
4757        editor.update_in(cx, |splittable_editor, window, cx| {
4758            splittable_editor.split(window, cx);
4759        });
4760
4761        cx.run_until_parked();
4762
4763        let lhs_editor =
4764            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4765
4766        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4767            let display_map = lhs_editor.display_map.read(cx);
4768            let companion = display_map.companion().unwrap().read(cx);
4769            let mapping = companion
4770                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4771            *mapping.borrow().get(&block_ids[1]).unwrap()
4772        });
4773
4774        cx.update(|_, cx| {
4775            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4776                "custom block 2".to_string()
4777            });
4778        });
4779
4780        cx.run_until_parked();
4781
4782        assert_split_content(
4783            &editor,
4784            "
4785            § <no file>
4786            § -----
4787            aaa
4788            bbb
4789            ccc
4790            § custom block 2"
4791                .unindent(),
4792            "
4793            § <no file>
4794            § -----
4795            § spacer
4796            bbb
4797            ccc
4798            § custom block 2"
4799                .unindent(),
4800            &mut cx,
4801        );
4802
4803        let new_block_ids = editor.update(cx, |splittable_editor, cx| {
4804            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4805                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4806                let anchor = snapshot.anchor_before(Point::new(2, 0));
4807                rhs_editor.insert_blocks(
4808                    [BlockProperties {
4809                        placement: BlockPlacement::Above(anchor),
4810                        height: Some(1),
4811                        style: BlockStyle::Fixed,
4812                        render: Arc::new(|_| div().into_any()),
4813                        priority: 0,
4814                    }],
4815                    None,
4816                    cx,
4817                )
4818            })
4819        });
4820
4821        cx.update(|_, cx| {
4822            set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
4823                "custom block 3".to_string()
4824            });
4825        });
4826
4827        let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4828            let display_map = lhs_editor.display_map.read(cx);
4829            let companion = display_map.companion().unwrap().read(cx);
4830            let mapping = companion
4831                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4832            *mapping.borrow().get(&new_block_ids[0]).unwrap()
4833        });
4834
4835        cx.update(|_, cx| {
4836            set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
4837                "custom block 3".to_string()
4838            });
4839        });
4840
4841        cx.run_until_parked();
4842
4843        assert_split_content(
4844            &editor,
4845            "
4846            § <no file>
4847            § -----
4848            aaa
4849            bbb
4850            § custom block 3
4851            ccc
4852            § custom block 2"
4853                .unindent(),
4854            "
4855            § <no file>
4856            § -----
4857            § spacer
4858            bbb
4859            § custom block 3
4860            ccc
4861            § custom block 2"
4862                .unindent(),
4863            &mut cx,
4864        );
4865
4866        editor.update(cx, |splittable_editor, cx| {
4867            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4868                rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
4869            });
4870        });
4871
4872        cx.run_until_parked();
4873
4874        assert_split_content(
4875            &editor,
4876            "
4877            § <no file>
4878            § -----
4879            aaa
4880            bbb
4881            ccc
4882            § custom block 2"
4883                .unindent(),
4884            "
4885            § <no file>
4886            § -----
4887            § spacer
4888            bbb
4889            ccc
4890            § custom block 2"
4891                .unindent(),
4892            &mut cx,
4893        );
4894    }
4895
4896    #[gpui::test]
4897    async fn test_buffer_folding_sync(cx: &mut gpui::TestAppContext) {
4898        use rope::Point;
4899        use unindent::Unindent as _;
4900
4901        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
4902
4903        let base_text1 = "
4904            aaa
4905            bbb
4906            ccc"
4907        .unindent();
4908        let current_text1 = "
4909            aaa
4910            bbb
4911            ccc"
4912        .unindent();
4913
4914        let base_text2 = "
4915            ddd
4916            eee
4917            fff"
4918        .unindent();
4919        let current_text2 = "
4920            ddd
4921            eee
4922            fff"
4923        .unindent();
4924
4925        let (buffer1, diff1) = buffer_with_diff(&base_text1, &current_text1, &mut cx);
4926        let (buffer2, diff2) = buffer_with_diff(&base_text2, &current_text2, &mut cx);
4927
4928        let buffer1_id = buffer1.read_with(cx, |buffer, _| buffer.remote_id());
4929        let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
4930
4931        editor.update(cx, |editor, cx| {
4932            let path1 = PathKey::for_buffer(&buffer1, cx);
4933            editor.set_excerpts_for_path(
4934                path1,
4935                buffer1.clone(),
4936                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
4937                0,
4938                diff1.clone(),
4939                cx,
4940            );
4941            let path2 = PathKey::for_buffer(&buffer2, cx);
4942            editor.set_excerpts_for_path(
4943                path2,
4944                buffer2.clone(),
4945                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
4946                1,
4947                diff2.clone(),
4948                cx,
4949            );
4950        });
4951
4952        cx.run_until_parked();
4953
4954        editor.update(cx, |editor, cx| {
4955            editor.rhs_editor.update(cx, |rhs_editor, cx| {
4956                rhs_editor.fold_buffer(buffer1_id, cx);
4957            });
4958        });
4959
4960        cx.run_until_parked();
4961
4962        let rhs_buffer1_folded = editor.read_with(cx, |editor, cx| {
4963            editor.rhs_editor.read(cx).is_buffer_folded(buffer1_id, cx)
4964        });
4965        assert!(
4966            rhs_buffer1_folded,
4967            "buffer1 should be folded in rhs before split"
4968        );
4969
4970        editor.update_in(cx, |editor, window, cx| {
4971            editor.split(window, cx);
4972        });
4973
4974        cx.run_until_parked();
4975
4976        let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
4977            (
4978                editor.rhs_editor.clone(),
4979                editor.lhs.as_ref().unwrap().editor.clone(),
4980            )
4981        });
4982
4983        let rhs_buffer1_folded =
4984            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
4985        assert!(
4986            rhs_buffer1_folded,
4987            "buffer1 should be folded in rhs after split"
4988        );
4989
4990        let base_buffer1_id = diff1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
4991        let lhs_buffer1_folded = lhs_editor.read_with(cx, |editor, cx| {
4992            editor.is_buffer_folded(base_buffer1_id, cx)
4993        });
4994        assert!(
4995            lhs_buffer1_folded,
4996            "buffer1 should be folded in lhs after split"
4997        );
4998
4999        assert_split_content(
5000            &editor,
5001            "
5002            § <no file>
5003            § -----
5004            § <no file>
5005            § -----
5006            ddd
5007            eee
5008            fff"
5009            .unindent(),
5010            "
5011            § <no file>
5012            § -----
5013            § <no file>
5014            § -----
5015            ddd
5016            eee
5017            fff"
5018            .unindent(),
5019            &mut cx,
5020        );
5021
5022        editor.update(cx, |editor, cx| {
5023            editor.rhs_editor.update(cx, |rhs_editor, cx| {
5024                rhs_editor.fold_buffer(buffer2_id, cx);
5025            });
5026        });
5027
5028        cx.run_until_parked();
5029
5030        let rhs_buffer2_folded =
5031            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer2_id, cx));
5032        assert!(rhs_buffer2_folded, "buffer2 should be folded in rhs");
5033
5034        let base_buffer2_id = diff2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
5035        let lhs_buffer2_folded = lhs_editor.read_with(cx, |editor, cx| {
5036            editor.is_buffer_folded(base_buffer2_id, cx)
5037        });
5038        assert!(lhs_buffer2_folded, "buffer2 should be folded in lhs");
5039
5040        let rhs_buffer1_still_folded =
5041            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
5042        assert!(
5043            rhs_buffer1_still_folded,
5044            "buffer1 should still be folded in rhs"
5045        );
5046
5047        let lhs_buffer1_still_folded = lhs_editor.read_with(cx, |editor, cx| {
5048            editor.is_buffer_folded(base_buffer1_id, cx)
5049        });
5050        assert!(
5051            lhs_buffer1_still_folded,
5052            "buffer1 should still be folded in lhs"
5053        );
5054
5055        assert_split_content(
5056            &editor,
5057            "
5058            § <no file>
5059            § -----
5060            § <no file>
5061            § -----"
5062                .unindent(),
5063            "
5064            § <no file>
5065            § -----
5066            § <no file>
5067            § -----"
5068                .unindent(),
5069            &mut cx,
5070        );
5071    }
5072
5073    #[gpui::test]
5074    async fn test_custom_block_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5075        use rope::Point;
5076        use unindent::Unindent as _;
5077
5078        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5079
5080        let base_text = "
5081            ddd
5082            eee
5083        "
5084        .unindent();
5085        let current_text = "
5086            aaa
5087            bbb
5088            ccc
5089            ddd
5090            eee
5091        "
5092        .unindent();
5093
5094        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5095
5096        editor.update(cx, |editor, cx| {
5097            let path = PathKey::for_buffer(&buffer, cx);
5098            editor.set_excerpts_for_path(
5099                path,
5100                buffer.clone(),
5101                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5102                0,
5103                diff.clone(),
5104                cx,
5105            );
5106        });
5107
5108        cx.run_until_parked();
5109
5110        assert_split_content(
5111            &editor,
5112            "
5113            § <no file>
5114            § -----
5115            aaa
5116            bbb
5117            ccc
5118            ddd
5119            eee"
5120            .unindent(),
5121            "
5122            § <no file>
5123            § -----
5124            § spacer
5125            § spacer
5126            § spacer
5127            ddd
5128            eee"
5129            .unindent(),
5130            &mut cx,
5131        );
5132
5133        let block_ids = editor.update(cx, |splittable_editor, cx| {
5134            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5135                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5136                let anchor = snapshot.anchor_before(Point::new(2, 0));
5137                rhs_editor.insert_blocks(
5138                    [BlockProperties {
5139                        placement: BlockPlacement::Above(anchor),
5140                        height: Some(1),
5141                        style: BlockStyle::Fixed,
5142                        render: Arc::new(|_| div().into_any()),
5143                        priority: 0,
5144                    }],
5145                    None,
5146                    cx,
5147                )
5148            })
5149        });
5150
5151        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5152        let lhs_editor =
5153            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5154
5155        cx.update(|_, cx| {
5156            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5157                "custom block".to_string()
5158            });
5159        });
5160
5161        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5162            let display_map = lhs_editor.display_map.read(cx);
5163            let companion = display_map.companion().unwrap().read(cx);
5164            let mapping = companion
5165                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5166            *mapping.borrow().get(&block_ids[0]).unwrap()
5167        });
5168
5169        cx.update(|_, cx| {
5170            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5171                "custom block".to_string()
5172            });
5173        });
5174
5175        cx.run_until_parked();
5176
5177        assert_split_content(
5178            &editor,
5179            "
5180            § <no file>
5181            § -----
5182            aaa
5183            bbb
5184            § custom block
5185            ccc
5186            ddd
5187            eee"
5188            .unindent(),
5189            "
5190            § <no file>
5191            § -----
5192            § spacer
5193            § spacer
5194            § spacer
5195            § custom block
5196            ddd
5197            eee"
5198            .unindent(),
5199            &mut cx,
5200        );
5201
5202        editor.update(cx, |splittable_editor, cx| {
5203            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5204                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5205            });
5206        });
5207
5208        cx.run_until_parked();
5209
5210        assert_split_content(
5211            &editor,
5212            "
5213            § <no file>
5214            § -----
5215            aaa
5216            bbb
5217            ccc
5218            ddd
5219            eee"
5220            .unindent(),
5221            "
5222            § <no file>
5223            § -----
5224            § spacer
5225            § spacer
5226            § spacer
5227            ddd
5228            eee"
5229            .unindent(),
5230            &mut cx,
5231        );
5232    }
5233
5234    #[gpui::test]
5235    async fn test_custom_block_below_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5236        use rope::Point;
5237        use unindent::Unindent as _;
5238
5239        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5240
5241        let base_text = "
5242            ddd
5243            eee
5244        "
5245        .unindent();
5246        let current_text = "
5247            aaa
5248            bbb
5249            ccc
5250            ddd
5251            eee
5252        "
5253        .unindent();
5254
5255        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5256
5257        editor.update(cx, |editor, cx| {
5258            let path = PathKey::for_buffer(&buffer, cx);
5259            editor.set_excerpts_for_path(
5260                path,
5261                buffer.clone(),
5262                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5263                0,
5264                diff.clone(),
5265                cx,
5266            );
5267        });
5268
5269        cx.run_until_parked();
5270
5271        assert_split_content(
5272            &editor,
5273            "
5274            § <no file>
5275            § -----
5276            aaa
5277            bbb
5278            ccc
5279            ddd
5280            eee"
5281            .unindent(),
5282            "
5283            § <no file>
5284            § -----
5285            § spacer
5286            § spacer
5287            § spacer
5288            ddd
5289            eee"
5290            .unindent(),
5291            &mut cx,
5292        );
5293
5294        let block_ids = editor.update(cx, |splittable_editor, cx| {
5295            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5296                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5297                let anchor = snapshot.anchor_after(Point::new(1, 3));
5298                rhs_editor.insert_blocks(
5299                    [BlockProperties {
5300                        placement: BlockPlacement::Below(anchor),
5301                        height: Some(1),
5302                        style: BlockStyle::Fixed,
5303                        render: Arc::new(|_| div().into_any()),
5304                        priority: 0,
5305                    }],
5306                    None,
5307                    cx,
5308                )
5309            })
5310        });
5311
5312        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5313        let lhs_editor =
5314            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5315
5316        cx.update(|_, cx| {
5317            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5318                "custom block".to_string()
5319            });
5320        });
5321
5322        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5323            let display_map = lhs_editor.display_map.read(cx);
5324            let companion = display_map.companion().unwrap().read(cx);
5325            let mapping = companion
5326                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5327            *mapping.borrow().get(&block_ids[0]).unwrap()
5328        });
5329
5330        cx.update(|_, cx| {
5331            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5332                "custom block".to_string()
5333            });
5334        });
5335
5336        cx.run_until_parked();
5337
5338        assert_split_content(
5339            &editor,
5340            "
5341            § <no file>
5342            § -----
5343            aaa
5344            bbb
5345            § custom block
5346            ccc
5347            ddd
5348            eee"
5349            .unindent(),
5350            "
5351            § <no file>
5352            § -----
5353            § spacer
5354            § spacer
5355            § spacer
5356            § custom block
5357            ddd
5358            eee"
5359            .unindent(),
5360            &mut cx,
5361        );
5362
5363        editor.update(cx, |splittable_editor, cx| {
5364            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5365                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5366            });
5367        });
5368
5369        cx.run_until_parked();
5370
5371        assert_split_content(
5372            &editor,
5373            "
5374            § <no file>
5375            § -----
5376            aaa
5377            bbb
5378            ccc
5379            ddd
5380            eee"
5381            .unindent(),
5382            "
5383            § <no file>
5384            § -----
5385            § spacer
5386            § spacer
5387            § spacer
5388            ddd
5389            eee"
5390            .unindent(),
5391            &mut cx,
5392        );
5393    }
5394
5395    #[gpui::test]
5396    async fn test_custom_block_resize_syncs_balancing_block(cx: &mut gpui::TestAppContext) {
5397        use rope::Point;
5398        use unindent::Unindent as _;
5399
5400        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5401
5402        let base_text = "
5403            bbb
5404            ccc
5405        "
5406        .unindent();
5407        let current_text = "
5408            aaa
5409            bbb
5410            ccc
5411        "
5412        .unindent();
5413
5414        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5415
5416        editor.update(cx, |editor, cx| {
5417            let path = PathKey::for_buffer(&buffer, cx);
5418            editor.set_excerpts_for_path(
5419                path,
5420                buffer.clone(),
5421                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5422                0,
5423                diff.clone(),
5424                cx,
5425            );
5426        });
5427
5428        cx.run_until_parked();
5429
5430        let block_ids = editor.update(cx, |splittable_editor, cx| {
5431            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5432                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5433                let anchor = snapshot.anchor_before(Point::new(2, 0));
5434                rhs_editor.insert_blocks(
5435                    [BlockProperties {
5436                        placement: BlockPlacement::Above(anchor),
5437                        height: Some(1),
5438                        style: BlockStyle::Fixed,
5439                        render: Arc::new(|_| div().into_any()),
5440                        priority: 0,
5441                    }],
5442                    None,
5443                    cx,
5444                )
5445            })
5446        });
5447
5448        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5449        let lhs_editor =
5450            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5451
5452        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5453            let display_map = lhs_editor.display_map.read(cx);
5454            let companion = display_map.companion().unwrap().read(cx);
5455            let mapping = companion
5456                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5457            *mapping.borrow().get(&block_ids[0]).unwrap()
5458        });
5459
5460        cx.run_until_parked();
5461
5462        let get_block_height = |editor: &Entity<crate::Editor>,
5463                                block_id: crate::CustomBlockId,
5464                                cx: &mut VisualTestContext| {
5465            editor.update_in(cx, |editor, window, cx| {
5466                let snapshot = editor.snapshot(window, cx);
5467                snapshot
5468                    .block_for_id(crate::BlockId::Custom(block_id))
5469                    .map(|block| block.height())
5470            })
5471        };
5472
5473        assert_eq!(
5474            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5475            Some(1)
5476        );
5477        assert_eq!(
5478            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5479            Some(1)
5480        );
5481
5482        editor.update(cx, |splittable_editor, cx| {
5483            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5484                let mut heights = HashMap::default();
5485                heights.insert(block_ids[0], 3);
5486                rhs_editor.resize_blocks(heights, None, cx);
5487            });
5488        });
5489
5490        cx.run_until_parked();
5491
5492        assert_eq!(
5493            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5494            Some(3)
5495        );
5496        assert_eq!(
5497            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5498            Some(3)
5499        );
5500
5501        editor.update(cx, |splittable_editor, cx| {
5502            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5503                let mut heights = HashMap::default();
5504                heights.insert(block_ids[0], 5);
5505                rhs_editor.resize_blocks(heights, None, cx);
5506            });
5507        });
5508
5509        cx.run_until_parked();
5510
5511        assert_eq!(
5512            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5513            Some(5)
5514        );
5515        assert_eq!(
5516            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5517            Some(5)
5518        );
5519    }
5520
5521    #[gpui::test]
5522    async fn test_edit_spanning_excerpt_boundaries_then_resplit(cx: &mut gpui::TestAppContext) {
5523        use rope::Point;
5524        use unindent::Unindent as _;
5525
5526        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5527
5528        let base_text = "
5529            aaa
5530            bbb
5531            ccc
5532            ddd
5533            eee
5534            fff
5535            ggg
5536            hhh
5537            iii
5538            jjj
5539            kkk
5540            lll
5541        "
5542        .unindent();
5543        let current_text = base_text.clone();
5544
5545        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5546
5547        editor.update(cx, |editor, cx| {
5548            let path = PathKey::for_buffer(&buffer, cx);
5549            editor.set_excerpts_for_path(
5550                path,
5551                buffer.clone(),
5552                vec![
5553                    Point::new(0, 0)..Point::new(3, 3),
5554                    Point::new(5, 0)..Point::new(8, 3),
5555                    Point::new(10, 0)..Point::new(11, 3),
5556                ],
5557                0,
5558                diff.clone(),
5559                cx,
5560            );
5561        });
5562
5563        cx.run_until_parked();
5564
5565        buffer.update(cx, |buffer, cx| {
5566            buffer.edit([(Point::new(1, 0)..Point::new(10, 0), "")], None, cx);
5567        });
5568
5569        cx.run_until_parked();
5570
5571        editor.update_in(cx, |splittable_editor, window, cx| {
5572            splittable_editor.unsplit(window, cx);
5573        });
5574
5575        cx.run_until_parked();
5576
5577        editor.update_in(cx, |splittable_editor, window, cx| {
5578            splittable_editor.split(window, cx);
5579        });
5580
5581        cx.run_until_parked();
5582    }
5583
5584    #[gpui::test]
5585    async fn test_range_folds_removed_on_split(cx: &mut gpui::TestAppContext) {
5586        use rope::Point;
5587        use unindent::Unindent as _;
5588
5589        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5590
5591        let base_text = "
5592            aaa
5593            bbb
5594            ccc
5595            ddd
5596            eee"
5597        .unindent();
5598        let current_text = "
5599            aaa
5600            bbb
5601            ccc
5602            ddd
5603            eee"
5604        .unindent();
5605
5606        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5607
5608        editor.update(cx, |editor, cx| {
5609            let path = PathKey::for_buffer(&buffer, cx);
5610            editor.set_excerpts_for_path(
5611                path,
5612                buffer.clone(),
5613                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5614                0,
5615                diff.clone(),
5616                cx,
5617            );
5618        });
5619
5620        cx.run_until_parked();
5621
5622        editor.update_in(cx, |editor, window, cx| {
5623            editor.rhs_editor.update(cx, |rhs_editor, cx| {
5624                rhs_editor.fold_creases(
5625                    vec![Crease::simple(
5626                        Point::new(1, 0)..Point::new(3, 0),
5627                        FoldPlaceholder::test(),
5628                    )],
5629                    false,
5630                    window,
5631                    cx,
5632                );
5633            });
5634        });
5635
5636        cx.run_until_parked();
5637
5638        editor.update_in(cx, |editor, window, cx| {
5639            editor.split(window, cx);
5640        });
5641
5642        cx.run_until_parked();
5643
5644        let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
5645            (
5646                editor.rhs_editor.clone(),
5647                editor.lhs.as_ref().unwrap().editor.clone(),
5648            )
5649        });
5650
5651        let rhs_has_folds_after_split = rhs_editor.update(cx, |editor, cx| {
5652            let snapshot = editor.display_snapshot(cx);
5653            snapshot
5654                .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5655                .next()
5656                .is_some()
5657        });
5658        assert!(
5659            !rhs_has_folds_after_split,
5660            "rhs should not have range folds after split"
5661        );
5662
5663        let lhs_has_folds = lhs_editor.update(cx, |editor, cx| {
5664            let snapshot = editor.display_snapshot(cx);
5665            snapshot
5666                .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5667                .next()
5668                .is_some()
5669        });
5670        assert!(!lhs_has_folds, "lhs should not have any range folds");
5671    }
5672
5673    #[gpui::test]
5674    async fn test_multiline_inlays_create_spacers(cx: &mut gpui::TestAppContext) {
5675        use rope::Point;
5676        use unindent::Unindent as _;
5677
5678        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5679
5680        let base_text = "
5681            aaa
5682            bbb
5683            ccc
5684            ddd
5685        "
5686        .unindent();
5687        let current_text = base_text.clone();
5688
5689        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5690
5691        editor.update(cx, |editor, cx| {
5692            let path = PathKey::for_buffer(&buffer, cx);
5693            editor.set_excerpts_for_path(
5694                path,
5695                buffer.clone(),
5696                vec![Point::new(0, 0)..Point::new(3, 3)],
5697                0,
5698                diff.clone(),
5699                cx,
5700            );
5701        });
5702
5703        cx.run_until_parked();
5704
5705        let rhs_editor = editor.read_with(cx, |e, _| e.rhs_editor.clone());
5706        rhs_editor.update(cx, |rhs_editor, cx| {
5707            let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5708            rhs_editor.splice_inlays(
5709                &[],
5710                vec![
5711                    Inlay::edit_prediction(
5712                        0,
5713                        snapshot.anchor_after(Point::new(0, 3)),
5714                        "\nINLAY_WITHIN",
5715                    ),
5716                    Inlay::edit_prediction(
5717                        1,
5718                        snapshot.anchor_after(Point::new(1, 3)),
5719                        "\nINLAY_MID_1\nINLAY_MID_2",
5720                    ),
5721                    Inlay::edit_prediction(
5722                        2,
5723                        snapshot.anchor_after(Point::new(3, 3)),
5724                        "\nINLAY_END_1\nINLAY_END_2",
5725                    ),
5726                ],
5727                cx,
5728            );
5729        });
5730
5731        cx.run_until_parked();
5732
5733        assert_split_content(
5734            &editor,
5735            "
5736            § <no file>
5737            § -----
5738            aaa
5739            INLAY_WITHIN
5740            bbb
5741            INLAY_MID_1
5742            INLAY_MID_2
5743            ccc
5744            ddd
5745            INLAY_END_1
5746            INLAY_END_2"
5747                .unindent(),
5748            "
5749            § <no file>
5750            § -----
5751            aaa
5752            § spacer
5753            bbb
5754            § spacer
5755            § spacer
5756            ccc
5757            ddd
5758            § spacer
5759            § spacer"
5760                .unindent(),
5761            &mut cx,
5762        );
5763    }
5764
5765    #[gpui::test]
5766    async fn test_split_after_removing_folded_buffer(cx: &mut gpui::TestAppContext) {
5767        use rope::Point;
5768        use unindent::Unindent as _;
5769
5770        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5771
5772        let base_text_a = "
5773            aaa
5774            bbb
5775            ccc
5776        "
5777        .unindent();
5778        let current_text_a = "
5779            aaa
5780            bbb modified
5781            ccc
5782        "
5783        .unindent();
5784
5785        let base_text_b = "
5786            xxx
5787            yyy
5788            zzz
5789        "
5790        .unindent();
5791        let current_text_b = "
5792            xxx
5793            yyy modified
5794            zzz
5795        "
5796        .unindent();
5797
5798        let (buffer_a, diff_a) = buffer_with_diff(&base_text_a, &current_text_a, &mut cx);
5799        let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, &current_text_b, &mut cx);
5800
5801        let path_a = cx.read(|cx| PathKey::for_buffer(&buffer_a, cx));
5802        let path_b = cx.read(|cx| PathKey::for_buffer(&buffer_b, cx));
5803
5804        editor.update(cx, |editor, cx| {
5805            editor.set_excerpts_for_path(
5806                path_a.clone(),
5807                buffer_a.clone(),
5808                vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
5809                0,
5810                diff_a.clone(),
5811                cx,
5812            );
5813            editor.set_excerpts_for_path(
5814                path_b.clone(),
5815                buffer_b.clone(),
5816                vec![Point::new(0, 0)..buffer_b.read(cx).max_point()],
5817                0,
5818                diff_b.clone(),
5819                cx,
5820            );
5821        });
5822
5823        cx.run_until_parked();
5824
5825        let buffer_a_id = buffer_a.read_with(cx, |buffer, _| buffer.remote_id());
5826        editor.update(cx, |editor, cx| {
5827            editor.rhs_editor().update(cx, |right_editor, cx| {
5828                right_editor.fold_buffer(buffer_a_id, cx)
5829            });
5830        });
5831
5832        cx.run_until_parked();
5833
5834        editor.update(cx, |editor, cx| {
5835            editor.remove_excerpts_for_path(path_a.clone(), cx);
5836        });
5837        cx.run_until_parked();
5838
5839        editor.update_in(cx, |editor, window, cx| editor.split(window, cx));
5840        cx.run_until_parked();
5841
5842        editor.update(cx, |editor, cx| {
5843            editor.set_excerpts_for_path(
5844                path_a.clone(),
5845                buffer_a.clone(),
5846                vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
5847                0,
5848                diff_a.clone(),
5849                cx,
5850            );
5851            assert!(
5852                !editor
5853                    .lhs_editor()
5854                    .unwrap()
5855                    .read(cx)
5856                    .is_buffer_folded(buffer_a_id, cx)
5857            );
5858            assert!(
5859                !editor
5860                    .rhs_editor()
5861                    .read(cx)
5862                    .is_buffer_folded(buffer_a_id, cx)
5863            );
5864        });
5865    }
5866
5867    #[gpui::test]
5868    async fn test_two_path_keys_for_one_buffer(cx: &mut gpui::TestAppContext) {
5869        use multi_buffer::PathKey;
5870        use rope::Point;
5871        use unindent::Unindent as _;
5872
5873        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5874
5875        let base_text = "
5876            aaa
5877            bbb
5878            ccc
5879        "
5880        .unindent();
5881        let current_text = "
5882            aaa
5883            bbb modified
5884            ccc
5885        "
5886        .unindent();
5887
5888        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5889
5890        let path_key_1 = PathKey {
5891            sort_prefix: Some(0),
5892            path: rel_path("file1.txt").into(),
5893        };
5894        let path_key_2 = PathKey {
5895            sort_prefix: Some(1),
5896            path: rel_path("file1.txt").into(),
5897        };
5898
5899        editor.update(cx, |editor, cx| {
5900            editor.set_excerpts_for_path(
5901                path_key_1.clone(),
5902                buffer.clone(),
5903                vec![Point::new(0, 0)..Point::new(1, 0)],
5904                0,
5905                diff.clone(),
5906                cx,
5907            );
5908            editor.set_excerpts_for_path(
5909                path_key_2.clone(),
5910                buffer.clone(),
5911                vec![Point::new(1, 0)..buffer.read(cx).max_point()],
5912                1,
5913                diff.clone(),
5914                cx,
5915            );
5916        });
5917
5918        cx.run_until_parked();
5919    }
5920}