split.rs

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