split.rs

   1use std::{
   2    ops::{Bound, Range, RangeInclusive},
   3    sync::Arc,
   4};
   5
   6use buffer_diff::{BufferDiff, BufferDiffSnapshot};
   7use collections::HashMap;
   8use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
   9use gpui::{Action, AppContext as _, Entity, EventEmitter, Focusable, Subscription, WeakEntity};
  10use itertools::Itertools;
  11use language::{Buffer, Capability};
  12use multi_buffer::{
  13    Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferDiffHunk,
  14    MultiBufferPoint, MultiBufferSnapshot, PathKey,
  15};
  16use project::Project;
  17use rope::Point;
  18use text::{OffsetRangeExt as _, Patch, ToPoint as _};
  19use ui::{
  20    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
  21    Styled as _, Window, div,
  22};
  23
  24use crate::{
  25    display_map::CompanionExcerptPatch,
  26    split_editor_view::{SplitEditorState, SplitEditorView},
  27};
  28use workspace::{
  29    ActivatePaneLeft, ActivatePaneRight, Item, ToolbarItemLocation, Workspace,
  30    item::{BreadcrumbText, ItemBufferKind, ItemEvent, SaveOptions, TabContentParams},
  31    searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
  32};
  33
  34use crate::{
  35    Autoscroll, DisplayMap, Editor, EditorEvent, RenderDiffHunkControlsFn, ToggleCodeActions,
  36    ToggleSoftWrap,
  37    actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
  38    display_map::Companion,
  39};
  40use zed_actions::assistant::InlineAssist;
  41
  42pub(crate) fn convert_lhs_rows_to_rhs(
  43    lhs_excerpt_to_rhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
  44    rhs_snapshot: &MultiBufferSnapshot,
  45    lhs_snapshot: &MultiBufferSnapshot,
  46    lhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
  47) -> Vec<CompanionExcerptPatch> {
  48    patches_for_range(
  49        lhs_excerpt_to_rhs_excerpt,
  50        lhs_snapshot,
  51        rhs_snapshot,
  52        lhs_bounds,
  53        |diff, range, buffer| diff.patch_for_base_text_range(range, buffer),
  54    )
  55}
  56
  57pub(crate) fn convert_rhs_rows_to_lhs(
  58    rhs_excerpt_to_lhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
  59    lhs_snapshot: &MultiBufferSnapshot,
  60    rhs_snapshot: &MultiBufferSnapshot,
  61    rhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
  62) -> Vec<CompanionExcerptPatch> {
  63    patches_for_range(
  64        rhs_excerpt_to_lhs_excerpt,
  65        rhs_snapshot,
  66        lhs_snapshot,
  67        rhs_bounds,
  68        |diff, range, buffer| diff.patch_for_buffer_range(range, buffer),
  69    )
  70}
  71
  72fn translate_lhs_hunks_to_rhs(
  73    lhs_hunks: &[MultiBufferDiffHunk],
  74    splittable: &SplittableEditor,
  75    cx: &App,
  76) -> Vec<MultiBufferDiffHunk> {
  77    let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx);
  78    let Some(companion) = rhs_display_map.companion() else {
  79        return vec![];
  80    };
  81    let companion = companion.read(cx);
  82    let rhs_snapshot = splittable.rhs_multibuffer.read(cx).snapshot(cx);
  83    let rhs_hunks: Vec<MultiBufferDiffHunk> = rhs_snapshot.diff_hunks().collect();
  84
  85    let mut translated = Vec::new();
  86    for lhs_hunk in lhs_hunks {
  87        let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(lhs_hunk.buffer_id) else {
  88            continue;
  89        };
  90        if let Some(rhs_hunk) = rhs_hunks.iter().find(|rhs_hunk| {
  91            rhs_hunk.buffer_id == rhs_buffer_id
  92                && rhs_hunk.diff_base_byte_range == lhs_hunk.diff_base_byte_range
  93        }) {
  94            translated.push(rhs_hunk.clone());
  95        }
  96    }
  97    translated
  98}
  99
 100fn patches_for_range<F>(
 101    excerpt_map: &HashMap<ExcerptId, ExcerptId>,
 102    source_snapshot: &MultiBufferSnapshot,
 103    target_snapshot: &MultiBufferSnapshot,
 104    source_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
 105    translate_fn: F,
 106) -> Vec<CompanionExcerptPatch>
 107where
 108    F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>,
 109{
 110    let mut result = Vec::new();
 111    let mut patches = HashMap::default();
 112
 113    for (source_buffer, buffer_offset_range, source_excerpt_id) in
 114        source_snapshot.range_to_buffer_ranges(source_bounds)
 115    {
 116        let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied().unwrap();
 117        let target_buffer = target_snapshot
 118            .buffer_for_excerpt(target_excerpt_id)
 119            .unwrap();
 120        let patch = patches.entry(source_buffer.remote_id()).or_insert_with(|| {
 121            let diff = source_snapshot
 122                .diff_for_buffer_id(source_buffer.remote_id())
 123                .unwrap();
 124            let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() {
 125                &target_buffer
 126            } else {
 127                source_buffer
 128            };
 129            // TODO(split-diff) pass only the union of the ranges for the affected excerpts
 130            translate_fn(diff, Point::zero()..=source_buffer.max_point(), rhs_buffer)
 131        });
 132        let buffer_point_range = buffer_offset_range.to_point(source_buffer);
 133
 134        // TODO(split-diff) maybe narrow the patch to only the edited part of the excerpt
 135        // (less useful for project diff, but important if we want to do singleton side-by-side diff)
 136        result.push(patch_for_excerpt(
 137            source_snapshot,
 138            target_snapshot,
 139            source_excerpt_id,
 140            target_excerpt_id,
 141            source_buffer,
 142            target_buffer,
 143            patch,
 144            buffer_point_range,
 145        ));
 146    }
 147
 148    result
 149}
 150
 151fn patch_for_excerpt(
 152    source_snapshot: &MultiBufferSnapshot,
 153    target_snapshot: &MultiBufferSnapshot,
 154    source_excerpt_id: ExcerptId,
 155    target_excerpt_id: ExcerptId,
 156    source_buffer: &text::BufferSnapshot,
 157    target_buffer: &text::BufferSnapshot,
 158    patch: &Patch<Point>,
 159    source_edited_range: Range<Point>,
 160) -> CompanionExcerptPatch {
 161    let source_multibuffer_range = source_snapshot
 162        .range_for_excerpt(source_excerpt_id)
 163        .unwrap();
 164    let source_excerpt_start_in_multibuffer = source_multibuffer_range.start;
 165    let source_context_range = source_snapshot
 166        .context_range_for_excerpt(source_excerpt_id)
 167        .unwrap();
 168    let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer);
 169    let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer);
 170    let target_multibuffer_range = target_snapshot
 171        .range_for_excerpt(target_excerpt_id)
 172        .unwrap();
 173    let target_excerpt_start_in_multibuffer = target_multibuffer_range.start;
 174    let target_context_range = target_snapshot
 175        .context_range_for_excerpt(target_excerpt_id)
 176        .unwrap();
 177    let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer);
 178    let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer);
 179
 180    let edits = patch
 181        .edits()
 182        .iter()
 183        .skip_while(|edit| edit.old.end < source_excerpt_start_in_buffer)
 184        .take_while(|edit| edit.old.start <= source_excerpt_end_in_buffer)
 185        .map(|edit| {
 186            let clamped_source_start = edit
 187                .old
 188                .start
 189                .max(source_excerpt_start_in_buffer)
 190                .min(source_excerpt_end_in_buffer);
 191            let clamped_source_end = edit
 192                .old
 193                .end
 194                .max(source_excerpt_start_in_buffer)
 195                .min(source_excerpt_end_in_buffer);
 196            let source_multibuffer_start = source_excerpt_start_in_multibuffer
 197                + (clamped_source_start - source_excerpt_start_in_buffer);
 198            let source_multibuffer_end = source_excerpt_start_in_multibuffer
 199                + (clamped_source_end - source_excerpt_start_in_buffer);
 200            let clamped_target_start = edit
 201                .new
 202                .start
 203                .max(target_excerpt_start_in_buffer)
 204                .min(target_excerpt_end_in_buffer);
 205            let clamped_target_end = edit
 206                .new
 207                .end
 208                .max(target_excerpt_start_in_buffer)
 209                .min(target_excerpt_end_in_buffer);
 210            let target_multibuffer_start = target_excerpt_start_in_multibuffer
 211                + (clamped_target_start - target_excerpt_start_in_buffer);
 212            let target_multibuffer_end = target_excerpt_start_in_multibuffer
 213                + (clamped_target_end - target_excerpt_start_in_buffer);
 214            text::Edit {
 215                old: source_multibuffer_start..source_multibuffer_end,
 216                new: target_multibuffer_start..target_multibuffer_end,
 217            }
 218        });
 219
 220    let edits = [text::Edit {
 221        old: source_excerpt_start_in_multibuffer..source_excerpt_start_in_multibuffer,
 222        new: target_excerpt_start_in_multibuffer..target_excerpt_start_in_multibuffer,
 223    }]
 224    .into_iter()
 225    .chain(edits);
 226
 227    let mut merged_edits: Vec<text::Edit<Point>> = Vec::new();
 228    for edit in edits {
 229        if let Some(last) = merged_edits.last_mut() {
 230            if edit.new.start <= last.new.end {
 231                last.old.end = last.old.end.max(edit.old.end);
 232                last.new.end = last.new.end.max(edit.new.end);
 233                continue;
 234            }
 235        }
 236        merged_edits.push(edit);
 237    }
 238
 239    let edited_range = source_excerpt_start_in_multibuffer
 240        + (source_edited_range.start - source_excerpt_start_in_buffer)
 241        ..source_excerpt_start_in_multibuffer
 242            + (source_edited_range.end - source_excerpt_start_in_buffer);
 243
 244    let source_excerpt_end = source_excerpt_start_in_multibuffer
 245        + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer);
 246    let target_excerpt_end = target_excerpt_start_in_multibuffer
 247        + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer);
 248
 249    CompanionExcerptPatch {
 250        patch: Patch::new(merged_edits),
 251        edited_range,
 252        source_excerpt_range: source_excerpt_start_in_multibuffer..source_excerpt_end,
 253        target_excerpt_range: target_excerpt_start_in_multibuffer..target_excerpt_end,
 254    }
 255}
 256
 257pub struct SplitDiffFeatureFlag;
 258
 259impl FeatureFlag for SplitDiffFeatureFlag {
 260    const NAME: &'static str = "split-diff";
 261
 262    fn enabled_for_staff() -> bool {
 263        true
 264    }
 265}
 266
 267#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 268#[action(namespace = editor)]
 269struct SplitDiff;
 270
 271#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 272#[action(namespace = editor)]
 273struct UnsplitDiff;
 274
 275#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 276#[action(namespace = editor)]
 277pub struct ToggleSplitDiff;
 278
 279#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 280#[action(namespace = editor)]
 281struct JumpToCorrespondingRow;
 282
 283/// When locked cursors mode is enabled, cursor movements in one editor will
 284/// update the cursor position in the other editor to the corresponding row.
 285#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 286#[action(namespace = editor)]
 287pub struct ToggleLockedCursors;
 288
 289pub struct SplittableEditor {
 290    rhs_multibuffer: Entity<MultiBuffer>,
 291    rhs_editor: Entity<Editor>,
 292    lhs: Option<LhsEditor>,
 293    workspace: WeakEntity<Workspace>,
 294    split_state: Entity<SplitEditorState>,
 295    locked_cursors: bool,
 296    _subscriptions: Vec<Subscription>,
 297}
 298
 299struct LhsEditor {
 300    multibuffer: Entity<MultiBuffer>,
 301    editor: Entity<Editor>,
 302    was_last_focused: bool,
 303    _subscriptions: Vec<Subscription>,
 304}
 305
 306impl SplittableEditor {
 307    pub fn rhs_editor(&self) -> &Entity<Editor> {
 308        &self.rhs_editor
 309    }
 310
 311    pub fn lhs_editor(&self) -> Option<&Entity<Editor>> {
 312        self.lhs.as_ref().map(|s| &s.editor)
 313    }
 314
 315    pub fn is_split(&self) -> bool {
 316        self.lhs.is_some()
 317    }
 318
 319    pub fn set_render_diff_hunk_controls(
 320        &self,
 321        render_diff_hunk_controls: RenderDiffHunkControlsFn,
 322        cx: &mut Context<Self>,
 323    ) {
 324        self.rhs_editor.update(cx, |editor, cx| {
 325            editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
 326        });
 327
 328        if let Some(lhs) = &self.lhs {
 329            lhs.editor.update(cx, |editor, cx| {
 330                editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
 331            });
 332        }
 333    }
 334
 335    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 336        if let Some(lhs) = &self.lhs
 337            && lhs.was_last_focused
 338        {
 339            &lhs.editor
 340        } else {
 341            &self.rhs_editor
 342        }
 343    }
 344
 345    pub fn new_unsplit(
 346        rhs_multibuffer: Entity<MultiBuffer>,
 347        project: Entity<Project>,
 348        workspace: Entity<Workspace>,
 349        window: &mut Window,
 350        cx: &mut Context<Self>,
 351    ) -> Self {
 352        let rhs_editor = cx.new(|cx| {
 353            let mut editor =
 354                Editor::for_multibuffer(rhs_multibuffer.clone(), Some(project.clone()), window, cx);
 355            editor.set_expand_all_diff_hunks(cx);
 356            editor
 357        });
 358        // TODO(split-diff) we might want to tag editor events with whether they came from rhs/lhs
 359        let subscriptions = vec![
 360            cx.subscribe(
 361                &rhs_editor,
 362                |this, _, event: &EditorEvent, cx| match event {
 363                    EditorEvent::ExpandExcerptsRequested {
 364                        excerpt_ids,
 365                        lines,
 366                        direction,
 367                    } => {
 368                        this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
 369                    }
 370                    _ => cx.emit(event.clone()),
 371                },
 372            ),
 373            cx.subscribe(&rhs_editor, |_, _, event: &SearchEvent, cx| {
 374                cx.emit(event.clone());
 375            }),
 376        ];
 377
 378        window.defer(cx, {
 379            let workspace = workspace.downgrade();
 380            let rhs_editor = rhs_editor.downgrade();
 381            move |window, cx| {
 382                workspace
 383                    .update(cx, |workspace, cx| {
 384                        rhs_editor.update(cx, |editor, cx| {
 385                            editor.added_to_workspace(workspace, window, cx);
 386                        })
 387                    })
 388                    .ok();
 389            }
 390        });
 391        let split_state = cx.new(|cx| SplitEditorState::new(cx));
 392        Self {
 393            rhs_editor,
 394            rhs_multibuffer,
 395            lhs: None,
 396            workspace: workspace.downgrade(),
 397            split_state,
 398            locked_cursors: false,
 399            _subscriptions: subscriptions,
 400        }
 401    }
 402
 403    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
 404        if !cx.has_flag::<SplitDiffFeatureFlag>() {
 405            return;
 406        }
 407        if self.lhs.is_some() {
 408            return;
 409        }
 410        let Some(workspace) = self.workspace.upgrade() else {
 411            return;
 412        };
 413        let project = workspace.read(cx).project().clone();
 414
 415        let lhs_multibuffer = cx.new(|cx| {
 416            let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
 417            multibuffer.set_all_diff_hunks_expanded(cx);
 418            multibuffer
 419        });
 420
 421        let render_diff_hunk_controls = self.rhs_editor.read(cx).render_diff_hunk_controls.clone();
 422        let lhs_editor = cx.new(|cx| {
 423            let mut editor =
 424                Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx);
 425            editor.set_number_deleted_lines(true, cx);
 426            editor.set_delegate_expand_excerpts(true);
 427            editor.set_delegate_stage_and_restore(true);
 428            editor.set_show_vertical_scrollbar(false, cx);
 429            editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
 430            editor
 431        });
 432
 433        lhs_editor.update(cx, |editor, cx| {
 434            editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
 435        });
 436
 437        let mut subscriptions = vec![
 438            cx.subscribe(
 439                &lhs_editor,
 440                |this, _, event: &EditorEvent, cx| match event {
 441                    EditorEvent::ExpandExcerptsRequested {
 442                        excerpt_ids,
 443                        lines,
 444                        direction,
 445                    } => {
 446                        if this.lhs.is_some() {
 447                            let rhs_display_map = this.rhs_editor.read(cx).display_map.read(cx);
 448                            let rhs_ids: Vec<_> = excerpt_ids
 449                                .iter()
 450                                .filter_map(|id| {
 451                                    rhs_display_map.companion_excerpt_to_my_excerpt(*id, cx)
 452                                })
 453                                .collect();
 454                            this.expand_excerpts(rhs_ids.into_iter(), *lines, *direction, cx);
 455                        }
 456                    }
 457                    EditorEvent::StageOrUnstageRequested { stage, hunks } => {
 458                        if this.lhs.is_some() {
 459                            let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
 460                            if !translated.is_empty() {
 461                                let stage = *stage;
 462                                this.rhs_editor.update(cx, |editor, cx| {
 463                                    let chunk_by = translated.into_iter().chunk_by(|h| h.buffer_id);
 464                                    for (buffer_id, hunks) in &chunk_by {
 465                                        editor.do_stage_or_unstage(stage, buffer_id, hunks, cx);
 466                                    }
 467                                });
 468                            }
 469                        }
 470                    }
 471                    EditorEvent::RestoreRequested { hunks } => {
 472                        if this.lhs.is_some() {
 473                            let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
 474                            if !translated.is_empty() {
 475                                this.rhs_editor.update(cx, |editor, cx| {
 476                                    editor.restore_diff_hunks(translated, cx);
 477                                });
 478                            }
 479                        }
 480                    }
 481                    _ => cx.emit(event.clone()),
 482                },
 483            ),
 484            cx.subscribe(&lhs_editor, |_, _, event: &SearchEvent, cx| {
 485                cx.emit(event.clone());
 486            }),
 487        ];
 488
 489        let lhs_focus_handle = lhs_editor.read(cx).focus_handle(cx);
 490        subscriptions.push(
 491            cx.on_focus_in(&lhs_focus_handle, window, |this, _window, cx| {
 492                if let Some(lhs) = &mut this.lhs {
 493                    if !lhs.was_last_focused {
 494                        lhs.was_last_focused = true;
 495                        cx.emit(SearchEvent::MatchesInvalidated);
 496                        cx.notify();
 497                    }
 498                }
 499            }),
 500        );
 501
 502        let rhs_focus_handle = self.rhs_editor.read(cx).focus_handle(cx);
 503        subscriptions.push(
 504            cx.on_focus_in(&rhs_focus_handle, window, |this, _window, cx| {
 505                if let Some(lhs) = &mut this.lhs {
 506                    if lhs.was_last_focused {
 507                        lhs.was_last_focused = false;
 508                        cx.emit(SearchEvent::MatchesInvalidated);
 509                        cx.notify();
 510                    }
 511                }
 512            }),
 513        );
 514
 515        let mut lhs = LhsEditor {
 516            editor: lhs_editor,
 517            multibuffer: lhs_multibuffer,
 518            was_last_focused: false,
 519            _subscriptions: subscriptions,
 520        };
 521        let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
 522        let lhs_display_map = lhs.editor.read(cx).display_map.clone();
 523        let rhs_display_map_id = rhs_display_map.entity_id();
 524
 525        self.rhs_editor.update(cx, |editor, cx| {
 526            editor.set_delegate_expand_excerpts(true);
 527            editor.buffer().update(cx, |rhs_multibuffer, cx| {
 528                rhs_multibuffer.set_show_deleted_hunks(false, cx);
 529                rhs_multibuffer.set_use_extended_diff_range(true, cx);
 530            })
 531        });
 532
 533        let path_diffs: Vec<_> = {
 534            let rhs_multibuffer = self.rhs_multibuffer.read(cx);
 535            rhs_multibuffer
 536                .paths()
 537                .filter_map(|path| {
 538                    let excerpt_id = rhs_multibuffer.excerpts_for_path(path).next()?;
 539                    let snapshot = rhs_multibuffer.snapshot(cx);
 540                    let buffer = snapshot.buffer_for_excerpt(excerpt_id)?;
 541                    let diff = rhs_multibuffer.diff_for(buffer.remote_id())?;
 542                    Some((path.clone(), diff))
 543                })
 544                .collect()
 545        };
 546
 547        let rhs_folded_buffers = rhs_display_map.read(cx).folded_buffers().clone();
 548
 549        let mut companion = Companion::new(
 550            rhs_display_map_id,
 551            rhs_folded_buffers,
 552            convert_rhs_rows_to_lhs,
 553            convert_lhs_rows_to_rhs,
 554        );
 555
 556        // stream this
 557        for (path, diff) in path_diffs {
 558            for (lhs, rhs) in
 559                lhs.update_path_excerpts_from_rhs(path, &self.rhs_multibuffer, diff.clone(), cx)
 560            {
 561                companion.add_excerpt_mapping(lhs, rhs);
 562            }
 563            companion.add_buffer_mapping(
 564                diff.read(cx).base_text(cx).remote_id(),
 565                diff.read(cx).buffer_id,
 566            );
 567        }
 568
 569        let companion = cx.new(|_| companion);
 570
 571        rhs_display_map.update(cx, |dm, cx| {
 572            dm.set_companion(Some((lhs_display_map.downgrade(), companion.clone())), cx);
 573        });
 574        lhs_display_map.update(cx, |dm, cx| {
 575            dm.set_companion(Some((rhs_display_map.downgrade(), companion)), cx);
 576        });
 577
 578        let shared_scroll_anchor = self
 579            .rhs_editor
 580            .read(cx)
 581            .scroll_manager
 582            .scroll_anchor_entity();
 583        lhs.editor.update(cx, |editor, _cx| {
 584            editor
 585                .scroll_manager
 586                .set_shared_scroll_anchor(shared_scroll_anchor);
 587        });
 588
 589        let this = cx.entity().downgrade();
 590        self.rhs_editor.update(cx, |editor, _cx| {
 591            let this = this.clone();
 592            editor.set_on_local_selections_changed(Some(Box::new(
 593                move |cursor_position, window, cx| {
 594                    let this = this.clone();
 595                    window.defer(cx, move |window, cx| {
 596                        this.update(cx, |this, cx| {
 597                            if this.locked_cursors {
 598                                this.sync_cursor_to_other_side(true, cursor_position, window, cx);
 599                            }
 600                        })
 601                        .ok();
 602                    })
 603                },
 604            )));
 605        });
 606        lhs.editor.update(cx, |editor, _cx| {
 607            let this = this.clone();
 608            editor.set_on_local_selections_changed(Some(Box::new(
 609                move |cursor_position, window, cx| {
 610                    let this = this.clone();
 611                    window.defer(cx, move |window, cx| {
 612                        this.update(cx, |this, cx| {
 613                            if this.locked_cursors {
 614                                this.sync_cursor_to_other_side(false, cursor_position, window, cx);
 615                            }
 616                        })
 617                        .ok();
 618                    })
 619                },
 620            )));
 621        });
 622
 623        // Copy soft wrap state from rhs (source of truth) to lhs
 624        let rhs_soft_wrap_override = self.rhs_editor.read(cx).soft_wrap_mode_override;
 625        lhs.editor.update(cx, |editor, cx| {
 626            editor.soft_wrap_mode_override = rhs_soft_wrap_override;
 627            cx.notify();
 628        });
 629
 630        self.lhs = Some(lhs);
 631
 632        cx.notify();
 633    }
 634
 635    fn activate_pane_left(
 636        &mut self,
 637        _: &ActivatePaneLeft,
 638        window: &mut Window,
 639        cx: &mut Context<Self>,
 640    ) {
 641        if let Some(lhs) = &self.lhs {
 642            if !lhs.was_last_focused {
 643                lhs.editor.read(cx).focus_handle(cx).focus(window, cx);
 644                lhs.editor.update(cx, |editor, cx| {
 645                    editor.request_autoscroll(Autoscroll::fit(), cx);
 646                });
 647            } else {
 648                cx.propagate();
 649            }
 650        } else {
 651            cx.propagate();
 652        }
 653    }
 654
 655    fn activate_pane_right(
 656        &mut self,
 657        _: &ActivatePaneRight,
 658        window: &mut Window,
 659        cx: &mut Context<Self>,
 660    ) {
 661        if let Some(lhs) = &self.lhs {
 662            if lhs.was_last_focused {
 663                self.rhs_editor.read(cx).focus_handle(cx).focus(window, cx);
 664                self.rhs_editor.update(cx, |editor, cx| {
 665                    editor.request_autoscroll(Autoscroll::fit(), cx);
 666                });
 667            } else {
 668                cx.propagate();
 669            }
 670        } else {
 671            cx.propagate();
 672        }
 673    }
 674
 675    fn toggle_locked_cursors(
 676        &mut self,
 677        _: &ToggleLockedCursors,
 678        _window: &mut Window,
 679        cx: &mut Context<Self>,
 680    ) {
 681        self.locked_cursors = !self.locked_cursors;
 682        cx.notify();
 683    }
 684
 685    pub fn locked_cursors(&self) -> bool {
 686        self.locked_cursors
 687    }
 688
 689    fn sync_cursor_to_other_side(
 690        &mut self,
 691        from_rhs: bool,
 692        source_point: Point,
 693        window: &mut Window,
 694        cx: &mut Context<Self>,
 695    ) {
 696        let Some(lhs) = &self.lhs else {
 697            return;
 698        };
 699
 700        let target_editor = if from_rhs {
 701            &lhs.editor
 702        } else {
 703            &self.rhs_editor
 704        };
 705
 706        let (source_multibuffer, target_multibuffer) = if from_rhs {
 707            (&self.rhs_multibuffer, &lhs.multibuffer)
 708        } else {
 709            (&lhs.multibuffer, &self.rhs_multibuffer)
 710        };
 711
 712        let source_snapshot = source_multibuffer.read(cx).snapshot(cx);
 713        let target_snapshot = target_multibuffer.read(cx).snapshot(cx);
 714
 715        let target_range = target_editor.update(cx, |target_editor, cx| {
 716            target_editor.display_map.update(cx, |display_map, cx| {
 717                let display_map_id = cx.entity_id();
 718                display_map.companion().unwrap().update(cx, |companion, _| {
 719                    companion.convert_point_from_companion(
 720                        display_map_id,
 721                        &target_snapshot,
 722                        &source_snapshot,
 723                        source_point,
 724                    )
 725                })
 726            })
 727        });
 728
 729        target_editor.update(cx, |editor, cx| {
 730            editor.set_suppress_selection_callback(true);
 731            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
 732                s.select_ranges([target_range]);
 733            });
 734            editor.set_suppress_selection_callback(false);
 735        });
 736    }
 737
 738    fn toggle_split(&mut self, _: &ToggleSplitDiff, window: &mut Window, cx: &mut Context<Self>) {
 739        if self.lhs.is_some() {
 740            self.unsplit(&UnsplitDiff, window, cx);
 741        } else {
 742            self.split(&SplitDiff, window, cx);
 743        }
 744    }
 745
 746    fn intercept_toggle_code_actions(
 747        &mut self,
 748        _: &ToggleCodeActions,
 749        _window: &mut Window,
 750        cx: &mut Context<Self>,
 751    ) {
 752        if self.lhs.is_some() {
 753            cx.stop_propagation();
 754        } else {
 755            cx.propagate();
 756        }
 757    }
 758
 759    fn intercept_toggle_breakpoint(
 760        &mut self,
 761        _: &ToggleBreakpoint,
 762        _window: &mut Window,
 763        cx: &mut Context<Self>,
 764    ) {
 765        // Only block breakpoint actions when the left (lhs) editor has focus
 766        if let Some(lhs) = &self.lhs {
 767            if lhs.was_last_focused {
 768                cx.stop_propagation();
 769            } else {
 770                cx.propagate();
 771            }
 772        } else {
 773            cx.propagate();
 774        }
 775    }
 776
 777    fn intercept_enable_breakpoint(
 778        &mut self,
 779        _: &EnableBreakpoint,
 780        _window: &mut Window,
 781        cx: &mut Context<Self>,
 782    ) {
 783        // Only block breakpoint actions when the left (lhs) editor has focus
 784        if let Some(lhs) = &self.lhs {
 785            if lhs.was_last_focused {
 786                cx.stop_propagation();
 787            } else {
 788                cx.propagate();
 789            }
 790        } else {
 791            cx.propagate();
 792        }
 793    }
 794
 795    fn intercept_disable_breakpoint(
 796        &mut self,
 797        _: &DisableBreakpoint,
 798        _window: &mut Window,
 799        cx: &mut Context<Self>,
 800    ) {
 801        // Only block breakpoint actions when the left (lhs) editor has focus
 802        if let Some(lhs) = &self.lhs {
 803            if lhs.was_last_focused {
 804                cx.stop_propagation();
 805            } else {
 806                cx.propagate();
 807            }
 808        } else {
 809            cx.propagate();
 810        }
 811    }
 812
 813    fn intercept_edit_log_breakpoint(
 814        &mut self,
 815        _: &EditLogBreakpoint,
 816        _window: &mut Window,
 817        cx: &mut Context<Self>,
 818    ) {
 819        // Only block breakpoint actions when the left (lhs) editor has focus
 820        if let Some(lhs) = &self.lhs {
 821            if lhs.was_last_focused {
 822                cx.stop_propagation();
 823            } else {
 824                cx.propagate();
 825            }
 826        } else {
 827            cx.propagate();
 828        }
 829    }
 830
 831    fn intercept_inline_assist(
 832        &mut self,
 833        _: &InlineAssist,
 834        _window: &mut Window,
 835        cx: &mut Context<Self>,
 836    ) {
 837        if self.lhs.is_some() {
 838            cx.stop_propagation();
 839        } else {
 840            cx.propagate();
 841        }
 842    }
 843
 844    fn toggle_soft_wrap(
 845        &mut self,
 846        _: &ToggleSoftWrap,
 847        window: &mut Window,
 848        cx: &mut Context<Self>,
 849    ) {
 850        if let Some(lhs) = &self.lhs {
 851            cx.stop_propagation();
 852
 853            let is_lhs_focused = lhs.was_last_focused;
 854            let (focused_editor, other_editor) = if is_lhs_focused {
 855                (&lhs.editor, &self.rhs_editor)
 856            } else {
 857                (&self.rhs_editor, &lhs.editor)
 858            };
 859
 860            // Toggle the focused editor
 861            focused_editor.update(cx, |editor, cx| {
 862                editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
 863            });
 864
 865            // Copy the soft wrap state from the focused editor to the other editor
 866            let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
 867            other_editor.update(cx, |editor, cx| {
 868                editor.soft_wrap_mode_override = soft_wrap_override;
 869                cx.notify();
 870            });
 871        } else {
 872            cx.propagate();
 873        }
 874    }
 875
 876    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
 877        let Some(lhs) = self.lhs.take() else {
 878            return;
 879        };
 880        self.rhs_editor.update(cx, |rhs, cx| {
 881            let rhs_snapshot = rhs.display_map.update(cx, |dm, cx| dm.snapshot(cx));
 882            let native_anchor = rhs.scroll_manager.native_anchor(&rhs_snapshot, cx);
 883            let rhs_display_map_id = rhs_snapshot.display_map_id;
 884            rhs.scroll_manager
 885                .scroll_anchor_entity()
 886                .update(cx, |shared, _| {
 887                    shared.scroll_anchor = native_anchor;
 888                    shared.display_map_id = Some(rhs_display_map_id);
 889                });
 890
 891            rhs.set_on_local_selections_changed(None);
 892            rhs.set_delegate_expand_excerpts(false);
 893            rhs.buffer().update(cx, |buffer, cx| {
 894                buffer.set_show_deleted_hunks(true, cx);
 895                buffer.set_use_extended_diff_range(false, cx);
 896            });
 897            rhs.display_map.update(cx, |dm, cx| {
 898                dm.set_companion(None, cx);
 899            });
 900        });
 901        lhs.editor.update(cx, |editor, _cx| {
 902            editor.set_on_local_selections_changed(None);
 903        });
 904        cx.notify();
 905    }
 906
 907    pub fn set_excerpts_for_path(
 908        &mut self,
 909        path: PathKey,
 910        buffer: Entity<Buffer>,
 911        ranges: impl IntoIterator<Item = Range<Point>> + Clone,
 912        context_line_count: u32,
 913        diff: Entity<BufferDiff>,
 914        cx: &mut Context<Self>,
 915    ) -> (Vec<Range<Anchor>>, bool) {
 916        let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
 917        let lhs_display_map = self
 918            .lhs
 919            .as_ref()
 920            .map(|s| s.editor.read(cx).display_map.clone());
 921
 922        let (anchors, added_a_new_excerpt) =
 923            self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
 924                let (anchors, added_a_new_excerpt) = rhs_multibuffer.set_excerpts_for_path(
 925                    path.clone(),
 926                    buffer.clone(),
 927                    ranges,
 928                    context_line_count,
 929                    cx,
 930                );
 931                if !anchors.is_empty()
 932                    && rhs_multibuffer
 933                        .diff_for(buffer.read(cx).remote_id())
 934                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
 935                {
 936                    rhs_multibuffer.add_diff(diff.clone(), cx);
 937                }
 938                (anchors, added_a_new_excerpt)
 939            });
 940
 941        if let Some(lhs) = &mut self.lhs {
 942            if let Some(lhs_display_map) = &lhs_display_map {
 943                lhs.sync_path_excerpts(
 944                    path,
 945                    &self.rhs_multibuffer,
 946                    diff,
 947                    &rhs_display_map,
 948                    lhs_display_map,
 949                    cx,
 950                );
 951            }
 952        }
 953
 954        (anchors, added_a_new_excerpt)
 955    }
 956
 957    fn expand_excerpts(
 958        &mut self,
 959        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
 960        lines: u32,
 961        direction: ExpandExcerptDirection,
 962        cx: &mut Context<Self>,
 963    ) {
 964        let mut corresponding_paths = HashMap::default();
 965        self.rhs_multibuffer.update(cx, |multibuffer, cx| {
 966            let snapshot = multibuffer.snapshot(cx);
 967            if self.lhs.is_some() {
 968                corresponding_paths = excerpt_ids
 969                    .clone()
 970                    .map(|excerpt_id| {
 971                        let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
 972                        let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
 973                        let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
 974                        (path, diff)
 975                    })
 976                    .collect::<HashMap<_, _>>();
 977            }
 978            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
 979        });
 980
 981        if let Some(lhs) = &mut self.lhs {
 982            let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
 983            let lhs_display_map = lhs.editor.read(cx).display_map.clone();
 984            for (path, diff) in corresponding_paths {
 985                lhs.sync_path_excerpts(
 986                    path,
 987                    &self.rhs_multibuffer,
 988                    diff,
 989                    &rhs_display_map,
 990                    &lhs_display_map,
 991                    cx,
 992                );
 993            }
 994        }
 995    }
 996
 997    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
 998        self.rhs_multibuffer.update(cx, |buffer, cx| {
 999            buffer.remove_excerpts_for_path(path.clone(), cx)
1000        });
1001        if let Some(lhs) = &self.lhs {
1002            let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
1003            let lhs_display_map = lhs.editor.read(cx).display_map.clone();
1004            lhs.remove_mappings_for_path(
1005                &path,
1006                &self.rhs_multibuffer,
1007                &rhs_display_map,
1008                &lhs_display_map,
1009                cx,
1010            );
1011            lhs.multibuffer
1012                .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
1013        }
1014    }
1015}
1016
1017#[cfg(test)]
1018impl SplittableEditor {
1019    fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1020        use multi_buffer::MultiBufferRow;
1021        use text::Bias;
1022
1023        use crate::display_map::Block;
1024        use crate::display_map::DisplayRow;
1025
1026        self.debug_print(cx);
1027
1028        let lhs = self.lhs.as_ref().unwrap();
1029        let rhs_excerpts = self.rhs_multibuffer.read(cx).excerpt_ids();
1030        let lhs_excerpts = lhs.multibuffer.read(cx).excerpt_ids();
1031        assert_eq!(
1032            lhs_excerpts.len(),
1033            rhs_excerpts.len(),
1034            "mismatch in excerpt count"
1035        );
1036
1037        if quiesced {
1038            let rhs_snapshot = lhs
1039                .editor
1040                .update(cx, |editor, cx| editor.display_snapshot(cx));
1041            let lhs_snapshot = self
1042                .rhs_editor
1043                .update(cx, |editor, cx| editor.display_snapshot(cx));
1044
1045            let lhs_max_row = lhs_snapshot.max_point().row();
1046            let rhs_max_row = rhs_snapshot.max_point().row();
1047            assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1048
1049            let lhs_excerpt_block_rows = lhs_snapshot
1050                .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1051                .filter(|(_, block)| {
1052                    matches!(
1053                        block,
1054                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1055                    )
1056                })
1057                .map(|(row, _)| row)
1058                .collect::<Vec<_>>();
1059            let rhs_excerpt_block_rows = rhs_snapshot
1060                .blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
1061                .filter(|(_, block)| {
1062                    matches!(
1063                        block,
1064                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1065                    )
1066                })
1067                .map(|(row, _)| row)
1068                .collect::<Vec<_>>();
1069            assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1070
1071            for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1072                assert_eq!(
1073                    lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1074                    "mismatch in hunks"
1075                );
1076                assert_eq!(
1077                    lhs_hunk.status, rhs_hunk.status,
1078                    "mismatch in hunk statuses"
1079                );
1080
1081                let (lhs_point, rhs_point) =
1082                    if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1083                        (
1084                            Point::new(lhs_hunk.row_range.end.0, 0),
1085                            Point::new(rhs_hunk.row_range.end.0, 0),
1086                        )
1087                    } else {
1088                        (
1089                            Point::new(lhs_hunk.row_range.start.0, 0),
1090                            Point::new(rhs_hunk.row_range.start.0, 0),
1091                        )
1092                    };
1093                let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1094                let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1095                assert_eq!(
1096                    lhs_point.row(),
1097                    rhs_point.row(),
1098                    "mismatch in hunk position"
1099                );
1100            }
1101
1102            // Filtering out empty lines is a bit of a hack, to work around a case where
1103            // the base text has a trailing newline but the current text doesn't, or vice versa.
1104            // In this case, we get the additional newline on one side, but that line is not
1105            // marked as added/deleted by rowinfos.
1106            self.check_sides_match(cx, |snapshot| {
1107                snapshot
1108                    .buffer_snapshot()
1109                    .text()
1110                    .split("\n")
1111                    .zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
1112                    .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
1113                    .map(|(line, _)| line.to_owned())
1114                    .collect::<Vec<_>>()
1115            });
1116        }
1117    }
1118
1119    #[track_caller]
1120    fn check_sides_match<T: std::fmt::Debug + PartialEq>(
1121        &self,
1122        cx: &mut App,
1123        mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
1124    ) {
1125        let lhs = self.lhs.as_ref().expect("requires split");
1126        let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1127            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1128        });
1129        let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1130            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1131        });
1132
1133        let rhs_t = extract(&rhs_snapshot);
1134        let lhs_t = extract(&lhs_snapshot);
1135
1136        if rhs_t != lhs_t {
1137            self.debug_print(cx);
1138            pretty_assertions::assert_eq!(rhs_t, lhs_t);
1139        }
1140    }
1141
1142    fn debug_print(&self, cx: &mut App) {
1143        use crate::DisplayRow;
1144        use crate::display_map::Block;
1145        use buffer_diff::DiffHunkStatusKind;
1146
1147        assert!(
1148            self.lhs.is_some(),
1149            "debug_print is only useful when lhs editor exists"
1150        );
1151
1152        let lhs = self.lhs.as_ref().unwrap();
1153
1154        // Get terminal width, default to 80 if unavailable
1155        let terminal_width = std::env::var("COLUMNS")
1156            .ok()
1157            .and_then(|s| s.parse::<usize>().ok())
1158            .unwrap_or(80);
1159
1160        // Each side gets half the terminal width minus the separator
1161        let separator = "";
1162        let side_width = (terminal_width - separator.len()) / 2;
1163
1164        // Get display snapshots for both editors
1165        let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1166            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1167        });
1168        let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1169            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1170        });
1171
1172        let lhs_max_row = lhs_snapshot.max_point().row().0;
1173        let rhs_max_row = rhs_snapshot.max_point().row().0;
1174        let max_row = lhs_max_row.max(rhs_max_row);
1175
1176        // Build a map from display row -> block type string
1177        // Each row of a multi-row block gets an entry with the same block type
1178        // For spacers, the ID is included in brackets
1179        fn build_block_map(
1180            snapshot: &crate::DisplaySnapshot,
1181            max_row: u32,
1182        ) -> std::collections::HashMap<u32, String> {
1183            let mut block_map = std::collections::HashMap::new();
1184            for (start_row, block) in
1185                snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1186            {
1187                let (block_type, height) = match block {
1188                    Block::Spacer {
1189                        id,
1190                        height,
1191                        is_below: _,
1192                    } => (format!("SPACER[{}]", id.0), *height),
1193                    Block::ExcerptBoundary { height, .. } => {
1194                        ("EXCERPT_BOUNDARY".to_string(), *height)
1195                    }
1196                    Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1197                    Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1198                    Block::Custom(custom) => {
1199                        ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1200                    }
1201                };
1202                for offset in 0..height {
1203                    block_map.insert(start_row.0 + offset, block_type.clone());
1204                }
1205            }
1206            block_map
1207        }
1208
1209        let lhs_blocks = build_block_map(&lhs_snapshot, lhs_max_row);
1210        let rhs_blocks = build_block_map(&rhs_snapshot, rhs_max_row);
1211
1212        fn display_width(s: &str) -> usize {
1213            unicode_width::UnicodeWidthStr::width(s)
1214        }
1215
1216        fn truncate_line(line: &str, max_width: usize) -> String {
1217            let line_width = display_width(line);
1218            if line_width <= max_width {
1219                return line.to_string();
1220            }
1221            if max_width < 9 {
1222                let mut result = String::new();
1223                let mut width = 0;
1224                for c in line.chars() {
1225                    let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1226                    if width + c_width > max_width {
1227                        break;
1228                    }
1229                    result.push(c);
1230                    width += c_width;
1231                }
1232                return result;
1233            }
1234            let ellipsis = "...";
1235            let target_prefix_width = 3;
1236            let target_suffix_width = 3;
1237
1238            let mut prefix = String::new();
1239            let mut prefix_width = 0;
1240            for c in line.chars() {
1241                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1242                if prefix_width + c_width > target_prefix_width {
1243                    break;
1244                }
1245                prefix.push(c);
1246                prefix_width += c_width;
1247            }
1248
1249            let mut suffix_chars: Vec<char> = Vec::new();
1250            let mut suffix_width = 0;
1251            for c in line.chars().rev() {
1252                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1253                if suffix_width + c_width > target_suffix_width {
1254                    break;
1255                }
1256                suffix_chars.push(c);
1257                suffix_width += c_width;
1258            }
1259            suffix_chars.reverse();
1260            let suffix: String = suffix_chars.into_iter().collect();
1261
1262            format!("{}{}{}", prefix, ellipsis, suffix)
1263        }
1264
1265        fn pad_to_width(s: &str, target_width: usize) -> String {
1266            let current_width = display_width(s);
1267            if current_width >= target_width {
1268                s.to_string()
1269            } else {
1270                format!("{}{}", s, " ".repeat(target_width - current_width))
1271            }
1272        }
1273
1274        // Helper to format a single row for one side
1275        // Format: "ln# diff bytes(cumul) text" or block info
1276        // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1277        fn format_row(
1278            row: u32,
1279            max_row: u32,
1280            snapshot: &crate::DisplaySnapshot,
1281            blocks: &std::collections::HashMap<u32, String>,
1282            row_infos: &[multi_buffer::RowInfo],
1283            cumulative_bytes: &[usize],
1284            side_width: usize,
1285        ) -> String {
1286            // Get row info if available
1287            let row_info = row_infos.get(row as usize);
1288
1289            // Line number prefix (3 chars + space)
1290            // Use buffer_row from RowInfo, which is None for block rows
1291            let line_prefix = if row > max_row {
1292                "    ".to_string()
1293            } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1294                format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1295            } else {
1296                "    ".to_string() // block rows have no line number
1297            };
1298            let content_width = side_width.saturating_sub(line_prefix.len());
1299
1300            if row > max_row {
1301                return format!("{}{}", line_prefix, " ".repeat(content_width));
1302            }
1303
1304            // Check if this row is a block row
1305            if let Some(block_type) = blocks.get(&row) {
1306                let block_str = format!("~~~[{}]~~~", block_type);
1307                let formatted = format!("{:^width$}", block_str, width = content_width);
1308                return format!(
1309                    "{}{}",
1310                    line_prefix,
1311                    truncate_line(&formatted, content_width)
1312                );
1313            }
1314
1315            // Get line text
1316            let line_text = snapshot.line(DisplayRow(row));
1317            let line_bytes = line_text.len();
1318
1319            // Diff status marker
1320            let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1321                Some(status) => match status.kind {
1322                    DiffHunkStatusKind::Added => "+",
1323                    DiffHunkStatusKind::Deleted => "-",
1324                    DiffHunkStatusKind::Modified => "~",
1325                },
1326                None => " ",
1327            };
1328
1329            // Cumulative bytes
1330            let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1331
1332            // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1333            let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1334            let text_width = content_width.saturating_sub(info_prefix.len());
1335            let truncated_text = truncate_line(&line_text, text_width);
1336
1337            let text_part = pad_to_width(&truncated_text, text_width);
1338            format!("{}{}{}", line_prefix, info_prefix, text_part)
1339        }
1340
1341        // Collect row infos for both sides
1342        let lhs_row_infos: Vec<_> = lhs_snapshot
1343            .row_infos(DisplayRow(0))
1344            .take((lhs_max_row + 1) as usize)
1345            .collect();
1346        let rhs_row_infos: Vec<_> = rhs_snapshot
1347            .row_infos(DisplayRow(0))
1348            .take((rhs_max_row + 1) as usize)
1349            .collect();
1350
1351        // Calculate cumulative bytes for each side (only counting non-block rows)
1352        let mut lhs_cumulative = Vec::with_capacity((lhs_max_row + 1) as usize);
1353        let mut cumulative = 0usize;
1354        for row in 0..=lhs_max_row {
1355            if !lhs_blocks.contains_key(&row) {
1356                cumulative += lhs_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1357            }
1358            lhs_cumulative.push(cumulative);
1359        }
1360
1361        let mut rhs_cumulative = Vec::with_capacity((rhs_max_row + 1) as usize);
1362        cumulative = 0;
1363        for row in 0..=rhs_max_row {
1364            if !rhs_blocks.contains_key(&row) {
1365                cumulative += rhs_snapshot.line(DisplayRow(row)).len() + 1;
1366            }
1367            rhs_cumulative.push(cumulative);
1368        }
1369
1370        // Print header
1371        eprintln!();
1372        eprintln!("{}", "".repeat(terminal_width));
1373        let header_left = format!("{:^width$}", "(LHS)", width = side_width);
1374        let header_right = format!("{:^width$}", "(RHS)", width = side_width);
1375        eprintln!("{}{}{}", header_left, separator, header_right);
1376        eprintln!(
1377            "{:^width$}{}{:^width$}",
1378            "ln# diff len(cum) text",
1379            separator,
1380            "ln# diff len(cum) text",
1381            width = side_width
1382        );
1383        eprintln!("{}", "".repeat(terminal_width));
1384
1385        // Print each row
1386        for row in 0..=max_row {
1387            let left = format_row(
1388                row,
1389                lhs_max_row,
1390                &lhs_snapshot,
1391                &lhs_blocks,
1392                &lhs_row_infos,
1393                &lhs_cumulative,
1394                side_width,
1395            );
1396            let right = format_row(
1397                row,
1398                rhs_max_row,
1399                &rhs_snapshot,
1400                &rhs_blocks,
1401                &rhs_row_infos,
1402                &rhs_cumulative,
1403                side_width,
1404            );
1405            eprintln!("{}{}{}", left, separator, right);
1406        }
1407
1408        eprintln!("{}", "".repeat(terminal_width));
1409        eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1410        eprintln!();
1411    }
1412
1413    fn randomly_edit_excerpts(
1414        &mut self,
1415        rng: &mut impl rand::Rng,
1416        mutation_count: usize,
1417        cx: &mut Context<Self>,
1418    ) {
1419        use collections::HashSet;
1420        use rand::prelude::*;
1421        use std::env;
1422        use util::RandomCharIter;
1423
1424        let max_buffers = env::var("MAX_BUFFERS")
1425            .map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
1426            .unwrap_or(4);
1427
1428        for _ in 0..mutation_count {
1429            let paths = self
1430                .rhs_multibuffer
1431                .read(cx)
1432                .paths()
1433                .cloned()
1434                .collect::<Vec<_>>();
1435            let excerpt_ids = self.rhs_multibuffer.read(cx).excerpt_ids();
1436
1437            if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
1438                let mut excerpts = HashSet::default();
1439                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
1440                    excerpts.extend(excerpt_ids.choose(rng).copied());
1441                }
1442
1443                let line_count = rng.random_range(1..5);
1444
1445                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
1446
1447                self.expand_excerpts(
1448                    excerpts.iter().cloned(),
1449                    line_count,
1450                    ExpandExcerptDirection::UpAndDown,
1451                    cx,
1452                );
1453                continue;
1454            }
1455
1456            if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
1457                let len = rng.random_range(100..500);
1458                let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
1459                let buffer = cx.new(|cx| Buffer::local(text, cx));
1460                log::info!(
1461                    "Creating new buffer {} with text: {:?}",
1462                    buffer.read(cx).remote_id(),
1463                    buffer.read(cx).text()
1464                );
1465                let buffer_snapshot = buffer.read(cx).snapshot();
1466                let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
1467                // Create some initial diff hunks.
1468                buffer.update(cx, |buffer, cx| {
1469                    buffer.randomly_edit(rng, 1, cx);
1470                });
1471                let buffer_snapshot = buffer.read(cx).text_snapshot();
1472                diff.update(cx, |diff, cx| {
1473                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
1474                });
1475                let path = PathKey::for_buffer(&buffer, cx);
1476                let ranges = diff.update(cx, |diff, cx| {
1477                    diff.snapshot(cx)
1478                        .hunks(&buffer_snapshot)
1479                        .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
1480                        .collect::<Vec<_>>()
1481                });
1482                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1483            } else {
1484                log::info!("removing excerpts");
1485                let remove_count = rng.random_range(1..=paths.len());
1486                let paths_to_remove = paths
1487                    .choose_multiple(rng, remove_count)
1488                    .cloned()
1489                    .collect::<Vec<_>>();
1490                for path in paths_to_remove {
1491                    self.remove_excerpts_for_path(path.clone(), cx);
1492                }
1493            }
1494        }
1495    }
1496}
1497
1498impl Item for SplittableEditor {
1499    type Event = EditorEvent;
1500
1501    fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1502        self.rhs_editor.read(cx).tab_content_text(detail, cx)
1503    }
1504
1505    fn tab_tooltip_text(&self, cx: &App) -> Option<ui::SharedString> {
1506        self.rhs_editor.read(cx).tab_tooltip_text(cx)
1507    }
1508
1509    fn tab_icon(&self, window: &Window, cx: &App) -> Option<ui::Icon> {
1510        self.rhs_editor.read(cx).tab_icon(window, cx)
1511    }
1512
1513    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> gpui::AnyElement {
1514        self.rhs_editor.read(cx).tab_content(params, window, cx)
1515    }
1516
1517    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
1518        Editor::to_item_events(event, f)
1519    }
1520
1521    fn for_each_project_item(
1522        &self,
1523        cx: &App,
1524        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1525    ) {
1526        self.rhs_editor.read(cx).for_each_project_item(cx, f)
1527    }
1528
1529    fn buffer_kind(&self, cx: &App) -> ItemBufferKind {
1530        self.rhs_editor.read(cx).buffer_kind(cx)
1531    }
1532
1533    fn is_dirty(&self, cx: &App) -> bool {
1534        self.rhs_editor.read(cx).is_dirty(cx)
1535    }
1536
1537    fn has_conflict(&self, cx: &App) -> bool {
1538        self.rhs_editor.read(cx).has_conflict(cx)
1539    }
1540
1541    fn has_deleted_file(&self, cx: &App) -> bool {
1542        self.rhs_editor.read(cx).has_deleted_file(cx)
1543    }
1544
1545    fn capability(&self, cx: &App) -> language::Capability {
1546        self.rhs_editor.read(cx).capability(cx)
1547    }
1548
1549    fn can_save(&self, cx: &App) -> bool {
1550        self.rhs_editor.read(cx).can_save(cx)
1551    }
1552
1553    fn can_save_as(&self, cx: &App) -> bool {
1554        self.rhs_editor.read(cx).can_save_as(cx)
1555    }
1556
1557    fn save(
1558        &mut self,
1559        options: SaveOptions,
1560        project: Entity<Project>,
1561        window: &mut Window,
1562        cx: &mut Context<Self>,
1563    ) -> gpui::Task<anyhow::Result<()>> {
1564        self.rhs_editor
1565            .update(cx, |editor, cx| editor.save(options, project, window, cx))
1566    }
1567
1568    fn save_as(
1569        &mut self,
1570        project: Entity<Project>,
1571        path: project::ProjectPath,
1572        window: &mut Window,
1573        cx: &mut Context<Self>,
1574    ) -> gpui::Task<anyhow::Result<()>> {
1575        self.rhs_editor
1576            .update(cx, |editor, cx| editor.save_as(project, path, window, cx))
1577    }
1578
1579    fn reload(
1580        &mut self,
1581        project: Entity<Project>,
1582        window: &mut Window,
1583        cx: &mut Context<Self>,
1584    ) -> gpui::Task<anyhow::Result<()>> {
1585        self.rhs_editor
1586            .update(cx, |editor, cx| editor.reload(project, window, cx))
1587    }
1588
1589    fn navigate(
1590        &mut self,
1591        data: Arc<dyn std::any::Any + Send>,
1592        window: &mut Window,
1593        cx: &mut Context<Self>,
1594    ) -> bool {
1595        self.last_selected_editor()
1596            .update(cx, |editor, cx| editor.navigate(data, window, cx))
1597    }
1598
1599    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1600        self.last_selected_editor().update(cx, |editor, cx| {
1601            editor.deactivated(window, cx);
1602        });
1603    }
1604
1605    fn added_to_workspace(
1606        &mut self,
1607        workspace: &mut Workspace,
1608        window: &mut Window,
1609        cx: &mut Context<Self>,
1610    ) {
1611        self.workspace = workspace.weak_handle();
1612        self.rhs_editor.update(cx, |rhs_editor, cx| {
1613            rhs_editor.added_to_workspace(workspace, window, cx);
1614        });
1615        if let Some(lhs) = &self.lhs {
1616            lhs.editor.update(cx, |lhs_editor, cx| {
1617                lhs_editor.added_to_workspace(workspace, window, cx);
1618            });
1619        }
1620    }
1621
1622    fn as_searchable(
1623        &self,
1624        handle: &Entity<Self>,
1625        _: &App,
1626    ) -> Option<Box<dyn SearchableItemHandle>> {
1627        Some(Box::new(handle.clone()))
1628    }
1629
1630    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1631        self.rhs_editor.read(cx).breadcrumb_location(cx)
1632    }
1633
1634    fn breadcrumbs(&self, cx: &App) -> Option<Vec<BreadcrumbText>> {
1635        self.rhs_editor.read(cx).breadcrumbs(cx)
1636    }
1637
1638    fn pixel_position_of_cursor(&self, cx: &App) -> Option<gpui::Point<gpui::Pixels>> {
1639        self.last_selected_editor()
1640            .read(cx)
1641            .pixel_position_of_cursor(cx)
1642    }
1643}
1644
1645impl SearchableItem for SplittableEditor {
1646    type Match = Range<Anchor>;
1647
1648    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1649        self.last_selected_editor().update(cx, |editor, cx| {
1650            editor.clear_matches(window, cx);
1651        });
1652    }
1653
1654    fn update_matches(
1655        &mut self,
1656        matches: &[Self::Match],
1657        active_match_index: Option<usize>,
1658        window: &mut Window,
1659        cx: &mut Context<Self>,
1660    ) {
1661        self.last_selected_editor().update(cx, |editor, cx| {
1662            editor.update_matches(matches, active_match_index, window, cx);
1663        });
1664    }
1665
1666    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1667        self.last_selected_editor()
1668            .update(cx, |editor, cx| editor.query_suggestion(window, cx))
1669    }
1670
1671    fn activate_match(
1672        &mut self,
1673        index: usize,
1674        matches: &[Self::Match],
1675        window: &mut Window,
1676        cx: &mut Context<Self>,
1677    ) {
1678        self.last_selected_editor().update(cx, |editor, cx| {
1679            editor.activate_match(index, matches, window, cx);
1680        });
1681    }
1682
1683    fn select_matches(
1684        &mut self,
1685        matches: &[Self::Match],
1686        window: &mut Window,
1687        cx: &mut Context<Self>,
1688    ) {
1689        self.last_selected_editor().update(cx, |editor, cx| {
1690            editor.select_matches(matches, window, cx);
1691        });
1692    }
1693
1694    fn replace(
1695        &mut self,
1696        identifier: &Self::Match,
1697        query: &project::search::SearchQuery,
1698        window: &mut Window,
1699        cx: &mut Context<Self>,
1700    ) {
1701        self.last_selected_editor().update(cx, |editor, cx| {
1702            editor.replace(identifier, query, window, cx);
1703        });
1704    }
1705
1706    fn find_matches(
1707        &mut self,
1708        query: Arc<project::search::SearchQuery>,
1709        window: &mut Window,
1710        cx: &mut Context<Self>,
1711    ) -> gpui::Task<Vec<Self::Match>> {
1712        self.last_selected_editor()
1713            .update(cx, |editor, cx| editor.find_matches(query, window, cx))
1714    }
1715
1716    fn active_match_index(
1717        &mut self,
1718        direction: workspace::searchable::Direction,
1719        matches: &[Self::Match],
1720        window: &mut Window,
1721        cx: &mut Context<Self>,
1722    ) -> Option<usize> {
1723        self.last_selected_editor().update(cx, |editor, cx| {
1724            editor.active_match_index(direction, matches, window, cx)
1725        })
1726    }
1727}
1728
1729impl EventEmitter<EditorEvent> for SplittableEditor {}
1730impl EventEmitter<SearchEvent> for SplittableEditor {}
1731impl Focusable for SplittableEditor {
1732    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1733        self.last_selected_editor().read(cx).focus_handle(cx)
1734    }
1735}
1736
1737// impl Item for SplittableEditor {
1738//     type Event = EditorEvent;
1739
1740//     fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1741//         self.rhs_editor().tab_content_text(detail, cx)
1742//     }
1743
1744//     fn as_searchable(&self, _this: &Entity<Self>, cx: &App) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
1745//         Some(Box::new(self.last_selected_editor().clone()))
1746//     }
1747// }
1748
1749impl Render for SplittableEditor {
1750    fn render(
1751        &mut self,
1752        _window: &mut ui::Window,
1753        cx: &mut ui::Context<Self>,
1754    ) -> impl ui::IntoElement {
1755        let inner = if self.lhs.is_some() {
1756            let style = self.rhs_editor.read(cx).create_style(cx);
1757            SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
1758        } else {
1759            self.rhs_editor.clone().into_any_element()
1760        };
1761        div()
1762            .id("splittable-editor")
1763            .on_action(cx.listener(Self::split))
1764            .on_action(cx.listener(Self::unsplit))
1765            .on_action(cx.listener(Self::toggle_split))
1766            .on_action(cx.listener(Self::activate_pane_left))
1767            .on_action(cx.listener(Self::activate_pane_right))
1768            .on_action(cx.listener(Self::toggle_locked_cursors))
1769            .on_action(cx.listener(Self::intercept_toggle_code_actions))
1770            .on_action(cx.listener(Self::intercept_toggle_breakpoint))
1771            .on_action(cx.listener(Self::intercept_enable_breakpoint))
1772            .on_action(cx.listener(Self::intercept_disable_breakpoint))
1773            .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
1774            .on_action(cx.listener(Self::intercept_inline_assist))
1775            .capture_action(cx.listener(Self::toggle_soft_wrap))
1776            .size_full()
1777            .child(inner)
1778    }
1779}
1780
1781impl LhsEditor {
1782    fn update_path_excerpts_from_rhs(
1783        &mut self,
1784        path_key: PathKey,
1785        rhs_multibuffer: &Entity<MultiBuffer>,
1786        diff: Entity<BufferDiff>,
1787        cx: &mut App,
1788    ) -> Vec<(ExcerptId, ExcerptId)> {
1789        let rhs_multibuffer_ref = rhs_multibuffer.read(cx);
1790        let rhs_excerpt_ids: Vec<ExcerptId> =
1791            rhs_multibuffer_ref.excerpts_for_path(&path_key).collect();
1792
1793        let Some(excerpt_id) = rhs_multibuffer_ref.excerpts_for_path(&path_key).next() else {
1794            self.multibuffer.update(cx, |multibuffer, cx| {
1795                multibuffer.remove_excerpts_for_path(path_key, cx);
1796            });
1797            return Vec::new();
1798        };
1799
1800        let rhs_multibuffer_snapshot = rhs_multibuffer_ref.snapshot(cx);
1801        let main_buffer = rhs_multibuffer_snapshot
1802            .buffer_for_excerpt(excerpt_id)
1803            .unwrap();
1804        let base_text_buffer = diff.read(cx).base_text_buffer();
1805        let diff_snapshot = diff.read(cx).snapshot(cx);
1806        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
1807        let new = rhs_multibuffer_ref
1808            .excerpts_for_buffer(main_buffer.remote_id(), cx)
1809            .into_iter()
1810            .map(|(_, excerpt_range)| {
1811                let point_range_to_base_text_point_range = |range: Range<Point>| {
1812                    let start = diff_snapshot
1813                        .buffer_point_to_base_text_range(
1814                            Point::new(range.start.row, 0),
1815                            main_buffer,
1816                        )
1817                        .start;
1818                    let end = diff_snapshot
1819                        .buffer_point_to_base_text_range(Point::new(range.end.row, 0), main_buffer)
1820                        .end;
1821                    let end_column = diff_snapshot.base_text().line_len(end.row);
1822                    Point::new(start.row, 0)..Point::new(end.row, end_column)
1823                };
1824                let rhs = excerpt_range.primary.to_point(main_buffer);
1825                let context = excerpt_range.context.to_point(main_buffer);
1826                ExcerptRange {
1827                    primary: point_range_to_base_text_point_range(rhs),
1828                    context: point_range_to_base_text_point_range(context),
1829                }
1830            })
1831            .collect();
1832
1833        self.editor.update(cx, |editor, cx| {
1834            editor.buffer().update(cx, |buffer, cx| {
1835                let (ids, _) = buffer.update_path_excerpts(
1836                    path_key.clone(),
1837                    base_text_buffer.clone(),
1838                    &base_text_buffer_snapshot,
1839                    new,
1840                    cx,
1841                );
1842                if !ids.is_empty()
1843                    && buffer
1844                        .diff_for(base_text_buffer.read(cx).remote_id())
1845                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1846                {
1847                    buffer.add_inverted_diff(diff, cx);
1848                }
1849            })
1850        });
1851
1852        let lhs_excerpt_ids: Vec<ExcerptId> = self
1853            .multibuffer
1854            .read(cx)
1855            .excerpts_for_path(&path_key)
1856            .collect();
1857
1858        debug_assert_eq!(rhs_excerpt_ids.len(), lhs_excerpt_ids.len());
1859
1860        lhs_excerpt_ids.into_iter().zip(rhs_excerpt_ids).collect()
1861    }
1862
1863    fn sync_path_excerpts(
1864        &mut self,
1865        path_key: PathKey,
1866        rhs_multibuffer: &Entity<MultiBuffer>,
1867        diff: Entity<BufferDiff>,
1868        rhs_display_map: &Entity<DisplayMap>,
1869        lhs_display_map: &Entity<DisplayMap>,
1870        cx: &mut App,
1871    ) {
1872        self.remove_mappings_for_path(
1873            &path_key,
1874            rhs_multibuffer,
1875            rhs_display_map,
1876            lhs_display_map,
1877            cx,
1878        );
1879
1880        let mappings =
1881            self.update_path_excerpts_from_rhs(path_key, rhs_multibuffer, diff.clone(), cx);
1882
1883        let lhs_buffer_id = diff.read(cx).base_text(cx).remote_id();
1884        let rhs_buffer_id = diff.read(cx).buffer_id;
1885
1886        if let Some(companion) = rhs_display_map.read(cx).companion().cloned() {
1887            companion.update(cx, |c, _| {
1888                for (lhs, rhs) in mappings {
1889                    c.add_excerpt_mapping(lhs, rhs);
1890                }
1891                c.add_buffer_mapping(lhs_buffer_id, rhs_buffer_id);
1892            });
1893        }
1894    }
1895
1896    fn remove_mappings_for_path(
1897        &self,
1898        path_key: &PathKey,
1899        rhs_multibuffer: &Entity<MultiBuffer>,
1900        rhs_display_map: &Entity<DisplayMap>,
1901        _lhs_display_map: &Entity<DisplayMap>,
1902        cx: &mut App,
1903    ) {
1904        let rhs_excerpt_ids: Vec<ExcerptId> = rhs_multibuffer
1905            .read(cx)
1906            .excerpts_for_path(path_key)
1907            .collect();
1908        let lhs_excerpt_ids: Vec<ExcerptId> = self
1909            .multibuffer
1910            .read(cx)
1911            .excerpts_for_path(path_key)
1912            .collect();
1913
1914        if let Some(companion) = rhs_display_map.read(cx).companion().cloned() {
1915            companion.update(cx, |c, _| {
1916                c.remove_excerpt_mappings(lhs_excerpt_ids, rhs_excerpt_ids);
1917            });
1918        }
1919    }
1920}
1921
1922#[cfg(test)]
1923mod tests {
1924    use buffer_diff::BufferDiff;
1925    use fs::FakeFs;
1926    use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
1927    use language::language_settings::SoftWrap;
1928    use language::{Buffer, Capability};
1929    use multi_buffer::{MultiBuffer, PathKey};
1930    use pretty_assertions::assert_eq;
1931    use project::Project;
1932    use rand::rngs::StdRng;
1933    use settings::SettingsStore;
1934    use ui::{VisualContext as _, px};
1935    use workspace::Workspace;
1936
1937    use crate::SplittableEditor;
1938    use crate::test::editor_content_with_blocks_and_width;
1939
1940    async fn init_test(
1941        cx: &mut gpui::TestAppContext,
1942        soft_wrap: SoftWrap,
1943    ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
1944        cx.update(|cx| {
1945            let store = SettingsStore::test(cx);
1946            cx.set_global(store);
1947            theme::init(theme::LoadThemes::JustBase, cx);
1948            crate::init(cx);
1949        });
1950        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
1951        let (workspace, cx) =
1952            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1953        let rhs_multibuffer = cx.new(|cx| {
1954            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1955            multibuffer.set_all_diff_hunks_expanded(cx);
1956            multibuffer
1957        });
1958        let editor = cx.new_window_entity(|window, cx| {
1959            let mut editor = SplittableEditor::new_unsplit(
1960                rhs_multibuffer.clone(),
1961                project.clone(),
1962                workspace,
1963                window,
1964                cx,
1965            );
1966            editor.split(&Default::default(), window, cx);
1967            editor.rhs_editor.update(cx, |editor, cx| {
1968                editor.set_soft_wrap_mode(soft_wrap, cx);
1969            });
1970            editor
1971                .lhs
1972                .as_ref()
1973                .unwrap()
1974                .editor
1975                .update(cx, |editor, cx| {
1976                    editor.set_soft_wrap_mode(soft_wrap, cx);
1977                });
1978            editor
1979        });
1980        (editor, cx)
1981    }
1982
1983    fn buffer_with_diff(
1984        base_text: &str,
1985        current_text: &str,
1986        cx: &mut VisualTestContext,
1987    ) -> (Entity<Buffer>, Entity<BufferDiff>) {
1988        let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
1989        let diff = cx.new(|cx| {
1990            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
1991        });
1992        (buffer, diff)
1993    }
1994
1995    #[track_caller]
1996    fn assert_split_content(
1997        editor: &Entity<SplittableEditor>,
1998        expected_rhs: String,
1999        expected_lhs: String,
2000        cx: &mut VisualTestContext,
2001    ) {
2002        assert_split_content_with_widths(
2003            editor,
2004            px(3000.0),
2005            px(3000.0),
2006            expected_rhs,
2007            expected_lhs,
2008            cx,
2009        );
2010    }
2011
2012    #[track_caller]
2013    fn assert_split_content_with_widths(
2014        editor: &Entity<SplittableEditor>,
2015        rhs_width: Pixels,
2016        lhs_width: Pixels,
2017        expected_rhs: String,
2018        expected_lhs: String,
2019        cx: &mut VisualTestContext,
2020    ) {
2021        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2022            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2023            (editor.rhs_editor.clone(), lhs.editor.clone())
2024        });
2025
2026        // Make sure both sides learn if the other has soft-wrapped
2027        let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2028        cx.run_until_parked();
2029        let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2030        cx.run_until_parked();
2031
2032        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2033        let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2034
2035        if rhs_content != expected_rhs || lhs_content != expected_lhs {
2036            editor.update(cx, |editor, cx| editor.debug_print(cx));
2037        }
2038
2039        assert_eq!(rhs_content, expected_rhs, "rhs");
2040        assert_eq!(lhs_content, expected_lhs, "lhs");
2041    }
2042
2043    #[gpui::test(iterations = 100)]
2044    async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2045        use rand::prelude::*;
2046
2047        let (editor, cx) = init_test(cx, SoftWrap::EditorWidth).await;
2048        let operations = std::env::var("OPERATIONS")
2049            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2050            .unwrap_or(10);
2051        let rng = &mut rng;
2052        for _ in 0..operations {
2053            let buffers = editor.update(cx, |editor, cx| {
2054                editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2055            });
2056
2057            if buffers.is_empty() {
2058                log::info!("adding excerpts to empty multibuffer");
2059                editor.update(cx, |editor, cx| {
2060                    editor.randomly_edit_excerpts(rng, 2, cx);
2061                    editor.check_invariants(true, cx);
2062                });
2063                continue;
2064            }
2065
2066            let mut quiesced = false;
2067
2068            match rng.random_range(0..100) {
2069                0..=44 => {
2070                    log::info!("randomly editing multibuffer");
2071                    editor.update(cx, |editor, cx| {
2072                        editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2073                            multibuffer.randomly_edit(rng, 5, cx);
2074                        })
2075                    })
2076                }
2077                45..=64 => {
2078                    log::info!("randomly undoing/redoing in single buffer");
2079                    let buffer = buffers.iter().choose(rng).unwrap();
2080                    buffer.update(cx, |buffer, cx| {
2081                        buffer.randomly_undo_redo(rng, cx);
2082                    });
2083                }
2084                65..=79 => {
2085                    log::info!("mutating excerpts");
2086                    editor.update(cx, |editor, cx| {
2087                        editor.randomly_edit_excerpts(rng, 2, cx);
2088                    });
2089                }
2090                _ => {
2091                    log::info!("quiescing");
2092                    for buffer in buffers {
2093                        let buffer_snapshot =
2094                            buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2095                        let diff = editor.update(cx, |editor, cx| {
2096                            editor
2097                                .rhs_multibuffer
2098                                .read(cx)
2099                                .diff_for(buffer.read(cx).remote_id())
2100                                .unwrap()
2101                        });
2102                        diff.update(cx, |diff, cx| {
2103                            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2104                        });
2105                        cx.run_until_parked();
2106                        let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2107                        let ranges = diff_snapshot
2108                            .hunks(&buffer_snapshot)
2109                            .map(|hunk| hunk.range)
2110                            .collect::<Vec<_>>();
2111                        editor.update(cx, |editor, cx| {
2112                            let path = PathKey::for_buffer(&buffer, cx);
2113                            editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2114                        });
2115                    }
2116                    quiesced = true;
2117                }
2118            }
2119
2120            editor.update(cx, |editor, cx| {
2121                editor.check_invariants(quiesced, cx);
2122            });
2123        }
2124    }
2125
2126    #[gpui::test]
2127    async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2128        use rope::Point;
2129        use unindent::Unindent as _;
2130
2131        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2132
2133        let base_text = "
2134            aaa
2135            bbb
2136            ccc
2137            ddd
2138            eee
2139            fff
2140        "
2141        .unindent();
2142        let current_text = "
2143            aaa
2144            ddd
2145            eee
2146            fff
2147        "
2148        .unindent();
2149
2150        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2151
2152        editor.update(cx, |editor, cx| {
2153            let path = PathKey::for_buffer(&buffer, cx);
2154            editor.set_excerpts_for_path(
2155                path,
2156                buffer.clone(),
2157                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2158                0,
2159                diff.clone(),
2160                cx,
2161            );
2162        });
2163
2164        cx.run_until_parked();
2165
2166        assert_split_content(
2167            &editor,
2168            "
2169            § <no file>
2170            § -----
2171            aaa
2172            § spacer
2173            § spacer
2174            ddd
2175            eee
2176            fff"
2177            .unindent(),
2178            "
2179            § <no file>
2180            § -----
2181            aaa
2182            bbb
2183            ccc
2184            ddd
2185            eee
2186            fff"
2187            .unindent(),
2188            &mut cx,
2189        );
2190
2191        buffer.update(cx, |buffer, cx| {
2192            buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2193        });
2194
2195        cx.run_until_parked();
2196
2197        assert_split_content(
2198            &editor,
2199            "
2200            § <no file>
2201            § -----
2202            aaa
2203            § spacer
2204            § spacer
2205            ddd
2206            eee
2207            FFF"
2208            .unindent(),
2209            "
2210            § <no file>
2211            § -----
2212            aaa
2213            bbb
2214            ccc
2215            ddd
2216            eee
2217            fff"
2218            .unindent(),
2219            &mut cx,
2220        );
2221
2222        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2223        diff.update(cx, |diff, cx| {
2224            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2225        });
2226
2227        cx.run_until_parked();
2228
2229        assert_split_content(
2230            &editor,
2231            "
2232            § <no file>
2233            § -----
2234            aaa
2235            § spacer
2236            § spacer
2237            ddd
2238            eee
2239            FFF"
2240            .unindent(),
2241            "
2242            § <no file>
2243            § -----
2244            aaa
2245            bbb
2246            ccc
2247            ddd
2248            eee
2249            fff"
2250            .unindent(),
2251            &mut cx,
2252        );
2253    }
2254
2255    #[gpui::test]
2256    async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2257        use rope::Point;
2258        use unindent::Unindent as _;
2259
2260        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2261
2262        let base_text1 = "
2263            aaa
2264            bbb
2265            ccc
2266            ddd
2267            eee"
2268        .unindent();
2269
2270        let base_text2 = "
2271            fff
2272            ggg
2273            hhh
2274            iii
2275            jjj"
2276        .unindent();
2277
2278        let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2279        let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2280
2281        editor.update(cx, |editor, cx| {
2282            let path1 = PathKey::for_buffer(&buffer1, cx);
2283            editor.set_excerpts_for_path(
2284                path1,
2285                buffer1.clone(),
2286                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2287                0,
2288                diff1.clone(),
2289                cx,
2290            );
2291            let path2 = PathKey::for_buffer(&buffer2, cx);
2292            editor.set_excerpts_for_path(
2293                path2,
2294                buffer2.clone(),
2295                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2296                1,
2297                diff2.clone(),
2298                cx,
2299            );
2300        });
2301
2302        cx.run_until_parked();
2303
2304        buffer1.update(cx, |buffer, cx| {
2305            buffer.edit(
2306                [
2307                    (Point::new(0, 0)..Point::new(1, 0), ""),
2308                    (Point::new(3, 0)..Point::new(4, 0), ""),
2309                ],
2310                None,
2311                cx,
2312            );
2313        });
2314        buffer2.update(cx, |buffer, cx| {
2315            buffer.edit(
2316                [
2317                    (Point::new(0, 0)..Point::new(1, 0), ""),
2318                    (Point::new(3, 0)..Point::new(4, 0), ""),
2319                ],
2320                None,
2321                cx,
2322            );
2323        });
2324
2325        cx.run_until_parked();
2326
2327        assert_split_content(
2328            &editor,
2329            "
2330            § <no file>
2331            § -----
2332            § spacer
2333            bbb
2334            ccc
2335            § spacer
2336            eee
2337            § <no file>
2338            § -----
2339            § spacer
2340            ggg
2341            hhh
2342            § spacer
2343            jjj"
2344            .unindent(),
2345            "
2346            § <no file>
2347            § -----
2348            aaa
2349            bbb
2350            ccc
2351            ddd
2352            eee
2353            § <no file>
2354            § -----
2355            fff
2356            ggg
2357            hhh
2358            iii
2359            jjj"
2360            .unindent(),
2361            &mut cx,
2362        );
2363
2364        let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2365        diff1.update(cx, |diff, cx| {
2366            diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2367        });
2368        let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2369        diff2.update(cx, |diff, cx| {
2370            diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2371        });
2372
2373        cx.run_until_parked();
2374
2375        assert_split_content(
2376            &editor,
2377            "
2378            § <no file>
2379            § -----
2380            § spacer
2381            bbb
2382            ccc
2383            § spacer
2384            eee
2385            § <no file>
2386            § -----
2387            § spacer
2388            ggg
2389            hhh
2390            § spacer
2391            jjj"
2392            .unindent(),
2393            "
2394            § <no file>
2395            § -----
2396            aaa
2397            bbb
2398            ccc
2399            ddd
2400            eee
2401            § <no file>
2402            § -----
2403            fff
2404            ggg
2405            hhh
2406            iii
2407            jjj"
2408            .unindent(),
2409            &mut cx,
2410        );
2411    }
2412
2413    #[gpui::test]
2414    async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2415        use rope::Point;
2416        use unindent::Unindent as _;
2417
2418        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2419
2420        let base_text = "
2421            aaa
2422            bbb
2423            ccc
2424            ddd
2425        "
2426        .unindent();
2427
2428        let current_text = "
2429            aaa
2430            NEW1
2431            NEW2
2432            ccc
2433            ddd
2434        "
2435        .unindent();
2436
2437        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2438
2439        editor.update(cx, |editor, cx| {
2440            let path = PathKey::for_buffer(&buffer, cx);
2441            editor.set_excerpts_for_path(
2442                path,
2443                buffer.clone(),
2444                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2445                0,
2446                diff.clone(),
2447                cx,
2448            );
2449        });
2450
2451        cx.run_until_parked();
2452
2453        assert_split_content(
2454            &editor,
2455            "
2456            § <no file>
2457            § -----
2458            aaa
2459            NEW1
2460            NEW2
2461            ccc
2462            ddd"
2463            .unindent(),
2464            "
2465            § <no file>
2466            § -----
2467            aaa
2468            bbb
2469            § spacer
2470            ccc
2471            ddd"
2472            .unindent(),
2473            &mut cx,
2474        );
2475
2476        buffer.update(cx, |buffer, cx| {
2477            buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2478        });
2479
2480        cx.run_until_parked();
2481
2482        assert_split_content(
2483            &editor,
2484            "
2485            § <no file>
2486            § -----
2487            aaa
2488            NEW1
2489            ccc
2490            ddd"
2491            .unindent(),
2492            "
2493            § <no file>
2494            § -----
2495            aaa
2496            bbb
2497            ccc
2498            ddd"
2499            .unindent(),
2500            &mut cx,
2501        );
2502
2503        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2504        diff.update(cx, |diff, cx| {
2505            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2506        });
2507
2508        cx.run_until_parked();
2509
2510        assert_split_content(
2511            &editor,
2512            "
2513            § <no file>
2514            § -----
2515            aaa
2516            NEW1
2517            ccc
2518            ddd"
2519            .unindent(),
2520            "
2521            § <no file>
2522            § -----
2523            aaa
2524            bbb
2525            ccc
2526            ddd"
2527            .unindent(),
2528            &mut cx,
2529        );
2530    }
2531
2532    #[gpui::test]
2533    async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2534        use rope::Point;
2535        use unindent::Unindent as _;
2536
2537        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2538
2539        let base_text = "
2540            aaa
2541            bbb
2542
2543
2544
2545
2546
2547            ccc
2548            ddd
2549        "
2550        .unindent();
2551        let current_text = "
2552            aaa
2553            bbb
2554
2555
2556
2557
2558
2559            CCC
2560            ddd
2561        "
2562        .unindent();
2563
2564        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2565
2566        editor.update(cx, |editor, cx| {
2567            let path = PathKey::for_buffer(&buffer, cx);
2568            editor.set_excerpts_for_path(
2569                path,
2570                buffer.clone(),
2571                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2572                0,
2573                diff.clone(),
2574                cx,
2575            );
2576        });
2577
2578        cx.run_until_parked();
2579
2580        buffer.update(cx, |buffer, cx| {
2581            buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2582        });
2583
2584        cx.run_until_parked();
2585
2586        assert_split_content(
2587            &editor,
2588            "
2589            § <no file>
2590            § -----
2591            aaa
2592            bbb
2593
2594
2595
2596
2597
2598
2599            CCC
2600            ddd"
2601            .unindent(),
2602            "
2603            § <no file>
2604            § -----
2605            aaa
2606            bbb
2607            § spacer
2608
2609
2610
2611
2612
2613            ccc
2614            ddd"
2615            .unindent(),
2616            &mut cx,
2617        );
2618
2619        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2620        diff.update(cx, |diff, cx| {
2621            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2622        });
2623
2624        cx.run_until_parked();
2625
2626        assert_split_content(
2627            &editor,
2628            "
2629            § <no file>
2630            § -----
2631            aaa
2632            bbb
2633
2634
2635
2636
2637
2638
2639            CCC
2640            ddd"
2641            .unindent(),
2642            "
2643            § <no file>
2644            § -----
2645            aaa
2646            bbb
2647
2648
2649
2650
2651
2652            ccc
2653            § spacer
2654            ddd"
2655            .unindent(),
2656            &mut cx,
2657        );
2658    }
2659
2660    #[gpui::test]
2661    async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2662        use git::Restore;
2663        use rope::Point;
2664        use unindent::Unindent as _;
2665
2666        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2667
2668        let base_text = "
2669            aaa
2670            bbb
2671            ccc
2672            ddd
2673            eee
2674        "
2675        .unindent();
2676        let current_text = "
2677            aaa
2678            ddd
2679            eee
2680        "
2681        .unindent();
2682
2683        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2684
2685        editor.update(cx, |editor, cx| {
2686            let path = PathKey::for_buffer(&buffer, cx);
2687            editor.set_excerpts_for_path(
2688                path,
2689                buffer.clone(),
2690                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2691                0,
2692                diff.clone(),
2693                cx,
2694            );
2695        });
2696
2697        cx.run_until_parked();
2698
2699        assert_split_content(
2700            &editor,
2701            "
2702            § <no file>
2703            § -----
2704            aaa
2705            § spacer
2706            § spacer
2707            ddd
2708            eee"
2709            .unindent(),
2710            "
2711            § <no file>
2712            § -----
2713            aaa
2714            bbb
2715            ccc
2716            ddd
2717            eee"
2718            .unindent(),
2719            &mut cx,
2720        );
2721
2722        let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
2723        cx.update_window_entity(&rhs_editor, |editor, window, cx| {
2724            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2725                s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2726            });
2727            editor.git_restore(&Restore, window, cx);
2728        });
2729
2730        cx.run_until_parked();
2731
2732        assert_split_content(
2733            &editor,
2734            "
2735            § <no file>
2736            § -----
2737            aaa
2738            bbb
2739            ccc
2740            ddd
2741            eee"
2742            .unindent(),
2743            "
2744            § <no file>
2745            § -----
2746            aaa
2747            bbb
2748            ccc
2749            ddd
2750            eee"
2751            .unindent(),
2752            &mut cx,
2753        );
2754
2755        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2756        diff.update(cx, |diff, cx| {
2757            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2758        });
2759
2760        cx.run_until_parked();
2761
2762        assert_split_content(
2763            &editor,
2764            "
2765            § <no file>
2766            § -----
2767            aaa
2768            bbb
2769            ccc
2770            ddd
2771            eee"
2772            .unindent(),
2773            "
2774            § <no file>
2775            § -----
2776            aaa
2777            bbb
2778            ccc
2779            ddd
2780            eee"
2781            .unindent(),
2782            &mut cx,
2783        );
2784    }
2785
2786    #[gpui::test]
2787    async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
2788        use rope::Point;
2789        use unindent::Unindent as _;
2790
2791        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2792
2793        let base_text = "
2794            aaa
2795            old1
2796            old2
2797            old3
2798            old4
2799            zzz
2800        "
2801        .unindent();
2802
2803        let current_text = "
2804            aaa
2805            new1
2806            new2
2807            new3
2808            new4
2809            zzz
2810        "
2811        .unindent();
2812
2813        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2814
2815        editor.update(cx, |editor, cx| {
2816            let path = PathKey::for_buffer(&buffer, cx);
2817            editor.set_excerpts_for_path(
2818                path,
2819                buffer.clone(),
2820                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2821                0,
2822                diff.clone(),
2823                cx,
2824            );
2825        });
2826
2827        cx.run_until_parked();
2828
2829        buffer.update(cx, |buffer, cx| {
2830            buffer.edit(
2831                [
2832                    (Point::new(2, 0)..Point::new(3, 0), ""),
2833                    (Point::new(4, 0)..Point::new(5, 0), ""),
2834                ],
2835                None,
2836                cx,
2837            );
2838        });
2839        cx.run_until_parked();
2840
2841        assert_split_content(
2842            &editor,
2843            "
2844            § <no file>
2845            § -----
2846            aaa
2847            new1
2848            new3
2849            § spacer
2850            § spacer
2851            zzz"
2852            .unindent(),
2853            "
2854            § <no file>
2855            § -----
2856            aaa
2857            old1
2858            old2
2859            old3
2860            old4
2861            zzz"
2862            .unindent(),
2863            &mut cx,
2864        );
2865
2866        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2867        diff.update(cx, |diff, cx| {
2868            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2869        });
2870
2871        cx.run_until_parked();
2872
2873        assert_split_content(
2874            &editor,
2875            "
2876            § <no file>
2877            § -----
2878            aaa
2879            new1
2880            new3
2881            § spacer
2882            § spacer
2883            zzz"
2884            .unindent(),
2885            "
2886            § <no file>
2887            § -----
2888            aaa
2889            old1
2890            old2
2891            old3
2892            old4
2893            zzz"
2894            .unindent(),
2895            &mut cx,
2896        );
2897    }
2898
2899    #[gpui::test]
2900    async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
2901        use rope::Point;
2902        use unindent::Unindent as _;
2903
2904        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2905
2906        let text = "aaaa bbbb cccc dddd eeee ffff";
2907
2908        let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
2909        let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
2910
2911        editor.update(cx, |editor, cx| {
2912            let end = Point::new(0, text.len() as u32);
2913            let path1 = PathKey::for_buffer(&buffer1, cx);
2914            editor.set_excerpts_for_path(
2915                path1,
2916                buffer1.clone(),
2917                vec![Point::new(0, 0)..end],
2918                0,
2919                diff1.clone(),
2920                cx,
2921            );
2922            let path2 = PathKey::for_buffer(&buffer2, cx);
2923            editor.set_excerpts_for_path(
2924                path2,
2925                buffer2.clone(),
2926                vec![Point::new(0, 0)..end],
2927                0,
2928                diff2.clone(),
2929                cx,
2930            );
2931        });
2932
2933        cx.run_until_parked();
2934
2935        assert_split_content_with_widths(
2936            &editor,
2937            px(200.0),
2938            px(400.0),
2939            "
2940            § <no file>
2941            § -----
2942            aaaa bbbb\x20
2943            cccc dddd\x20
2944            eeee ffff
2945            § <no file>
2946            § -----
2947            aaaa bbbb\x20
2948            cccc dddd\x20
2949            eeee ffff"
2950                .unindent(),
2951            "
2952            § <no file>
2953            § -----
2954            aaaa bbbb cccc dddd eeee ffff
2955            § spacer
2956            § spacer
2957            § <no file>
2958            § -----
2959            aaaa bbbb cccc dddd eeee ffff
2960            § spacer
2961            § spacer"
2962                .unindent(),
2963            &mut cx,
2964        );
2965    }
2966
2967    #[gpui::test]
2968    async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
2969        use rope::Point;
2970        use unindent::Unindent as _;
2971
2972        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2973
2974        let base_text = "
2975            aaaa bbbb cccc dddd eeee ffff
2976            old line one
2977            old line two
2978        "
2979        .unindent();
2980
2981        let current_text = "
2982            aaaa bbbb cccc dddd eeee ffff
2983            new line
2984        "
2985        .unindent();
2986
2987        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2988
2989        editor.update(cx, |editor, cx| {
2990            let path = PathKey::for_buffer(&buffer, cx);
2991            editor.set_excerpts_for_path(
2992                path,
2993                buffer.clone(),
2994                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2995                0,
2996                diff.clone(),
2997                cx,
2998            );
2999        });
3000
3001        cx.run_until_parked();
3002
3003        assert_split_content_with_widths(
3004            &editor,
3005            px(200.0),
3006            px(400.0),
3007            "
3008            § <no file>
3009            § -----
3010            aaaa bbbb\x20
3011            cccc dddd\x20
3012            eeee ffff
3013            new line
3014            § spacer"
3015                .unindent(),
3016            "
3017            § <no file>
3018            § -----
3019            aaaa bbbb cccc dddd eeee ffff
3020            § spacer
3021            § spacer
3022            old line one
3023            old line two"
3024                .unindent(),
3025            &mut cx,
3026        );
3027    }
3028
3029    #[gpui::test]
3030    async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3031        use rope::Point;
3032        use unindent::Unindent as _;
3033
3034        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3035
3036        let base_text = "
3037            aaaa bbbb cccc dddd eeee ffff
3038            deleted line one
3039            deleted line two
3040            after
3041        "
3042        .unindent();
3043
3044        let current_text = "
3045            aaaa bbbb cccc dddd eeee ffff
3046            after
3047        "
3048        .unindent();
3049
3050        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3051
3052        editor.update(cx, |editor, cx| {
3053            let path = PathKey::for_buffer(&buffer, cx);
3054            editor.set_excerpts_for_path(
3055                path,
3056                buffer.clone(),
3057                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3058                0,
3059                diff.clone(),
3060                cx,
3061            );
3062        });
3063
3064        cx.run_until_parked();
3065
3066        assert_split_content_with_widths(
3067            &editor,
3068            px(400.0),
3069            px(200.0),
3070            "
3071            § <no file>
3072            § -----
3073            aaaa bbbb cccc dddd eeee ffff
3074            § spacer
3075            § spacer
3076            § spacer
3077            § spacer
3078            § spacer
3079            § spacer
3080            after"
3081                .unindent(),
3082            "
3083            § <no file>
3084            § -----
3085            aaaa bbbb\x20
3086            cccc dddd\x20
3087            eeee ffff
3088            deleted line\x20
3089            one
3090            deleted line\x20
3091            two
3092            after"
3093                .unindent(),
3094            &mut cx,
3095        );
3096    }
3097
3098    #[gpui::test]
3099    async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3100        use rope::Point;
3101        use unindent::Unindent as _;
3102
3103        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3104
3105        let text = "
3106            aaaa bbbb cccc dddd eeee ffff
3107            short
3108        "
3109        .unindent();
3110
3111        let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3112
3113        editor.update(cx, |editor, cx| {
3114            let path = PathKey::for_buffer(&buffer, cx);
3115            editor.set_excerpts_for_path(
3116                path,
3117                buffer.clone(),
3118                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3119                0,
3120                diff.clone(),
3121                cx,
3122            );
3123        });
3124
3125        cx.run_until_parked();
3126
3127        assert_split_content_with_widths(
3128            &editor,
3129            px(400.0),
3130            px(200.0),
3131            "
3132            § <no file>
3133            § -----
3134            aaaa bbbb cccc dddd eeee ffff
3135            § spacer
3136            § spacer
3137            short"
3138                .unindent(),
3139            "
3140            § <no file>
3141            § -----
3142            aaaa bbbb\x20
3143            cccc dddd\x20
3144            eeee ffff
3145            short"
3146                .unindent(),
3147            &mut cx,
3148        );
3149
3150        buffer.update(cx, |buffer, cx| {
3151            buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3152        });
3153
3154        cx.run_until_parked();
3155
3156        assert_split_content_with_widths(
3157            &editor,
3158            px(400.0),
3159            px(200.0),
3160            "
3161            § <no file>
3162            § -----
3163            aaaa bbbb cccc dddd eeee ffff
3164            § spacer
3165            § spacer
3166            modified"
3167                .unindent(),
3168            "
3169            § <no file>
3170            § -----
3171            aaaa bbbb\x20
3172            cccc dddd\x20
3173            eeee ffff
3174            short"
3175                .unindent(),
3176            &mut cx,
3177        );
3178
3179        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3180        diff.update(cx, |diff, cx| {
3181            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3182        });
3183
3184        cx.run_until_parked();
3185
3186        assert_split_content_with_widths(
3187            &editor,
3188            px(400.0),
3189            px(200.0),
3190            "
3191            § <no file>
3192            § -----
3193            aaaa bbbb cccc dddd eeee ffff
3194            § spacer
3195            § spacer
3196            modified"
3197                .unindent(),
3198            "
3199            § <no file>
3200            § -----
3201            aaaa bbbb\x20
3202            cccc dddd\x20
3203            eeee ffff
3204            short"
3205                .unindent(),
3206            &mut cx,
3207        );
3208    }
3209
3210    #[gpui::test]
3211    async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3212        use rope::Point;
3213        use unindent::Unindent as _;
3214
3215        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3216
3217        let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3218
3219        let current_text = "
3220            aaa
3221            bbb
3222            ccc
3223        "
3224        .unindent();
3225
3226        let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3227        let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3228
3229        editor.update(cx, |editor, cx| {
3230            let path1 = PathKey::for_buffer(&buffer1, cx);
3231            editor.set_excerpts_for_path(
3232                path1,
3233                buffer1.clone(),
3234                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3235                0,
3236                diff1.clone(),
3237                cx,
3238            );
3239
3240            let path2 = PathKey::for_buffer(&buffer2, cx);
3241            editor.set_excerpts_for_path(
3242                path2,
3243                buffer2.clone(),
3244                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3245                1,
3246                diff2.clone(),
3247                cx,
3248            );
3249        });
3250
3251        cx.run_until_parked();
3252
3253        assert_split_content(
3254            &editor,
3255            "
3256            § <no file>
3257            § -----
3258            xxx
3259            yyy
3260            § <no file>
3261            § -----
3262            aaa
3263            bbb
3264            ccc"
3265            .unindent(),
3266            "
3267            § <no file>
3268            § -----
3269            xxx
3270            yyy
3271            § <no file>
3272            § -----
3273            § spacer
3274            § spacer
3275            § spacer"
3276                .unindent(),
3277            &mut cx,
3278        );
3279
3280        buffer1.update(cx, |buffer, cx| {
3281            buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3282        });
3283
3284        cx.run_until_parked();
3285
3286        assert_split_content(
3287            &editor,
3288            "
3289            § <no file>
3290            § -----
3291            xxxz
3292            yyy
3293            § <no file>
3294            § -----
3295            aaa
3296            bbb
3297            ccc"
3298            .unindent(),
3299            "
3300            § <no file>
3301            § -----
3302            xxx
3303            yyy
3304            § <no file>
3305            § -----
3306            § spacer
3307            § spacer
3308            § spacer"
3309                .unindent(),
3310            &mut cx,
3311        );
3312    }
3313
3314    #[gpui::test]
3315    async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3316        use rope::Point;
3317        use unindent::Unindent as _;
3318
3319        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3320
3321        let base_text = "
3322            aaa
3323            bbb
3324            ccc
3325        "
3326        .unindent();
3327
3328        let current_text = "
3329            NEW1
3330            NEW2
3331            ccc
3332        "
3333        .unindent();
3334
3335        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3336
3337        editor.update(cx, |editor, cx| {
3338            let path = PathKey::for_buffer(&buffer, cx);
3339            editor.set_excerpts_for_path(
3340                path,
3341                buffer.clone(),
3342                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3343                0,
3344                diff.clone(),
3345                cx,
3346            );
3347        });
3348
3349        cx.run_until_parked();
3350
3351        assert_split_content(
3352            &editor,
3353            "
3354            § <no file>
3355            § -----
3356            NEW1
3357            NEW2
3358            ccc"
3359            .unindent(),
3360            "
3361            § <no file>
3362            § -----
3363            aaa
3364            bbb
3365            ccc"
3366            .unindent(),
3367            &mut cx,
3368        );
3369
3370        buffer.update(cx, |buffer, cx| {
3371            buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3372        });
3373
3374        cx.run_until_parked();
3375
3376        assert_split_content(
3377            &editor,
3378            "
3379            § <no file>
3380            § -----
3381            NEW1
3382            NEW
3383            ccc"
3384            .unindent(),
3385            "
3386            § <no file>
3387            § -----
3388            aaa
3389            bbb
3390            ccc"
3391            .unindent(),
3392            &mut cx,
3393        );
3394    }
3395
3396    #[gpui::test]
3397    async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3398        use rope::Point;
3399        use unindent::Unindent as _;
3400
3401        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3402
3403        let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3404
3405        let current_text = "
3406            aaaa bbbb cccc dddd eeee ffff
3407            added line
3408        "
3409        .unindent();
3410
3411        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3412
3413        editor.update(cx, |editor, cx| {
3414            let path = PathKey::for_buffer(&buffer, cx);
3415            editor.set_excerpts_for_path(
3416                path,
3417                buffer.clone(),
3418                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3419                0,
3420                diff.clone(),
3421                cx,
3422            );
3423        });
3424
3425        cx.run_until_parked();
3426
3427        assert_split_content_with_widths(
3428            &editor,
3429            px(400.0),
3430            px(200.0),
3431            "
3432            § <no file>
3433            § -----
3434            aaaa bbbb cccc dddd eeee ffff
3435            § spacer
3436            § spacer
3437            added line"
3438                .unindent(),
3439            "
3440            § <no file>
3441            § -----
3442            aaaa bbbb\x20
3443            cccc dddd\x20
3444            eeee ffff
3445            § spacer"
3446                .unindent(),
3447            &mut cx,
3448        );
3449
3450        assert_split_content_with_widths(
3451            &editor,
3452            px(200.0),
3453            px(400.0),
3454            "
3455            § <no file>
3456            § -----
3457            aaaa bbbb\x20
3458            cccc dddd\x20
3459            eeee ffff
3460            added line"
3461                .unindent(),
3462            "
3463            § <no file>
3464            § -----
3465            aaaa bbbb cccc dddd eeee ffff
3466            § spacer
3467            § spacer
3468            § spacer"
3469                .unindent(),
3470            &mut cx,
3471        );
3472    }
3473
3474    #[gpui::test]
3475    #[ignore]
3476    async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3477        use rope::Point;
3478        use unindent::Unindent as _;
3479
3480        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3481
3482        let base_text = "
3483            aaa
3484            bbb
3485            ccc
3486            ddd
3487            eee
3488        "
3489        .unindent();
3490
3491        let current_text = "
3492            aaa
3493            NEW
3494            eee
3495        "
3496        .unindent();
3497
3498        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3499
3500        editor.update(cx, |editor, cx| {
3501            let path = PathKey::for_buffer(&buffer, cx);
3502            editor.set_excerpts_for_path(
3503                path,
3504                buffer.clone(),
3505                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3506                0,
3507                diff.clone(),
3508                cx,
3509            );
3510        });
3511
3512        cx.run_until_parked();
3513
3514        assert_split_content(
3515            &editor,
3516            "
3517            § <no file>
3518            § -----
3519            aaa
3520            NEW
3521            § spacer
3522            § spacer
3523            eee"
3524            .unindent(),
3525            "
3526            § <no file>
3527            § -----
3528            aaa
3529            bbb
3530            ccc
3531            ddd
3532            eee"
3533            .unindent(),
3534            &mut cx,
3535        );
3536
3537        buffer.update(cx, |buffer, cx| {
3538            buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3539        });
3540
3541        cx.run_until_parked();
3542
3543        assert_split_content(
3544            &editor,
3545            "
3546            § <no file>
3547            § -----
3548            aaa
3549            § spacer
3550            § spacer
3551            § spacer
3552            NEWeee"
3553                .unindent(),
3554            "
3555            § <no file>
3556            § -----
3557            aaa
3558            bbb
3559            ccc
3560            ddd
3561            eee"
3562            .unindent(),
3563            &mut cx,
3564        );
3565
3566        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3567        diff.update(cx, |diff, cx| {
3568            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3569        });
3570
3571        cx.run_until_parked();
3572
3573        assert_split_content(
3574            &editor,
3575            "
3576            § <no file>
3577            § -----
3578            aaa
3579            NEWeee
3580            § spacer
3581            § spacer
3582            § spacer"
3583                .unindent(),
3584            "
3585            § <no file>
3586            § -----
3587            aaa
3588            bbb
3589            ccc
3590            ddd
3591            eee"
3592            .unindent(),
3593            &mut cx,
3594        );
3595    }
3596
3597    #[gpui::test]
3598    async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3599        use rope::Point;
3600        use unindent::Unindent as _;
3601
3602        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3603
3604        let base_text = "";
3605        let current_text = "
3606            aaaa bbbb cccc dddd eeee ffff
3607            bbb
3608            ccc
3609        "
3610        .unindent();
3611
3612        let (buffer, diff) = buffer_with_diff(base_text, &current_text, &mut cx);
3613
3614        editor.update(cx, |editor, cx| {
3615            let path = PathKey::for_buffer(&buffer, cx);
3616            editor.set_excerpts_for_path(
3617                path,
3618                buffer.clone(),
3619                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3620                0,
3621                diff.clone(),
3622                cx,
3623            );
3624        });
3625
3626        cx.run_until_parked();
3627
3628        assert_split_content(
3629            &editor,
3630            "
3631            § <no file>
3632            § -----
3633            aaaa bbbb cccc dddd eeee ffff
3634            bbb
3635            ccc"
3636            .unindent(),
3637            "
3638            § <no file>
3639            § -----
3640            § spacer
3641            § spacer
3642            § spacer"
3643                .unindent(),
3644            &mut cx,
3645        );
3646
3647        assert_split_content_with_widths(
3648            &editor,
3649            px(200.0),
3650            px(200.0),
3651            "
3652            § <no file>
3653            § -----
3654            aaaa bbbb\x20
3655            cccc dddd\x20
3656            eeee ffff
3657            bbb
3658            ccc"
3659            .unindent(),
3660            "
3661            § <no file>
3662            § -----
3663            § spacer
3664            § spacer
3665            § spacer
3666            § spacer
3667            § spacer"
3668                .unindent(),
3669            &mut cx,
3670        );
3671    }
3672
3673    #[gpui::test]
3674    async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
3675        use rope::Point;
3676        use unindent::Unindent as _;
3677
3678        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3679
3680        let base_text = "
3681            aaa
3682            bbb
3683            ccc
3684        "
3685        .unindent();
3686
3687        let current_text = "
3688            aaa
3689            bbb
3690            xxx
3691            yyy
3692            ccc
3693        "
3694        .unindent();
3695
3696        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3697
3698        editor.update(cx, |editor, cx| {
3699            let path = PathKey::for_buffer(&buffer, cx);
3700            editor.set_excerpts_for_path(
3701                path,
3702                buffer.clone(),
3703                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3704                0,
3705                diff.clone(),
3706                cx,
3707            );
3708        });
3709
3710        cx.run_until_parked();
3711
3712        assert_split_content(
3713            &editor,
3714            "
3715            § <no file>
3716            § -----
3717            aaa
3718            bbb
3719            xxx
3720            yyy
3721            ccc"
3722            .unindent(),
3723            "
3724            § <no file>
3725            § -----
3726            aaa
3727            bbb
3728            § spacer
3729            § spacer
3730            ccc"
3731            .unindent(),
3732            &mut cx,
3733        );
3734
3735        buffer.update(cx, |buffer, cx| {
3736            buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
3737        });
3738
3739        cx.run_until_parked();
3740
3741        assert_split_content(
3742            &editor,
3743            "
3744            § <no file>
3745            § -----
3746            aaa
3747            bbb
3748            xxx
3749            yyy
3750            zzz
3751            ccc"
3752            .unindent(),
3753            "
3754            § <no file>
3755            § -----
3756            aaa
3757            bbb
3758            § spacer
3759            § spacer
3760            § spacer
3761            ccc"
3762            .unindent(),
3763            &mut cx,
3764        );
3765    }
3766
3767    #[gpui::test]
3768    async fn test_scrolling(cx: &mut gpui::TestAppContext) {
3769        use crate::test::editor_content_with_blocks_and_size;
3770        use gpui::size;
3771        use rope::Point;
3772
3773        let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
3774
3775        let long_line = "x".repeat(200);
3776        let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
3777        lines[25] = long_line;
3778        let content = lines.join("\n");
3779
3780        let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
3781
3782        editor.update(cx, |editor, cx| {
3783            let path = PathKey::for_buffer(&buffer, cx);
3784            editor.set_excerpts_for_path(
3785                path,
3786                buffer.clone(),
3787                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3788                0,
3789                diff.clone(),
3790                cx,
3791            );
3792        });
3793
3794        cx.run_until_parked();
3795
3796        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
3797            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
3798            (editor.rhs_editor.clone(), lhs.editor.clone())
3799        });
3800
3801        rhs_editor.update_in(cx, |e, window, cx| {
3802            e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
3803        });
3804
3805        let rhs_pos =
3806            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3807        let lhs_pos =
3808            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3809        assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
3810        assert_eq!(
3811            lhs_pos.y, rhs_pos.y,
3812            "LHS should have same scroll position as RHS after set_scroll_position"
3813        );
3814
3815        let draw_size = size(px(300.), px(300.));
3816
3817        rhs_editor.update_in(cx, |e, window, cx| {
3818            e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
3819                s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
3820            });
3821        });
3822
3823        let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
3824        cx.run_until_parked();
3825        let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
3826        cx.run_until_parked();
3827
3828        let rhs_pos =
3829            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3830        let lhs_pos =
3831            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3832
3833        assert!(
3834            rhs_pos.y > 0.,
3835            "RHS should have scrolled vertically to show cursor at row 25"
3836        );
3837        assert!(
3838            rhs_pos.x > 0.,
3839            "RHS should have scrolled horizontally to show cursor at column 150"
3840        );
3841        assert_eq!(
3842            lhs_pos.y, rhs_pos.y,
3843            "LHS should have same vertical scroll position as RHS after autoscroll"
3844        );
3845        assert_eq!(
3846            lhs_pos.x, rhs_pos.x,
3847            "LHS should have same horizontal scroll position as RHS after autoscroll"
3848        );
3849    }
3850}