split.rs

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