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