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