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