split.rs

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