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