split.rs

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