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