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