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