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