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