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