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