split.rs

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