split.rs

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