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