split.rs

   1use std::ops::{Bound, Range};
   2
   3use buffer_diff::{BufferDiff, BufferDiffSnapshot};
   4use collections::HashMap;
   5use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
   6use gpui::{
   7    Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
   8};
   9use language::{Buffer, Capability};
  10use multi_buffer::{
  11    Anchor, BufferOffset, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer,
  12    MultiBufferPoint, MultiBufferSnapshot, PathKey,
  13};
  14use project::Project;
  15use rope::Point;
  16use text::{OffsetRangeExt as _, ToPoint as _};
  17use ui::{
  18    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
  19    Styled as _, Window, div,
  20};
  21
  22use crate::{
  23    display_map::MultiBufferRowMapping,
  24    split_editor_view::{SplitEditorState, SplitEditorView},
  25};
  26use workspace::{
  27    ActivatePaneLeft, ActivatePaneRight, Item, ItemHandle, Pane, PaneGroup, SplitDirection,
  28    Workspace,
  29};
  30
  31use crate::{
  32    Autoscroll, DisplayMap, Editor, EditorEvent, ToggleCodeActions, ToggleSoftWrap,
  33    actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
  34    display_map::Companion,
  35};
  36use zed_actions::assistant::InlineAssist;
  37
  38pub(crate) fn convert_lhs_rows_to_rhs(
  39    lhs_excerpt_to_rhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
  40    rhs_snapshot: &MultiBufferSnapshot,
  41    lhs_snapshot: &MultiBufferSnapshot,
  42    lhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
  43) -> Vec<MultiBufferRowMapping> {
  44    convert_rows(
  45        lhs_excerpt_to_rhs_excerpt,
  46        lhs_snapshot,
  47        rhs_snapshot,
  48        lhs_bounds,
  49        |diff, points, buffer| {
  50            let (points, first_group, prev_boundary) =
  51                diff.base_text_points_to_points(points, buffer);
  52            (points.collect(), first_group, prev_boundary)
  53        },
  54    )
  55}
  56
  57pub(crate) fn convert_rhs_rows_to_lhs(
  58    rhs_excerpt_to_lhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
  59    lhs_snapshot: &MultiBufferSnapshot,
  60    rhs_snapshot: &MultiBufferSnapshot,
  61    rhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
  62) -> Vec<MultiBufferRowMapping> {
  63    convert_rows(
  64        rhs_excerpt_to_lhs_excerpt,
  65        rhs_snapshot,
  66        lhs_snapshot,
  67        rhs_bounds,
  68        |diff, points, buffer| {
  69            let (points, first_group, prev_boundary) =
  70                diff.points_to_base_text_points(points, buffer);
  71            (points.collect(), first_group, prev_boundary)
  72        },
  73    )
  74}
  75
  76fn convert_rows<F>(
  77    excerpt_map: &HashMap<ExcerptId, ExcerptId>,
  78    source_snapshot: &MultiBufferSnapshot,
  79    target_snapshot: &MultiBufferSnapshot,
  80    source_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
  81    translate_fn: F,
  82) -> Vec<MultiBufferRowMapping>
  83where
  84    F: Fn(
  85        &BufferDiffSnapshot,
  86        Vec<Point>,
  87        &text::BufferSnapshot,
  88    ) -> (
  89        Vec<Range<Point>>,
  90        Option<Range<Point>>,
  91        Option<(Point, Range<Point>)>,
  92    ),
  93{
  94    let mut result = Vec::new();
  95
  96    for (buffer, buffer_offset_range, source_excerpt_id) in
  97        source_snapshot.range_to_buffer_ranges(source_bounds)
  98    {
  99        if let Some(translation) = convert_excerpt_rows(
 100            excerpt_map,
 101            source_snapshot,
 102            target_snapshot,
 103            source_excerpt_id,
 104            buffer,
 105            buffer_offset_range,
 106            &translate_fn,
 107        ) {
 108            result.push(translation);
 109        }
 110    }
 111
 112    result
 113}
 114
 115fn convert_excerpt_rows<F>(
 116    excerpt_map: &HashMap<ExcerptId, ExcerptId>,
 117    source_snapshot: &MultiBufferSnapshot,
 118    target_snapshot: &MultiBufferSnapshot,
 119    source_excerpt_id: ExcerptId,
 120    source_buffer: &text::BufferSnapshot,
 121    source_buffer_range: Range<BufferOffset>,
 122    translate_fn: F,
 123) -> Option<MultiBufferRowMapping>
 124where
 125    F: Fn(
 126        &BufferDiffSnapshot,
 127        Vec<Point>,
 128        &text::BufferSnapshot,
 129    ) -> (
 130        Vec<Range<Point>>,
 131        Option<Range<Point>>,
 132        Option<(Point, Range<Point>)>,
 133    ),
 134{
 135    let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied()?;
 136    let target_buffer = target_snapshot.buffer_for_excerpt(target_excerpt_id)?;
 137
 138    let diff = source_snapshot.diff_for_buffer_id(source_buffer.remote_id())?;
 139    let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() {
 140        &target_buffer
 141    } else {
 142        source_buffer
 143    };
 144
 145    let local_start = source_buffer.offset_to_point(source_buffer_range.start.0);
 146    let local_end = source_buffer.offset_to_point(source_buffer_range.end.0);
 147
 148    let mut input_points: Vec<Point> = (local_start.row..=local_end.row)
 149        .map(|row| Point::new(row, 0))
 150        .collect();
 151    if local_end.column > 0 {
 152        input_points.push(local_end);
 153    }
 154
 155    let (translated_ranges, first_group, prev_boundary) =
 156        translate_fn(&diff, input_points.clone(), rhs_buffer);
 157
 158    let source_multibuffer_range = source_snapshot.range_for_excerpt(source_excerpt_id)?;
 159    let source_excerpt_start_in_multibuffer = source_multibuffer_range.start;
 160    let source_context_range = source_snapshot.context_range_for_excerpt(source_excerpt_id)?;
 161    let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer);
 162    let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer);
 163    let target_multibuffer_range = target_snapshot.range_for_excerpt(target_excerpt_id)?;
 164    let target_excerpt_start_in_multibuffer = target_multibuffer_range.start;
 165    let target_context_range = target_snapshot.context_range_for_excerpt(target_excerpt_id)?;
 166    let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer);
 167    let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer);
 168
 169    let boundaries: Vec<_> = input_points
 170        .into_iter()
 171        .zip(translated_ranges)
 172        .map(|(source_buffer_point, target_range)| {
 173            let source_multibuffer_point = source_excerpt_start_in_multibuffer
 174                + (source_buffer_point - source_excerpt_start_in_buffer.min(source_buffer_point));
 175
 176            let clamped_target_start = target_range
 177                .start
 178                .max(target_excerpt_start_in_buffer)
 179                .min(target_excerpt_end_in_buffer);
 180            let clamped_target_end = target_range
 181                .end
 182                .max(target_excerpt_start_in_buffer)
 183                .min(target_excerpt_end_in_buffer);
 184
 185            let target_multibuffer_start = target_excerpt_start_in_multibuffer
 186                + (clamped_target_start - target_excerpt_start_in_buffer);
 187
 188            let target_multibuffer_end = target_excerpt_start_in_multibuffer
 189                + (clamped_target_end - target_excerpt_start_in_buffer);
 190
 191            (
 192                source_multibuffer_point,
 193                target_multibuffer_start..target_multibuffer_end,
 194            )
 195        })
 196        .collect();
 197    let first_group = first_group.map(|first_group| {
 198        let start = source_excerpt_start_in_multibuffer
 199            + (first_group.start - source_excerpt_start_in_buffer.min(first_group.start));
 200        let end = source_excerpt_start_in_multibuffer
 201            + (first_group.end - source_excerpt_start_in_buffer.min(first_group.end));
 202        start..end
 203    });
 204
 205    let prev_boundary = prev_boundary.map(|(source_buffer_point, target_range)| {
 206        let source_multibuffer_point = source_excerpt_start_in_multibuffer
 207            + (source_buffer_point - source_excerpt_start_in_buffer.min(source_buffer_point));
 208
 209        let clamped_target_start = target_range
 210            .start
 211            .max(target_excerpt_start_in_buffer)
 212            .min(target_excerpt_end_in_buffer);
 213        let clamped_target_end = target_range
 214            .end
 215            .max(target_excerpt_start_in_buffer)
 216            .min(target_excerpt_end_in_buffer);
 217
 218        let target_multibuffer_start = target_excerpt_start_in_multibuffer
 219            + (clamped_target_start - target_excerpt_start_in_buffer);
 220        let target_multibuffer_end = target_excerpt_start_in_multibuffer
 221            + (clamped_target_end - target_excerpt_start_in_buffer);
 222
 223        (
 224            source_multibuffer_point,
 225            target_multibuffer_start..target_multibuffer_end,
 226        )
 227    });
 228
 229    Some(MultiBufferRowMapping {
 230        boundaries,
 231        first_group,
 232        prev_boundary,
 233        source_excerpt_end: source_excerpt_start_in_multibuffer
 234            + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer),
 235        target_excerpt_end: target_excerpt_start_in_multibuffer
 236            + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer),
 237    })
 238}
 239
 240struct SplitDiffFeatureFlag;
 241
 242impl FeatureFlag for SplitDiffFeatureFlag {
 243    const NAME: &'static str = "split-diff";
 244
 245    fn enabled_for_staff() -> bool {
 246        true
 247    }
 248}
 249
 250#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 251#[action(namespace = editor)]
 252struct SplitDiff;
 253
 254#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 255#[action(namespace = editor)]
 256struct UnsplitDiff;
 257
 258#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 259#[action(namespace = editor)]
 260pub struct ToggleSplitDiff;
 261
 262#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 263#[action(namespace = editor)]
 264struct JumpToCorrespondingRow;
 265
 266/// When locked cursors mode is enabled, cursor movements in one editor will
 267/// update the cursor position in the other editor to the corresponding row.
 268#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 269#[action(namespace = editor)]
 270pub struct ToggleLockedCursors;
 271
 272pub struct SplittableEditor {
 273    primary_multibuffer: Entity<MultiBuffer>,
 274    primary_editor: Entity<Editor>,
 275    secondary: Option<SecondaryEditor>,
 276    panes: PaneGroup,
 277    workspace: WeakEntity<Workspace>,
 278    split_state: Entity<SplitEditorState>,
 279    locked_cursors: bool,
 280    _subscriptions: Vec<Subscription>,
 281}
 282
 283struct SecondaryEditor {
 284    multibuffer: Entity<MultiBuffer>,
 285    editor: Entity<Editor>,
 286    pane: Entity<Pane>,
 287    has_latest_selection: bool,
 288    _subscriptions: Vec<Subscription>,
 289}
 290
 291impl SplittableEditor {
 292    pub fn primary_editor(&self) -> &Entity<Editor> {
 293        &self.primary_editor
 294    }
 295
 296    pub fn secondary_editor(&self) -> Option<&Entity<Editor>> {
 297        self.secondary.as_ref().map(|s| &s.editor)
 298    }
 299
 300    pub fn is_split(&self) -> bool {
 301        self.secondary.is_some()
 302    }
 303
 304    pub fn last_selected_editor(&self) -> &Entity<Editor> {
 305        if let Some(secondary) = &self.secondary
 306            && secondary.has_latest_selection
 307        {
 308            &secondary.editor
 309        } else {
 310            &self.primary_editor
 311        }
 312    }
 313
 314    pub fn new_unsplit(
 315        primary_multibuffer: Entity<MultiBuffer>,
 316        project: Entity<Project>,
 317        workspace: Entity<Workspace>,
 318        window: &mut Window,
 319        cx: &mut Context<Self>,
 320    ) -> Self {
 321        let primary_editor = cx.new(|cx| {
 322            let mut editor = Editor::for_multibuffer(
 323                primary_multibuffer.clone(),
 324                Some(project.clone()),
 325                window,
 326                cx,
 327            );
 328            editor.set_expand_all_diff_hunks(cx);
 329            editor
 330        });
 331        let pane = cx.new(|cx| {
 332            let mut pane = Pane::new(
 333                workspace.downgrade(),
 334                project,
 335                Default::default(),
 336                None,
 337                NoAction.boxed_clone(),
 338                true,
 339                window,
 340                cx,
 341            );
 342            pane.set_should_display_tab_bar(|_, _| false);
 343            pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
 344            pane
 345        });
 346        let panes = PaneGroup::new(pane);
 347        // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
 348        let subscriptions = vec![cx.subscribe(
 349            &primary_editor,
 350            |this, _, event: &EditorEvent, cx| match event {
 351                EditorEvent::ExpandExcerptsRequested {
 352                    excerpt_ids,
 353                    lines,
 354                    direction,
 355                } => {
 356                    this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
 357                }
 358                EditorEvent::SelectionsChanged { .. } => {
 359                    if let Some(secondary) = &mut this.secondary {
 360                        secondary.has_latest_selection = false;
 361                    }
 362                    cx.emit(event.clone());
 363                }
 364                _ => cx.emit(event.clone()),
 365            },
 366        )];
 367
 368        window.defer(cx, {
 369            let workspace = workspace.downgrade();
 370            let primary_editor = primary_editor.downgrade();
 371            move |window, cx| {
 372                workspace
 373                    .update(cx, |workspace, cx| {
 374                        primary_editor.update(cx, |editor, cx| {
 375                            editor.added_to_workspace(workspace, window, cx);
 376                        })
 377                    })
 378                    .ok();
 379            }
 380        });
 381        let split_state = cx.new(|cx| SplitEditorState::new(cx));
 382        Self {
 383            primary_editor,
 384            primary_multibuffer,
 385            secondary: None,
 386            panes,
 387            workspace: workspace.downgrade(),
 388            split_state,
 389            locked_cursors: false,
 390            _subscriptions: subscriptions,
 391        }
 392    }
 393
 394    fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
 395        if !cx.has_flag::<SplitDiffFeatureFlag>() {
 396            return;
 397        }
 398        if self.secondary.is_some() {
 399            return;
 400        }
 401        let Some(workspace) = self.workspace.upgrade() else {
 402            return;
 403        };
 404        let project = workspace.read(cx).project().clone();
 405
 406        let secondary_multibuffer = cx.new(|cx| {
 407            let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
 408            multibuffer.set_all_diff_hunks_expanded(cx);
 409            multibuffer
 410        });
 411        let secondary_editor = cx.new(|cx| {
 412            let mut editor = Editor::for_multibuffer(
 413                secondary_multibuffer.clone(),
 414                Some(project.clone()),
 415                window,
 416                cx,
 417            );
 418            editor.number_deleted_lines = true;
 419            editor.set_delegate_expand_excerpts(true);
 420            editor
 421        });
 422        let secondary_pane = cx.new(|cx| {
 423            let mut pane = Pane::new(
 424                workspace.downgrade(),
 425                workspace.read(cx).project().clone(),
 426                Default::default(),
 427                None,
 428                NoAction.boxed_clone(),
 429                true,
 430                window,
 431                cx,
 432            );
 433            pane.set_should_display_tab_bar(|_, _| false);
 434            pane.add_item(
 435                ItemHandle::boxed_clone(&secondary_editor),
 436                false,
 437                false,
 438                None,
 439                window,
 440                cx,
 441            );
 442            pane
 443        });
 444
 445        let subscriptions = vec![cx.subscribe(
 446            &secondary_editor,
 447            |this, _, event: &EditorEvent, cx| match event {
 448                EditorEvent::ExpandExcerptsRequested {
 449                    excerpt_ids,
 450                    lines,
 451                    direction,
 452                } => {
 453                    if this.secondary.is_some() {
 454                        let primary_display_map = this.primary_editor.read(cx).display_map.read(cx);
 455                        let primary_ids: Vec<_> = excerpt_ids
 456                            .iter()
 457                            .filter_map(|id| {
 458                                primary_display_map.companion_excerpt_to_my_excerpt(*id, cx)
 459                            })
 460                            .collect();
 461                        this.expand_excerpts(primary_ids.into_iter(), *lines, *direction, cx);
 462                    }
 463                }
 464                EditorEvent::SelectionsChanged { .. } => {
 465                    if let Some(secondary) = &mut this.secondary {
 466                        secondary.has_latest_selection = true;
 467                    }
 468                    cx.emit(event.clone());
 469                }
 470                _ => cx.emit(event.clone()),
 471            },
 472        )];
 473        let mut secondary = SecondaryEditor {
 474            editor: secondary_editor,
 475            multibuffer: secondary_multibuffer,
 476            pane: secondary_pane.clone(),
 477            has_latest_selection: false,
 478            _subscriptions: subscriptions,
 479        };
 480        let primary_display_map = self.primary_editor.read(cx).display_map.clone();
 481        let secondary_display_map = secondary.editor.read(cx).display_map.clone();
 482        let rhs_display_map_id = primary_display_map.entity_id();
 483
 484        self.primary_editor.update(cx, |editor, cx| {
 485            editor.set_delegate_expand_excerpts(true);
 486            editor.buffer().update(cx, |primary_multibuffer, cx| {
 487                primary_multibuffer.set_show_deleted_hunks(false, cx);
 488                primary_multibuffer.set_use_extended_diff_range(true, cx);
 489            })
 490        });
 491
 492        let path_diffs: Vec<_> = {
 493            let primary_multibuffer = self.primary_multibuffer.read(cx);
 494            primary_multibuffer
 495                .paths()
 496                .filter_map(|path| {
 497                    let excerpt_id = primary_multibuffer.excerpts_for_path(path).next()?;
 498                    let snapshot = primary_multibuffer.snapshot(cx);
 499                    let buffer = snapshot.buffer_for_excerpt(excerpt_id)?;
 500                    let diff = primary_multibuffer.diff_for(buffer.remote_id())?;
 501                    Some((path.clone(), diff))
 502                })
 503                .collect()
 504        };
 505
 506        let mut companion = Companion::new(
 507            rhs_display_map_id,
 508            convert_rhs_rows_to_lhs,
 509            convert_lhs_rows_to_rhs,
 510        );
 511
 512        for (path, diff) in path_diffs {
 513            for (lhs, rhs) in secondary.update_path_excerpts_from_primary(
 514                path,
 515                &self.primary_multibuffer,
 516                diff.clone(),
 517                cx,
 518            ) {
 519                companion.add_excerpt_mapping(lhs, rhs);
 520            }
 521            companion.add_buffer_mapping(
 522                diff.read(cx).base_text(cx).remote_id(),
 523                diff.read(cx).buffer_id,
 524            );
 525        }
 526
 527        let companion = cx.new(|_| companion);
 528
 529        primary_display_map.update(cx, |dm, cx| {
 530            dm.set_companion(
 531                Some((secondary_display_map.downgrade(), companion.clone())),
 532                cx,
 533            );
 534        });
 535        secondary_display_map.update(cx, |dm, cx| {
 536            dm.set_companion(Some((primary_display_map.downgrade(), companion)), cx);
 537        });
 538
 539        let primary_weak = self.primary_editor.downgrade();
 540        let secondary_weak = secondary.editor.downgrade();
 541
 542        let this = cx.entity().downgrade();
 543        self.primary_editor.update(cx, |editor, _cx| {
 544            editor.set_scroll_companion(Some(secondary_weak));
 545            let this = this.clone();
 546            editor.set_on_local_selections_changed(Some(Box::new(
 547                move |cursor_position, window, cx| {
 548                    let this = this.clone();
 549                    window.defer(cx, move |window, cx| {
 550                        this.update(cx, |this, cx| {
 551                            if this.locked_cursors {
 552                                this.sync_cursor_to_other_side(true, cursor_position, window, cx);
 553                            }
 554                        })
 555                        .ok();
 556                    })
 557                },
 558            )));
 559        });
 560        secondary.editor.update(cx, |editor, _cx| {
 561            editor.set_scroll_companion(Some(primary_weak));
 562            let this = this.clone();
 563            editor.set_on_local_selections_changed(Some(Box::new(
 564                move |cursor_position, window, cx| {
 565                    let this = this.clone();
 566                    window.defer(cx, move |window, cx| {
 567                        this.update(cx, |this, cx| {
 568                            if this.locked_cursors {
 569                                this.sync_cursor_to_other_side(false, cursor_position, window, cx);
 570                            }
 571                        })
 572                        .ok();
 573                    })
 574                },
 575            )));
 576        });
 577
 578        let primary_scroll_position = self
 579            .primary_editor
 580            .update(cx, |editor, cx| editor.scroll_position(cx));
 581        secondary.editor.update(cx, |editor, cx| {
 582            editor.set_scroll_position_internal(primary_scroll_position, false, false, window, cx);
 583        });
 584
 585        // Copy soft wrap state from primary (source of truth) to secondary
 586        let primary_soft_wrap_override = self.primary_editor.read(cx).soft_wrap_mode_override;
 587        secondary.editor.update(cx, |editor, cx| {
 588            editor.soft_wrap_mode_override = primary_soft_wrap_override;
 589            cx.notify();
 590        });
 591
 592        self.secondary = Some(secondary);
 593
 594        let primary_pane = self.panes.first_pane();
 595        self.panes
 596            .split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
 597            .unwrap();
 598        cx.notify();
 599    }
 600
 601    fn activate_pane_left(
 602        &mut self,
 603        _: &ActivatePaneLeft,
 604        window: &mut Window,
 605        cx: &mut Context<Self>,
 606    ) {
 607        if let Some(secondary) = &mut self.secondary {
 608            if !secondary.has_latest_selection {
 609                secondary.editor.read(cx).focus_handle(cx).focus(window, cx);
 610                secondary.editor.update(cx, |editor, cx| {
 611                    editor.request_autoscroll(Autoscroll::fit(), cx);
 612                });
 613                secondary.has_latest_selection = true;
 614                cx.notify();
 615            } else {
 616                cx.propagate();
 617            }
 618        } else {
 619            cx.propagate();
 620        }
 621    }
 622
 623    fn activate_pane_right(
 624        &mut self,
 625        _: &ActivatePaneRight,
 626        window: &mut Window,
 627        cx: &mut Context<Self>,
 628    ) {
 629        if let Some(secondary) = &mut self.secondary {
 630            if secondary.has_latest_selection {
 631                self.primary_editor
 632                    .read(cx)
 633                    .focus_handle(cx)
 634                    .focus(window, cx);
 635                self.primary_editor.update(cx, |editor, cx| {
 636                    editor.request_autoscroll(Autoscroll::fit(), cx);
 637                });
 638                secondary.has_latest_selection = false;
 639                cx.notify();
 640            } else {
 641                cx.propagate();
 642            }
 643        } else {
 644            cx.propagate();
 645        }
 646    }
 647
 648    fn toggle_locked_cursors(
 649        &mut self,
 650        _: &ToggleLockedCursors,
 651        _window: &mut Window,
 652        cx: &mut Context<Self>,
 653    ) {
 654        self.locked_cursors = !self.locked_cursors;
 655        cx.notify();
 656    }
 657
 658    pub fn locked_cursors(&self) -> bool {
 659        self.locked_cursors
 660    }
 661
 662    fn sync_cursor_to_other_side(
 663        &mut self,
 664        from_primary: bool,
 665        source_point: Point,
 666        window: &mut Window,
 667        cx: &mut Context<Self>,
 668    ) {
 669        let Some(secondary) = &self.secondary else {
 670            return;
 671        };
 672
 673        let target_editor = if from_primary {
 674            &secondary.editor
 675        } else {
 676            &self.primary_editor
 677        };
 678
 679        let (source_multibuffer, target_multibuffer) = if from_primary {
 680            (&self.primary_multibuffer, &secondary.multibuffer)
 681        } else {
 682            (&secondary.multibuffer, &self.primary_multibuffer)
 683        };
 684
 685        let source_snapshot = source_multibuffer.read(cx).snapshot(cx);
 686        let target_snapshot = target_multibuffer.read(cx).snapshot(cx);
 687
 688        let target_point = target_editor.update(cx, |target_editor, cx| {
 689            target_editor.display_map.update(cx, |display_map, cx| {
 690                let display_map_id = cx.entity_id();
 691                display_map.companion().unwrap().update(cx, |companion, _| {
 692                    companion
 693                        .convert_rows_from_companion(
 694                            display_map_id,
 695                            &target_snapshot,
 696                            &source_snapshot,
 697                            (Bound::Included(source_point), Bound::Included(source_point)),
 698                        )
 699                        .first()
 700                        .unwrap()
 701                        .boundaries
 702                        .first()
 703                        .unwrap()
 704                        .1
 705                        .start
 706                })
 707            })
 708        });
 709
 710        target_editor.update(cx, |editor, cx| {
 711            editor.set_suppress_selection_callback(true);
 712            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
 713                s.select_ranges([target_point..target_point]);
 714            });
 715            editor.set_suppress_selection_callback(false);
 716        });
 717    }
 718
 719    fn toggle_split(&mut self, _: &ToggleSplitDiff, window: &mut Window, cx: &mut Context<Self>) {
 720        if self.secondary.is_some() {
 721            self.unsplit(&UnsplitDiff, window, cx);
 722        } else {
 723            self.split(&SplitDiff, window, cx);
 724        }
 725    }
 726
 727    fn intercept_toggle_code_actions(
 728        &mut self,
 729        _: &ToggleCodeActions,
 730        _window: &mut Window,
 731        cx: &mut Context<Self>,
 732    ) {
 733        if self.secondary.is_some() {
 734            cx.stop_propagation();
 735        } else {
 736            cx.propagate();
 737        }
 738    }
 739
 740    fn intercept_toggle_breakpoint(
 741        &mut self,
 742        _: &ToggleBreakpoint,
 743        _window: &mut Window,
 744        cx: &mut Context<Self>,
 745    ) {
 746        // Only block breakpoint actions when the left (secondary) editor has focus
 747        if let Some(secondary) = &self.secondary {
 748            if secondary.has_latest_selection {
 749                cx.stop_propagation();
 750            } else {
 751                cx.propagate();
 752            }
 753        } else {
 754            cx.propagate();
 755        }
 756    }
 757
 758    fn intercept_enable_breakpoint(
 759        &mut self,
 760        _: &EnableBreakpoint,
 761        _window: &mut Window,
 762        cx: &mut Context<Self>,
 763    ) {
 764        // Only block breakpoint actions when the left (secondary) editor has focus
 765        if let Some(secondary) = &self.secondary {
 766            if secondary.has_latest_selection {
 767                cx.stop_propagation();
 768            } else {
 769                cx.propagate();
 770            }
 771        } else {
 772            cx.propagate();
 773        }
 774    }
 775
 776    fn intercept_disable_breakpoint(
 777        &mut self,
 778        _: &DisableBreakpoint,
 779        _window: &mut Window,
 780        cx: &mut Context<Self>,
 781    ) {
 782        // Only block breakpoint actions when the left (secondary) editor has focus
 783        if let Some(secondary) = &self.secondary {
 784            if secondary.has_latest_selection {
 785                cx.stop_propagation();
 786            } else {
 787                cx.propagate();
 788            }
 789        } else {
 790            cx.propagate();
 791        }
 792    }
 793
 794    fn intercept_edit_log_breakpoint(
 795        &mut self,
 796        _: &EditLogBreakpoint,
 797        _window: &mut Window,
 798        cx: &mut Context<Self>,
 799    ) {
 800        // Only block breakpoint actions when the left (secondary) editor has focus
 801        if let Some(secondary) = &self.secondary {
 802            if secondary.has_latest_selection {
 803                cx.stop_propagation();
 804            } else {
 805                cx.propagate();
 806            }
 807        } else {
 808            cx.propagate();
 809        }
 810    }
 811
 812    fn intercept_inline_assist(
 813        &mut self,
 814        _: &InlineAssist,
 815        _window: &mut Window,
 816        cx: &mut Context<Self>,
 817    ) {
 818        if self.secondary.is_some() {
 819            cx.stop_propagation();
 820        } else {
 821            cx.propagate();
 822        }
 823    }
 824
 825    fn toggle_soft_wrap(
 826        &mut self,
 827        _: &ToggleSoftWrap,
 828        window: &mut Window,
 829        cx: &mut Context<Self>,
 830    ) {
 831        if let Some(secondary) = &self.secondary {
 832            cx.stop_propagation();
 833
 834            let is_secondary_focused = secondary.has_latest_selection;
 835            let (focused_editor, other_editor) = if is_secondary_focused {
 836                (&secondary.editor, &self.primary_editor)
 837            } else {
 838                (&self.primary_editor, &secondary.editor)
 839            };
 840
 841            // Toggle the focused editor
 842            focused_editor.update(cx, |editor, cx| {
 843                editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
 844            });
 845
 846            // Copy the soft wrap state from the focused editor to the other editor
 847            let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
 848            other_editor.update(cx, |editor, cx| {
 849                editor.soft_wrap_mode_override = soft_wrap_override;
 850                cx.notify();
 851            });
 852        } else {
 853            cx.propagate();
 854        }
 855    }
 856
 857    fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
 858        let Some(secondary) = self.secondary.take() else {
 859            return;
 860        };
 861        self.panes.remove(&secondary.pane, cx).unwrap();
 862        self.primary_editor.update(cx, |primary, cx| {
 863            primary.set_on_local_selections_changed(None);
 864            primary.set_scroll_companion(None);
 865            primary.set_delegate_expand_excerpts(false);
 866            primary.buffer().update(cx, |buffer, cx| {
 867                buffer.set_show_deleted_hunks(true, cx);
 868                buffer.set_use_extended_diff_range(false, cx);
 869            });
 870            primary.display_map.update(cx, |dm, cx| {
 871                dm.set_companion(None, cx);
 872            });
 873        });
 874        secondary.editor.update(cx, |editor, _cx| {
 875            editor.set_on_local_selections_changed(None);
 876            editor.set_scroll_companion(None);
 877        });
 878        cx.notify();
 879    }
 880
 881    pub fn added_to_workspace(
 882        &mut self,
 883        workspace: &mut Workspace,
 884        window: &mut Window,
 885        cx: &mut Context<Self>,
 886    ) {
 887        self.workspace = workspace.weak_handle();
 888        self.primary_editor.update(cx, |primary_editor, cx| {
 889            primary_editor.added_to_workspace(workspace, window, cx);
 890        });
 891        if let Some(secondary) = &self.secondary {
 892            secondary.editor.update(cx, |secondary_editor, cx| {
 893                secondary_editor.added_to_workspace(workspace, window, cx);
 894            });
 895        }
 896    }
 897
 898    pub fn set_excerpts_for_path(
 899        &mut self,
 900        path: PathKey,
 901        buffer: Entity<Buffer>,
 902        ranges: impl IntoIterator<Item = Range<Point>> + Clone,
 903        context_line_count: u32,
 904        diff: Entity<BufferDiff>,
 905        cx: &mut Context<Self>,
 906    ) -> (Vec<Range<Anchor>>, bool) {
 907        let primary_display_map = self.primary_editor.read(cx).display_map.clone();
 908        let secondary_display_map = self
 909            .secondary
 910            .as_ref()
 911            .map(|s| s.editor.read(cx).display_map.clone());
 912
 913        let (anchors, added_a_new_excerpt) =
 914            self.primary_multibuffer
 915                .update(cx, |primary_multibuffer, cx| {
 916                    let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
 917                        path.clone(),
 918                        buffer.clone(),
 919                        ranges,
 920                        context_line_count,
 921                        cx,
 922                    );
 923                    if !anchors.is_empty()
 924                        && primary_multibuffer
 925                            .diff_for(buffer.read(cx).remote_id())
 926                            .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
 927                    {
 928                        primary_multibuffer.add_diff(diff.clone(), cx);
 929                    }
 930                    (anchors, added_a_new_excerpt)
 931                });
 932
 933        if let Some(secondary) = &mut self.secondary {
 934            if let Some(secondary_display_map) = &secondary_display_map {
 935                secondary.sync_path_excerpts(
 936                    path,
 937                    &self.primary_multibuffer,
 938                    diff,
 939                    &primary_display_map,
 940                    secondary_display_map,
 941                    cx,
 942                );
 943            }
 944        }
 945
 946        (anchors, added_a_new_excerpt)
 947    }
 948
 949    fn expand_excerpts(
 950        &mut self,
 951        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
 952        lines: u32,
 953        direction: ExpandExcerptDirection,
 954        cx: &mut Context<Self>,
 955    ) {
 956        let mut corresponding_paths = HashMap::default();
 957        self.primary_multibuffer.update(cx, |multibuffer, cx| {
 958            let snapshot = multibuffer.snapshot(cx);
 959            if self.secondary.is_some() {
 960                corresponding_paths = excerpt_ids
 961                    .clone()
 962                    .map(|excerpt_id| {
 963                        let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
 964                        let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
 965                        let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
 966                        (path, diff)
 967                    })
 968                    .collect::<HashMap<_, _>>();
 969            }
 970            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
 971        });
 972
 973        if let Some(secondary) = &mut self.secondary {
 974            let primary_display_map = self.primary_editor.read(cx).display_map.clone();
 975            let secondary_display_map = secondary.editor.read(cx).display_map.clone();
 976            for (path, diff) in corresponding_paths {
 977                secondary.sync_path_excerpts(
 978                    path,
 979                    &self.primary_multibuffer,
 980                    diff,
 981                    &primary_display_map,
 982                    &secondary_display_map,
 983                    cx,
 984                );
 985            }
 986        }
 987    }
 988
 989    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
 990        self.primary_multibuffer.update(cx, |buffer, cx| {
 991            buffer.remove_excerpts_for_path(path.clone(), cx)
 992        });
 993        if let Some(secondary) = &self.secondary {
 994            let primary_display_map = self.primary_editor.read(cx).display_map.clone();
 995            let secondary_display_map = secondary.editor.read(cx).display_map.clone();
 996            secondary.remove_mappings_for_path(
 997                &path,
 998                &self.primary_multibuffer,
 999                &primary_display_map,
1000                &secondary_display_map,
1001                cx,
1002            );
1003            secondary
1004                .multibuffer
1005                .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
1006        }
1007    }
1008}
1009
1010#[cfg(test)]
1011impl SplittableEditor {
1012    fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1013        use multi_buffer::MultiBufferRow;
1014        use text::Bias;
1015
1016        use crate::display_map::Block;
1017        use crate::display_map::DisplayRow;
1018
1019        self.debug_print(cx);
1020
1021        let secondary = self.secondary.as_ref().unwrap();
1022        let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
1023        let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
1024        assert_eq!(
1025            secondary_excerpts.len(),
1026            primary_excerpts.len(),
1027            "mismatch in excerpt count"
1028        );
1029
1030        if quiesced {
1031            let rhs_snapshot = secondary
1032                .editor
1033                .update(cx, |editor, cx| editor.display_snapshot(cx));
1034            let lhs_snapshot = self
1035                .primary_editor
1036                .update(cx, |editor, cx| editor.display_snapshot(cx));
1037
1038            let lhs_max_row = lhs_snapshot.max_point().row();
1039            let rhs_max_row = rhs_snapshot.max_point().row();
1040            assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1041
1042            let lhs_excerpt_block_rows = lhs_snapshot
1043                .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1044                .filter(|(_, block)| {
1045                    matches!(
1046                        block,
1047                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1048                    )
1049                })
1050                .map(|(row, _)| row)
1051                .collect::<Vec<_>>();
1052            let rhs_excerpt_block_rows = rhs_snapshot
1053                .blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
1054                .filter(|(_, block)| {
1055                    matches!(
1056                        block,
1057                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1058                    )
1059                })
1060                .map(|(row, _)| row)
1061                .collect::<Vec<_>>();
1062            assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1063
1064            for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1065                assert_eq!(
1066                    lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1067                    "mismatch in hunks"
1068                );
1069                assert_eq!(
1070                    lhs_hunk.status, rhs_hunk.status,
1071                    "mismatch in hunk statuses"
1072                );
1073
1074                let (lhs_point, rhs_point) =
1075                    if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1076                        (
1077                            Point::new(lhs_hunk.row_range.end.0, 0),
1078                            Point::new(rhs_hunk.row_range.end.0, 0),
1079                        )
1080                    } else {
1081                        (
1082                            Point::new(lhs_hunk.row_range.start.0, 0),
1083                            Point::new(rhs_hunk.row_range.start.0, 0),
1084                        )
1085                    };
1086                let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1087                let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1088                assert_eq!(
1089                    lhs_point.row(),
1090                    rhs_point.row(),
1091                    "mismatch in hunk position"
1092                );
1093            }
1094
1095            // Filtering out empty lines is a bit of a hack, to work around a case where
1096            // the base text has a trailing newline but the current text doesn't, or vice versa.
1097            // In this case, we get the additional newline on one side, but that line is not
1098            // marked as added/deleted by rowinfos.
1099            self.check_sides_match(cx, |snapshot| {
1100                snapshot
1101                    .buffer_snapshot()
1102                    .text()
1103                    .split("\n")
1104                    .zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
1105                    .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
1106                    .map(|(line, _)| line.to_owned())
1107                    .collect::<Vec<_>>()
1108            });
1109        }
1110    }
1111
1112    #[track_caller]
1113    fn check_sides_match<T: std::fmt::Debug + PartialEq>(
1114        &self,
1115        cx: &mut App,
1116        mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
1117    ) {
1118        let secondary = self.secondary.as_ref().expect("requires split");
1119        let primary_snapshot = self.primary_editor.update(cx, |editor, cx| {
1120            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1121        });
1122        let secondary_snapshot = secondary.editor.update(cx, |editor, cx| {
1123            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1124        });
1125
1126        let primary_t = extract(&primary_snapshot);
1127        let secondary_t = extract(&secondary_snapshot);
1128
1129        if primary_t != secondary_t {
1130            self.debug_print(cx);
1131            pretty_assertions::assert_eq!(primary_t, secondary_t);
1132        }
1133    }
1134
1135    fn debug_print(&self, cx: &mut App) {
1136        use crate::DisplayRow;
1137        use crate::display_map::Block;
1138        use buffer_diff::DiffHunkStatusKind;
1139
1140        assert!(
1141            self.secondary.is_some(),
1142            "debug_print is only useful when secondary editor exists"
1143        );
1144
1145        let secondary = self.secondary.as_ref().unwrap();
1146
1147        // Get terminal width, default to 80 if unavailable
1148        let terminal_width = std::env::var("COLUMNS")
1149            .ok()
1150            .and_then(|s| s.parse::<usize>().ok())
1151            .unwrap_or(80);
1152
1153        // Each side gets half the terminal width minus the separator
1154        let separator = "";
1155        let side_width = (terminal_width - separator.len()) / 2;
1156
1157        // Get display snapshots for both editors
1158        let secondary_snapshot = secondary.editor.update(cx, |editor, cx| {
1159            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1160        });
1161        let primary_snapshot = self.primary_editor.update(cx, |editor, cx| {
1162            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1163        });
1164
1165        let secondary_max_row = secondary_snapshot.max_point().row().0;
1166        let primary_max_row = primary_snapshot.max_point().row().0;
1167        let max_row = secondary_max_row.max(primary_max_row);
1168
1169        // Build a map from display row -> block type string
1170        // Each row of a multi-row block gets an entry with the same block type
1171        // For spacers, the ID is included in brackets
1172        fn build_block_map(
1173            snapshot: &crate::DisplaySnapshot,
1174            max_row: u32,
1175        ) -> std::collections::HashMap<u32, String> {
1176            let mut block_map = std::collections::HashMap::new();
1177            for (start_row, block) in
1178                snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1179            {
1180                let (block_type, height) = match block {
1181                    Block::Spacer {
1182                        id,
1183                        height,
1184                        is_below: _,
1185                    } => (format!("SPACER[{}]", id.0), *height),
1186                    Block::ExcerptBoundary { height, .. } => {
1187                        ("EXCERPT_BOUNDARY".to_string(), *height)
1188                    }
1189                    Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1190                    Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1191                    Block::Custom(custom) => {
1192                        ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1193                    }
1194                };
1195                for offset in 0..height {
1196                    block_map.insert(start_row.0 + offset, block_type.clone());
1197                }
1198            }
1199            block_map
1200        }
1201
1202        let secondary_blocks = build_block_map(&secondary_snapshot, secondary_max_row);
1203        let primary_blocks = build_block_map(&primary_snapshot, primary_max_row);
1204
1205        fn display_width(s: &str) -> usize {
1206            unicode_width::UnicodeWidthStr::width(s)
1207        }
1208
1209        fn truncate_line(line: &str, max_width: usize) -> String {
1210            let line_width = display_width(line);
1211            if line_width <= max_width {
1212                return line.to_string();
1213            }
1214            if max_width < 9 {
1215                let mut result = String::new();
1216                let mut width = 0;
1217                for c in line.chars() {
1218                    let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1219                    if width + c_width > max_width {
1220                        break;
1221                    }
1222                    result.push(c);
1223                    width += c_width;
1224                }
1225                return result;
1226            }
1227            let ellipsis = "...";
1228            let target_prefix_width = 3;
1229            let target_suffix_width = 3;
1230
1231            let mut prefix = String::new();
1232            let mut prefix_width = 0;
1233            for c in line.chars() {
1234                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1235                if prefix_width + c_width > target_prefix_width {
1236                    break;
1237                }
1238                prefix.push(c);
1239                prefix_width += c_width;
1240            }
1241
1242            let mut suffix_chars: Vec<char> = Vec::new();
1243            let mut suffix_width = 0;
1244            for c in line.chars().rev() {
1245                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1246                if suffix_width + c_width > target_suffix_width {
1247                    break;
1248                }
1249                suffix_chars.push(c);
1250                suffix_width += c_width;
1251            }
1252            suffix_chars.reverse();
1253            let suffix: String = suffix_chars.into_iter().collect();
1254
1255            format!("{}{}{}", prefix, ellipsis, suffix)
1256        }
1257
1258        fn pad_to_width(s: &str, target_width: usize) -> String {
1259            let current_width = display_width(s);
1260            if current_width >= target_width {
1261                s.to_string()
1262            } else {
1263                format!("{}{}", s, " ".repeat(target_width - current_width))
1264            }
1265        }
1266
1267        // Helper to format a single row for one side
1268        // Format: "ln# diff bytes(cumul) text" or block info
1269        // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1270        fn format_row(
1271            row: u32,
1272            max_row: u32,
1273            snapshot: &crate::DisplaySnapshot,
1274            blocks: &std::collections::HashMap<u32, String>,
1275            row_infos: &[multi_buffer::RowInfo],
1276            cumulative_bytes: &[usize],
1277            side_width: usize,
1278        ) -> String {
1279            // Get row info if available
1280            let row_info = row_infos.get(row as usize);
1281
1282            // Line number prefix (3 chars + space)
1283            // Use buffer_row from RowInfo, which is None for block rows
1284            let line_prefix = if row > max_row {
1285                "    ".to_string()
1286            } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1287                format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1288            } else {
1289                "    ".to_string() // block rows have no line number
1290            };
1291            let content_width = side_width.saturating_sub(line_prefix.len());
1292
1293            if row > max_row {
1294                return format!("{}{}", line_prefix, " ".repeat(content_width));
1295            }
1296
1297            // Check if this row is a block row
1298            if let Some(block_type) = blocks.get(&row) {
1299                let block_str = format!("~~~[{}]~~~", block_type);
1300                let formatted = format!("{:^width$}", block_str, width = content_width);
1301                return format!(
1302                    "{}{}",
1303                    line_prefix,
1304                    truncate_line(&formatted, content_width)
1305                );
1306            }
1307
1308            // Get line text
1309            let line_text = snapshot.line(DisplayRow(row));
1310            let line_bytes = line_text.len();
1311
1312            // Diff status marker
1313            let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1314                Some(status) => match status.kind {
1315                    DiffHunkStatusKind::Added => "+",
1316                    DiffHunkStatusKind::Deleted => "-",
1317                    DiffHunkStatusKind::Modified => "~",
1318                },
1319                None => " ",
1320            };
1321
1322            // Cumulative bytes
1323            let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1324
1325            // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1326            let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1327            let text_width = content_width.saturating_sub(info_prefix.len());
1328            let truncated_text = truncate_line(&line_text, text_width);
1329
1330            let text_part = pad_to_width(&truncated_text, text_width);
1331            format!("{}{}{}", line_prefix, info_prefix, text_part)
1332        }
1333
1334        // Collect row infos for both sides
1335        let secondary_row_infos: Vec<_> = secondary_snapshot
1336            .row_infos(DisplayRow(0))
1337            .take((secondary_max_row + 1) as usize)
1338            .collect();
1339        let primary_row_infos: Vec<_> = primary_snapshot
1340            .row_infos(DisplayRow(0))
1341            .take((primary_max_row + 1) as usize)
1342            .collect();
1343
1344        // Calculate cumulative bytes for each side (only counting non-block rows)
1345        let mut secondary_cumulative = Vec::with_capacity((secondary_max_row + 1) as usize);
1346        let mut cumulative = 0usize;
1347        for row in 0..=secondary_max_row {
1348            if !secondary_blocks.contains_key(&row) {
1349                cumulative += secondary_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1350            }
1351            secondary_cumulative.push(cumulative);
1352        }
1353
1354        let mut primary_cumulative = Vec::with_capacity((primary_max_row + 1) as usize);
1355        cumulative = 0;
1356        for row in 0..=primary_max_row {
1357            if !primary_blocks.contains_key(&row) {
1358                cumulative += primary_snapshot.line(DisplayRow(row)).len() + 1;
1359            }
1360            primary_cumulative.push(cumulative);
1361        }
1362
1363        // Print header
1364        eprintln!();
1365        eprintln!("{}", "".repeat(terminal_width));
1366        let header_left = format!("{:^width$}", "SECONDARY (LEFT)", width = side_width);
1367        let header_right = format!("{:^width$}", "PRIMARY (RIGHT)", width = side_width);
1368        eprintln!("{}{}{}", header_left, separator, header_right);
1369        eprintln!(
1370            "{:^width$}{}{:^width$}",
1371            "ln# diff len(cum) text",
1372            separator,
1373            "ln# diff len(cum) text",
1374            width = side_width
1375        );
1376        eprintln!("{}", "".repeat(terminal_width));
1377
1378        // Print each row
1379        for row in 0..=max_row {
1380            let left = format_row(
1381                row,
1382                secondary_max_row,
1383                &secondary_snapshot,
1384                &secondary_blocks,
1385                &secondary_row_infos,
1386                &secondary_cumulative,
1387                side_width,
1388            );
1389            let right = format_row(
1390                row,
1391                primary_max_row,
1392                &primary_snapshot,
1393                &primary_blocks,
1394                &primary_row_infos,
1395                &primary_cumulative,
1396                side_width,
1397            );
1398            eprintln!("{}{}{}", left, separator, right);
1399        }
1400
1401        eprintln!("{}", "".repeat(terminal_width));
1402        eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1403        eprintln!();
1404    }
1405
1406    fn randomly_edit_excerpts(
1407        &mut self,
1408        rng: &mut impl rand::Rng,
1409        mutation_count: usize,
1410        cx: &mut Context<Self>,
1411    ) {
1412        use collections::HashSet;
1413        use rand::prelude::*;
1414        use std::env;
1415        use util::RandomCharIter;
1416
1417        let max_buffers = env::var("MAX_BUFFERS")
1418            .map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
1419            .unwrap_or(4);
1420
1421        for _ in 0..mutation_count {
1422            let paths = self
1423                .primary_multibuffer
1424                .read(cx)
1425                .paths()
1426                .cloned()
1427                .collect::<Vec<_>>();
1428            let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
1429
1430            if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
1431                let mut excerpts = HashSet::default();
1432                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
1433                    excerpts.extend(excerpt_ids.choose(rng).copied());
1434                }
1435
1436                let line_count = rng.random_range(1..5);
1437
1438                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
1439
1440                self.expand_excerpts(
1441                    excerpts.iter().cloned(),
1442                    line_count,
1443                    ExpandExcerptDirection::UpAndDown,
1444                    cx,
1445                );
1446                continue;
1447            }
1448
1449            if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
1450                let len = rng.random_range(100..500);
1451                let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
1452                let buffer = cx.new(|cx| Buffer::local(text, cx));
1453                log::info!(
1454                    "Creating new buffer {} with text: {:?}",
1455                    buffer.read(cx).remote_id(),
1456                    buffer.read(cx).text()
1457                );
1458                let buffer_snapshot = buffer.read(cx).snapshot();
1459                let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
1460                // Create some initial diff hunks.
1461                buffer.update(cx, |buffer, cx| {
1462                    buffer.randomly_edit(rng, 1, cx);
1463                });
1464                let buffer_snapshot = buffer.read(cx).text_snapshot();
1465                diff.update(cx, |diff, cx| {
1466                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
1467                });
1468                let path = PathKey::for_buffer(&buffer, cx);
1469                let ranges = diff.update(cx, |diff, cx| {
1470                    diff.snapshot(cx)
1471                        .hunks(&buffer_snapshot)
1472                        .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
1473                        .collect::<Vec<_>>()
1474                });
1475                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1476            } else {
1477                log::info!("removing excerpts");
1478                let remove_count = rng.random_range(1..=paths.len());
1479                let paths_to_remove = paths
1480                    .choose_multiple(rng, remove_count)
1481                    .cloned()
1482                    .collect::<Vec<_>>();
1483                for path in paths_to_remove {
1484                    self.remove_excerpts_for_path(path.clone(), cx);
1485                }
1486            }
1487        }
1488    }
1489}
1490
1491impl EventEmitter<EditorEvent> for SplittableEditor {}
1492impl Focusable for SplittableEditor {
1493    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1494        self.primary_editor.read(cx).focus_handle(cx)
1495    }
1496}
1497
1498impl Render for SplittableEditor {
1499    fn render(
1500        &mut self,
1501        _window: &mut ui::Window,
1502        cx: &mut ui::Context<Self>,
1503    ) -> impl ui::IntoElement {
1504        let inner = if self.secondary.is_some() {
1505            let style = self.primary_editor.read(cx).create_style(cx);
1506            SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
1507        } else {
1508            self.primary_editor.clone().into_any_element()
1509        };
1510        div()
1511            .id("splittable-editor")
1512            .on_action(cx.listener(Self::split))
1513            .on_action(cx.listener(Self::unsplit))
1514            .on_action(cx.listener(Self::toggle_split))
1515            .on_action(cx.listener(Self::activate_pane_left))
1516            .on_action(cx.listener(Self::activate_pane_right))
1517            .on_action(cx.listener(Self::toggle_locked_cursors))
1518            .on_action(cx.listener(Self::intercept_toggle_code_actions))
1519            .on_action(cx.listener(Self::intercept_toggle_breakpoint))
1520            .on_action(cx.listener(Self::intercept_enable_breakpoint))
1521            .on_action(cx.listener(Self::intercept_disable_breakpoint))
1522            .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
1523            .on_action(cx.listener(Self::intercept_inline_assist))
1524            .capture_action(cx.listener(Self::toggle_soft_wrap))
1525            .size_full()
1526            .child(inner)
1527    }
1528}
1529
1530impl SecondaryEditor {
1531    fn update_path_excerpts_from_primary(
1532        &mut self,
1533        path_key: PathKey,
1534        primary_multibuffer: &Entity<MultiBuffer>,
1535        diff: Entity<BufferDiff>,
1536        cx: &mut App,
1537    ) -> Vec<(ExcerptId, ExcerptId)> {
1538        let primary_multibuffer_ref = primary_multibuffer.read(cx);
1539        let primary_excerpt_ids: Vec<ExcerptId> = primary_multibuffer_ref
1540            .excerpts_for_path(&path_key)
1541            .collect();
1542
1543        let Some(excerpt_id) = primary_multibuffer_ref.excerpts_for_path(&path_key).next() else {
1544            self.multibuffer.update(cx, |multibuffer, cx| {
1545                multibuffer.remove_excerpts_for_path(path_key, cx);
1546            });
1547            return Vec::new();
1548        };
1549
1550        let primary_multibuffer_snapshot = primary_multibuffer_ref.snapshot(cx);
1551        let main_buffer = primary_multibuffer_snapshot
1552            .buffer_for_excerpt(excerpt_id)
1553            .unwrap();
1554        let base_text_buffer = diff.read(cx).base_text_buffer();
1555        let diff_snapshot = diff.read(cx).snapshot(cx);
1556        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
1557        let new = primary_multibuffer_ref
1558            .excerpts_for_buffer(main_buffer.remote_id(), cx)
1559            .into_iter()
1560            .map(|(_, excerpt_range)| {
1561                let point_range_to_base_text_point_range = |range: Range<Point>| {
1562                    let (mut translated, _, _) = diff_snapshot.points_to_base_text_points(
1563                        [Point::new(range.start.row, 0), Point::new(range.end.row, 0)],
1564                        main_buffer,
1565                    );
1566                    let start_row = translated.next().unwrap().start.row;
1567                    let end_row = translated.next().unwrap().end.row;
1568                    let end_column = diff_snapshot.base_text().line_len(end_row);
1569                    Point::new(start_row, 0)..Point::new(end_row, end_column)
1570                };
1571                let primary = excerpt_range.primary.to_point(main_buffer);
1572                let context = excerpt_range.context.to_point(main_buffer);
1573                ExcerptRange {
1574                    primary: point_range_to_base_text_point_range(primary),
1575                    context: point_range_to_base_text_point_range(context),
1576                }
1577            })
1578            .collect();
1579
1580        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, 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}