split.rs

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