split.rs

   1use std::{
   2    ops::{Range, RangeInclusive},
   3    sync::Arc,
   4};
   5
   6use buffer_diff::{BufferDiff, BufferDiffSnapshot};
   7use collections::HashMap;
   8
   9use gpui::{
  10    Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Pixels, Subscription,
  11    WeakEntity, canvas,
  12};
  13use itertools::Itertools;
  14use language::{Buffer, Capability, HighlightedText};
  15use multi_buffer::{
  16    Anchor, AnchorRangeExt as _, BufferOffset, ExcerptRange, ExpandExcerptDirection, MultiBuffer,
  17    MultiBufferDiffHunk, MultiBufferPoint, MultiBufferSnapshot, PathKey,
  18};
  19use project::Project;
  20use rope::Point;
  21use settings::{DiffViewStyle, Settings};
  22use text::{Bias, BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
  23use ui::{
  24    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
  25    Styled as _, Window, div,
  26};
  27
  28use crate::{
  29    display_map::CompanionExcerptPatch,
  30    element::SplitSide,
  31    split_editor_view::{SplitEditorState, SplitEditorView},
  32};
  33use workspace::{
  34    ActivatePaneLeft, ActivatePaneRight, Item, ToolbarItemLocation, Workspace,
  35    item::{ItemBufferKind, ItemEvent, SaveOptions, TabContentParams},
  36    searchable::{SearchEvent, SearchToken, SearchableItem, SearchableItemHandle},
  37};
  38
  39use crate::{
  40    Autoscroll, Editor, EditorEvent, EditorSettings, RenderDiffHunkControlsFn, ToggleSoftWrap,
  41    actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
  42    display_map::Companion,
  43};
  44use zed_actions::assistant::InlineAssist;
  45
  46pub(crate) fn patches_for_lhs_range(
  47    rhs_snapshot: &MultiBufferSnapshot,
  48    lhs_snapshot: &MultiBufferSnapshot,
  49    lhs_bounds: Range<MultiBufferPoint>,
  50) -> Vec<CompanionExcerptPatch> {
  51    patches_for_range(
  52        lhs_snapshot,
  53        rhs_snapshot,
  54        lhs_bounds,
  55        |diff, range, buffer| diff.patch_for_base_text_range(range, buffer),
  56    )
  57}
  58
  59pub(crate) fn patches_for_rhs_range(
  60    lhs_snapshot: &MultiBufferSnapshot,
  61    rhs_snapshot: &MultiBufferSnapshot,
  62    rhs_bounds: Range<MultiBufferPoint>,
  63) -> Vec<CompanionExcerptPatch> {
  64    patches_for_range(
  65        rhs_snapshot,
  66        lhs_snapshot,
  67        rhs_bounds,
  68        |diff, range, buffer| diff.patch_for_buffer_range(range, buffer),
  69    )
  70}
  71
  72fn buffer_range_to_base_text_range(
  73    rhs_range: &Range<Point>,
  74    diff_snapshot: &BufferDiffSnapshot,
  75    rhs_buffer_snapshot: &text::BufferSnapshot,
  76) -> Range<Point> {
  77    let start = diff_snapshot
  78        .buffer_point_to_base_text_range(Point::new(rhs_range.start.row, 0), rhs_buffer_snapshot)
  79        .start;
  80    let end = diff_snapshot
  81        .buffer_point_to_base_text_range(Point::new(rhs_range.end.row, 0), rhs_buffer_snapshot)
  82        .end;
  83    let end_column = diff_snapshot.base_text().line_len(end.row);
  84    Point::new(start.row, 0)..Point::new(end.row, end_column)
  85}
  86
  87fn translate_lhs_selections_to_rhs(
  88    selections_by_buffer: &HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>,
  89    splittable: &SplittableEditor,
  90    cx: &App,
  91) -> HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> {
  92    let Some(lhs) = &splittable.lhs else {
  93        return HashMap::default();
  94    };
  95    let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
  96
  97    let mut translated: HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> =
  98        HashMap::default();
  99
 100    for (lhs_buffer_id, (ranges, scroll_offset)) in selections_by_buffer {
 101        let Some(diff) = lhs_snapshot.diff_for_buffer_id(*lhs_buffer_id) else {
 102            continue;
 103        };
 104        let rhs_buffer_id = diff.buffer_id();
 105
 106        let Some(rhs_buffer) = splittable
 107            .rhs_editor
 108            .read(cx)
 109            .buffer()
 110            .read(cx)
 111            .buffer(rhs_buffer_id)
 112        else {
 113            continue;
 114        };
 115
 116        let Some(diff) = splittable
 117            .rhs_editor
 118            .read(cx)
 119            .buffer()
 120            .read(cx)
 121            .diff_for(rhs_buffer_id)
 122        else {
 123            continue;
 124        };
 125
 126        let diff_snapshot = diff.read(cx).snapshot(cx);
 127        let rhs_buffer_snapshot = rhs_buffer.read(cx).snapshot();
 128        let base_text_buffer = diff.read(cx).base_text_buffer();
 129        let base_text_snapshot = base_text_buffer.read(cx).snapshot();
 130
 131        let translated_ranges: Vec<Range<BufferOffset>> = ranges
 132            .iter()
 133            .map(|range| {
 134                let start_point = base_text_snapshot.offset_to_point(range.start.0);
 135                let end_point = base_text_snapshot.offset_to_point(range.end.0);
 136
 137                let rhs_start = diff_snapshot
 138                    .base_text_point_to_buffer_point(start_point, &rhs_buffer_snapshot);
 139                let rhs_end =
 140                    diff_snapshot.base_text_point_to_buffer_point(end_point, &rhs_buffer_snapshot);
 141
 142                BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_start))
 143                    ..BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_end))
 144            })
 145            .collect();
 146
 147        translated.insert(rhs_buffer, (translated_ranges, *scroll_offset));
 148    }
 149
 150    translated
 151}
 152
 153fn translate_lhs_hunks_to_rhs(
 154    lhs_hunks: &[MultiBufferDiffHunk],
 155    splittable: &SplittableEditor,
 156    cx: &App,
 157) -> Vec<MultiBufferDiffHunk> {
 158    let Some(lhs) = &splittable.lhs else {
 159        return vec![];
 160    };
 161    let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
 162    let rhs_snapshot = splittable.rhs_multibuffer.read(cx).snapshot(cx);
 163    let rhs_hunks: Vec<MultiBufferDiffHunk> = rhs_snapshot.diff_hunks().collect();
 164
 165    let mut translated = Vec::new();
 166    for lhs_hunk in lhs_hunks {
 167        let Some(diff) = lhs_snapshot.diff_for_buffer_id(lhs_hunk.buffer_id) else {
 168            continue;
 169        };
 170        let rhs_buffer_id = diff.buffer_id();
 171        if let Some(rhs_hunk) = rhs_hunks.iter().find(|rhs_hunk| {
 172            rhs_hunk.buffer_id == rhs_buffer_id
 173                && rhs_hunk.diff_base_byte_range == lhs_hunk.diff_base_byte_range
 174        }) {
 175            translated.push(rhs_hunk.clone());
 176        }
 177    }
 178    translated
 179}
 180
 181fn patches_for_range<F>(
 182    source_snapshot: &MultiBufferSnapshot,
 183    target_snapshot: &MultiBufferSnapshot,
 184    source_bounds: Range<MultiBufferPoint>,
 185    translate_fn: F,
 186) -> Vec<CompanionExcerptPatch>
 187where
 188    F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>,
 189{
 190    struct PendingExcerpt {
 191        source_buffer_snapshot: language::BufferSnapshot,
 192        source_excerpt_range: ExcerptRange<text::Anchor>,
 193        buffer_point_range: Range<Point>,
 194    }
 195
 196    let mut result = Vec::new();
 197    let mut current_buffer_id: Option<BufferId> = None;
 198    let mut pending_excerpts: Vec<PendingExcerpt> = Vec::new();
 199    let mut union_context_start: Option<Point> = None;
 200    let mut union_context_end: Option<Point> = None;
 201
 202    let flush_buffer = |pending: &mut Vec<PendingExcerpt>,
 203                        union_start: Point,
 204                        union_end: Point,
 205                        result: &mut Vec<CompanionExcerptPatch>| {
 206        let Some(first) = pending.first() else {
 207            return;
 208        };
 209
 210        let source_buffer_id = first.source_buffer_snapshot.remote_id();
 211        let Some(diff) = source_snapshot.diff_for_buffer_id(source_buffer_id) else {
 212            pending.clear();
 213            return;
 214        };
 215        let source_is_lhs = source_buffer_id == diff.base_text().remote_id();
 216        let target_buffer_id = if source_is_lhs {
 217            diff.buffer_id()
 218        } else {
 219            diff.base_text().remote_id()
 220        };
 221        let Some(target_buffer) = target_snapshot.buffer_for_id(target_buffer_id) else {
 222            pending.clear();
 223            return;
 224        };
 225        let rhs_buffer = if source_is_lhs {
 226            target_buffer
 227        } else {
 228            &first.source_buffer_snapshot
 229        };
 230
 231        let patch = translate_fn(diff, union_start..=union_end, rhs_buffer);
 232
 233        let mut source_excerpts = source_snapshot
 234            .excerpts_for_buffer(source_buffer_id)
 235            .peekable();
 236        let mut target_excerpts = target_snapshot
 237            .excerpts_for_buffer(target_buffer_id)
 238            .peekable();
 239
 240        for excerpt in pending.drain(..) {
 241            while let Some(source_excerpt_range) = source_excerpts.peek()
 242                && source_excerpt_range != &excerpt.source_excerpt_range
 243            {
 244                source_excerpts.next();
 245                target_excerpts.next();
 246            }
 247            if let Some(source_excerpt_range) = source_excerpts.peek()
 248                && let Some(target_excerpt_range) = target_excerpts.peek()
 249            {
 250                result.push(patch_for_excerpt(
 251                    source_snapshot,
 252                    target_snapshot,
 253                    &excerpt.source_buffer_snapshot,
 254                    target_buffer,
 255                    source_excerpt_range.clone(),
 256                    target_excerpt_range.clone(),
 257                    &patch,
 258                    excerpt.buffer_point_range,
 259                ));
 260            }
 261        }
 262    };
 263
 264    for (buffer_snapshot, source_range, source_excerpt_range) in
 265        source_snapshot.range_to_buffer_ranges(source_bounds)
 266    {
 267        let buffer_id = buffer_snapshot.remote_id();
 268
 269        if current_buffer_id != Some(buffer_id) {
 270            if let (Some(start), Some(end)) = (union_context_start.take(), union_context_end.take())
 271            {
 272                flush_buffer(&mut pending_excerpts, start, end, &mut result);
 273            }
 274            current_buffer_id = Some(buffer_id);
 275        }
 276
 277        let buffer_point_range = source_range.to_point(&buffer_snapshot);
 278        let source_context_range = source_excerpt_range.context.to_point(&buffer_snapshot);
 279
 280        union_context_start = Some(union_context_start.map_or(source_context_range.start, |s| {
 281            s.min(source_context_range.start)
 282        }));
 283        union_context_end = Some(union_context_end.map_or(source_context_range.end, |e| {
 284            e.max(source_context_range.end)
 285        }));
 286
 287        pending_excerpts.push(PendingExcerpt {
 288            source_buffer_snapshot: buffer_snapshot,
 289            source_excerpt_range,
 290            buffer_point_range,
 291        });
 292    }
 293
 294    if let (Some(start), Some(end)) = (union_context_start, union_context_end) {
 295        flush_buffer(&mut pending_excerpts, start, end, &mut result);
 296    }
 297
 298    result
 299}
 300
 301fn patch_for_excerpt(
 302    source_snapshot: &MultiBufferSnapshot,
 303    target_snapshot: &MultiBufferSnapshot,
 304    source_buffer_snapshot: &language::BufferSnapshot,
 305    target_buffer_snapshot: &language::BufferSnapshot,
 306    source_excerpt_range: ExcerptRange<text::Anchor>,
 307    target_excerpt_range: ExcerptRange<text::Anchor>,
 308    patch: &Patch<Point>,
 309    source_edited_range: Range<Point>,
 310) -> CompanionExcerptPatch {
 311    let source_buffer_range = source_excerpt_range
 312        .context
 313        .to_point(source_buffer_snapshot);
 314    let source_multibuffer_range = (source_snapshot
 315        .anchor_in_buffer(source_excerpt_range.context.start)
 316        .expect("buffer should exist in multibuffer")
 317        ..source_snapshot
 318            .anchor_in_buffer(source_excerpt_range.context.end)
 319            .expect("buffer should exist in multibuffer"))
 320        .to_point(source_snapshot);
 321    let target_buffer_range = target_excerpt_range
 322        .context
 323        .to_point(target_buffer_snapshot);
 324    let target_multibuffer_range = (target_snapshot
 325        .anchor_in_buffer(target_excerpt_range.context.start)
 326        .expect("buffer should exist in multibuffer")
 327        ..target_snapshot
 328            .anchor_in_buffer(target_excerpt_range.context.end)
 329            .expect("buffer should exist in multibuffer"))
 330        .to_point(target_snapshot);
 331
 332    let edits = patch
 333        .edits()
 334        .iter()
 335        .skip_while(|edit| edit.old.end < source_buffer_range.start)
 336        .take_while(|edit| edit.old.start <= source_buffer_range.end)
 337        .map(|edit| {
 338            let clamped_source_start = edit.old.start.max(source_buffer_range.start);
 339            let clamped_source_end = edit.old.end.min(source_buffer_range.end);
 340            let source_multibuffer_start =
 341                source_multibuffer_range.start + (clamped_source_start - source_buffer_range.start);
 342            let source_multibuffer_end =
 343                source_multibuffer_range.start + (clamped_source_end - source_buffer_range.start);
 344            let clamped_target_start = edit
 345                .new
 346                .start
 347                .max(target_buffer_range.start)
 348                .min(target_buffer_range.end);
 349            let clamped_target_end = edit
 350                .new
 351                .end
 352                .max(target_buffer_range.start)
 353                .min(target_buffer_range.end);
 354            let target_multibuffer_start =
 355                target_multibuffer_range.start + (clamped_target_start - target_buffer_range.start);
 356            let target_multibuffer_end =
 357                target_multibuffer_range.start + (clamped_target_end - target_buffer_range.start);
 358            text::Edit {
 359                old: source_multibuffer_start..source_multibuffer_end,
 360                new: target_multibuffer_start..target_multibuffer_end,
 361            }
 362        });
 363
 364    let edits = [text::Edit {
 365        old: source_multibuffer_range.start..source_multibuffer_range.start,
 366        new: target_multibuffer_range.start..target_multibuffer_range.start,
 367    }]
 368    .into_iter()
 369    .chain(edits);
 370
 371    let mut merged_edits: Vec<text::Edit<Point>> = Vec::new();
 372    for edit in edits {
 373        if let Some(last) = merged_edits.last_mut() {
 374            if edit.new.start <= last.new.end || edit.old.start <= last.old.end {
 375                last.old.end = last.old.end.max(edit.old.end);
 376                last.new.end = last.new.end.max(edit.new.end);
 377                continue;
 378            }
 379        }
 380        merged_edits.push(edit);
 381    }
 382
 383    let edited_range = source_multibuffer_range.start
 384        + (source_edited_range.start - source_buffer_range.start)
 385        ..source_multibuffer_range.start + (source_edited_range.end - source_buffer_range.start);
 386
 387    let source_excerpt_end =
 388        source_multibuffer_range.start + (source_buffer_range.end - source_buffer_range.start);
 389    let target_excerpt_end =
 390        target_multibuffer_range.start + (target_buffer_range.end - target_buffer_range.start);
 391
 392    CompanionExcerptPatch {
 393        patch: Patch::new(merged_edits),
 394        edited_range,
 395        source_excerpt_range: source_multibuffer_range.start..source_excerpt_end,
 396        target_excerpt_range: target_multibuffer_range.start..target_excerpt_end,
 397    }
 398}
 399
 400#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 401#[action(namespace = editor)]
 402pub struct ToggleSplitDiff;
 403
 404pub struct SplittableEditor {
 405    rhs_multibuffer: Entity<MultiBuffer>,
 406    rhs_editor: Entity<Editor>,
 407    lhs: Option<LhsEditor>,
 408    workspace: WeakEntity<Workspace>,
 409    split_state: Entity<SplitEditorState>,
 410    searched_side: Option<SplitSide>,
 411    /// The preferred diff style.
 412    diff_view_style: DiffViewStyle,
 413    /// True when the current width is below the minimum threshold for split
 414    /// mode, regardless of the current diff view style setting.
 415    too_narrow_for_split: bool,
 416    last_width: Option<Pixels>,
 417    _subscriptions: Vec<Subscription>,
 418}
 419
 420struct LhsEditor {
 421    multibuffer: Entity<MultiBuffer>,
 422    editor: Entity<Editor>,
 423    was_last_focused: bool,
 424    _subscriptions: Vec<Subscription>,
 425}
 426
 427impl SplittableEditor {
 428    pub fn rhs_editor(&self) -> &Entity<Editor> {
 429        &self.rhs_editor
 430    }
 431
 432    pub fn lhs_editor(&self) -> Option<&Entity<Editor>> {
 433        self.lhs.as_ref().map(|s| &s.editor)
 434    }
 435
 436    pub fn update_editors(
 437        &self,
 438        cx: &mut Context<Self>,
 439        f: impl Fn(&mut Editor, &mut Context<Editor>),
 440    ) {
 441        if let Some(lhs) = &self.lhs {
 442            lhs.editor.update(cx, &f);
 443        }
 444        self.rhs_editor.update(cx, &f);
 445    }
 446
 447    pub fn diff_view_style(&self) -> DiffViewStyle {
 448        self.diff_view_style
 449    }
 450
 451    pub fn is_split(&self) -> bool {
 452        self.lhs.is_some()
 453    }
 454
 455    pub fn set_render_diff_hunk_controls(
 456        &self,
 457        render_diff_hunk_controls: RenderDiffHunkControlsFn,
 458        cx: &mut Context<Self>,
 459    ) {
 460        self.update_editors(cx, |editor, cx| {
 461            editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
 462        });
 463    }
 464
 465    pub fn disable_diff_hunk_controls(&self, cx: &mut Context<Self>) {
 466        let empty_controls = Arc::new(|_, _: &_, _, _, _, _: &_, _: &mut _, _: &mut _| {
 467            gpui::Empty.into_any_element()
 468        });
 469        self.update_editors(cx, |editor, cx| {
 470            editor.set_render_diff_hunk_controls(empty_controls.clone(), cx);
 471        });
 472    }
 473
 474    fn focused_side(&self) -> SplitSide {
 475        if let Some(lhs) = &self.lhs
 476            && lhs.was_last_focused
 477        {
 478            SplitSide::Left
 479        } else {
 480            SplitSide::Right
 481        }
 482    }
 483
 484    pub fn focused_editor(&self) -> &Entity<Editor> {
 485        if let Some(lhs) = &self.lhs
 486            && lhs.was_last_focused
 487        {
 488            &lhs.editor
 489        } else {
 490            &self.rhs_editor
 491        }
 492    }
 493
 494    pub fn new(
 495        style: DiffViewStyle,
 496        rhs_multibuffer: Entity<MultiBuffer>,
 497        project: Entity<Project>,
 498        workspace: Entity<Workspace>,
 499        window: &mut Window,
 500        cx: &mut Context<Self>,
 501    ) -> Self {
 502        let rhs_editor = cx.new(|cx| {
 503            let mut editor =
 504                Editor::for_multibuffer(rhs_multibuffer.clone(), Some(project.clone()), window, cx);
 505            editor.set_expand_all_diff_hunks(cx);
 506            editor.disable_runnables();
 507            editor.disable_inline_diagnostics();
 508            editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
 509            editor.start_temporary_diff_override();
 510            editor
 511        });
 512        // TODO(split-diff) we might want to tag editor events with whether they came from rhs/lhs
 513        let subscriptions = vec![
 514            cx.subscribe(
 515                &rhs_editor,
 516                |this, _, event: &EditorEvent, cx| match event {
 517                    EditorEvent::ExpandExcerptsRequested {
 518                        excerpt_anchors,
 519                        lines,
 520                        direction,
 521                    } => {
 522                        this.expand_excerpts(
 523                            excerpt_anchors.iter().copied(),
 524                            *lines,
 525                            *direction,
 526                            cx,
 527                        );
 528                    }
 529                    _ => cx.emit(event.clone()),
 530                },
 531            ),
 532            cx.subscribe(&rhs_editor, |this, _, event: &SearchEvent, cx| {
 533                if this.searched_side.is_none() || this.searched_side == Some(SplitSide::Right) {
 534                    cx.emit(event.clone());
 535                }
 536            }),
 537        ];
 538
 539        let this = cx.weak_entity();
 540        window.defer(cx, {
 541            let workspace = workspace.downgrade();
 542            let rhs_editor = rhs_editor.downgrade();
 543            move |window, cx| {
 544                workspace
 545                    .update(cx, |workspace, cx| {
 546                        rhs_editor
 547                            .update(cx, |editor, cx| {
 548                                editor.added_to_workspace(workspace, window, cx);
 549                            })
 550                            .ok();
 551                    })
 552                    .ok();
 553                if style == DiffViewStyle::Split {
 554                    this.update(cx, |this, cx| {
 555                        this.split(window, cx);
 556                    })
 557                    .ok();
 558                }
 559            }
 560        });
 561        let split_state = cx.new(|cx| SplitEditorState::new(cx));
 562        Self {
 563            diff_view_style: style,
 564            rhs_editor,
 565            rhs_multibuffer,
 566            lhs: None,
 567            workspace: workspace.downgrade(),
 568            split_state,
 569            searched_side: None,
 570            too_narrow_for_split: false,
 571            last_width: None,
 572            _subscriptions: subscriptions,
 573        }
 574    }
 575
 576    pub fn split(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 577        if self.lhs.is_some() {
 578            return;
 579        }
 580        let Some(workspace) = self.workspace.upgrade() else {
 581            return;
 582        };
 583        let project = workspace.read(cx).project().clone();
 584
 585        let lhs_multibuffer = cx.new(|cx| {
 586            let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
 587            multibuffer.set_all_diff_hunks_expanded(cx);
 588            multibuffer
 589        });
 590
 591        let render_diff_hunk_controls = self.rhs_editor.read(cx).render_diff_hunk_controls.clone();
 592        let lhs_editor = cx.new(|cx| {
 593            let mut editor =
 594                Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx);
 595            editor.set_number_deleted_lines(true, cx);
 596            editor.set_delegate_expand_excerpts(true);
 597            editor.set_delegate_stage_and_restore(true);
 598            editor.set_delegate_open_excerpts(true);
 599            editor.set_show_vertical_scrollbar(false, cx);
 600            editor.disable_lsp_data();
 601            editor.disable_runnables();
 602            editor.disable_diagnostics(cx);
 603            editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
 604            editor
 605        });
 606
 607        lhs_editor.update(cx, |editor, cx| {
 608            editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
 609        });
 610
 611        let mut subscriptions = vec![cx.subscribe_in(
 612            &lhs_editor,
 613            window,
 614            |this, _, event: &EditorEvent, window, cx| match event {
 615                EditorEvent::ExpandExcerptsRequested {
 616                    excerpt_anchors,
 617                    lines,
 618                    direction,
 619                } => {
 620                    if let Some(lhs) = &this.lhs {
 621                        let rhs_snapshot = this.rhs_multibuffer.read(cx).snapshot(cx);
 622                        let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
 623                        let rhs_anchors = excerpt_anchors
 624                            .iter()
 625                            .filter_map(|anchor| {
 626                                let (anchor, lhs_buffer) =
 627                                    lhs_snapshot.anchor_to_buffer_anchor(*anchor)?;
 628                                let diff = lhs_snapshot.diff_for_buffer_id(anchor.buffer_id)?;
 629                                let rhs_buffer_id = diff.buffer_id();
 630                                let rhs_buffer = rhs_snapshot.buffer_for_id(rhs_buffer_id)?;
 631                                let rhs_point = diff.base_text_point_to_buffer_point(
 632                                    anchor.to_point(&lhs_buffer),
 633                                    &rhs_buffer,
 634                                );
 635                                rhs_snapshot.anchor_in_excerpt(rhs_buffer.anchor_before(rhs_point))
 636                            })
 637                            .collect::<Vec<_>>();
 638                        this.expand_excerpts(rhs_anchors.into_iter(), *lines, *direction, cx);
 639                    }
 640                }
 641                EditorEvent::StageOrUnstageRequested { stage, hunks } => {
 642                    if this.lhs.is_some() {
 643                        let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
 644                        if !translated.is_empty() {
 645                            let stage = *stage;
 646                            this.rhs_editor.update(cx, |editor, cx| {
 647                                let chunk_by = translated.into_iter().chunk_by(|h| h.buffer_id);
 648                                for (buffer_id, hunks) in &chunk_by {
 649                                    editor.do_stage_or_unstage(stage, buffer_id, hunks, cx);
 650                                }
 651                            });
 652                        }
 653                    }
 654                }
 655                EditorEvent::RestoreRequested { hunks } => {
 656                    if this.lhs.is_some() {
 657                        let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
 658                        if !translated.is_empty() {
 659                            this.rhs_editor.update(cx, |editor, cx| {
 660                                editor.restore_diff_hunks(translated, cx);
 661                            });
 662                        }
 663                    }
 664                }
 665                EditorEvent::OpenExcerptsRequested {
 666                    selections_by_buffer,
 667                    split,
 668                } => {
 669                    if this.lhs.is_some() {
 670                        let translated =
 671                            translate_lhs_selections_to_rhs(selections_by_buffer, this, cx);
 672                        if !translated.is_empty() {
 673                            let workspace = this.workspace.clone();
 674                            let split = *split;
 675                            Editor::open_buffers_in_workspace(
 676                                workspace, translated, split, window, cx,
 677                            );
 678                        }
 679                    }
 680                }
 681                _ => cx.emit(event.clone()),
 682            },
 683        )];
 684
 685        subscriptions.push(
 686            cx.subscribe(&lhs_editor, |this, _, event: &SearchEvent, cx| {
 687                if this.searched_side == Some(SplitSide::Left) {
 688                    cx.emit(event.clone());
 689                }
 690            }),
 691        );
 692
 693        let lhs_focus_handle = lhs_editor.read(cx).focus_handle(cx);
 694        subscriptions.push(
 695            cx.on_focus_in(&lhs_focus_handle, window, |this, _window, cx| {
 696                if let Some(lhs) = &mut this.lhs {
 697                    if !lhs.was_last_focused {
 698                        lhs.was_last_focused = true;
 699                        cx.notify();
 700                    }
 701                }
 702            }),
 703        );
 704
 705        let rhs_focus_handle = self.rhs_editor.read(cx).focus_handle(cx);
 706        subscriptions.push(
 707            cx.on_focus_in(&rhs_focus_handle, window, |this, _window, cx| {
 708                if let Some(lhs) = &mut this.lhs {
 709                    if lhs.was_last_focused {
 710                        lhs.was_last_focused = false;
 711                        cx.notify();
 712                    }
 713                }
 714            }),
 715        );
 716
 717        let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
 718        let lhs_display_map = lhs_editor.read(cx).display_map.clone();
 719        let rhs_display_map_id = rhs_display_map.entity_id();
 720        let companion = cx.new(|_| Companion::new(rhs_display_map_id));
 721        let lhs = LhsEditor {
 722            editor: lhs_editor,
 723            multibuffer: lhs_multibuffer,
 724            was_last_focused: false,
 725            _subscriptions: subscriptions,
 726        };
 727
 728        self.rhs_editor.update(cx, |editor, cx| {
 729            editor.set_delegate_expand_excerpts(true);
 730            editor.buffer().update(cx, |rhs_multibuffer, cx| {
 731                rhs_multibuffer.set_show_deleted_hunks(false, cx);
 732                rhs_multibuffer.set_use_extended_diff_range(true, cx);
 733            })
 734        });
 735
 736        let all_paths: Vec<_> = {
 737            let rhs_multibuffer = self.rhs_multibuffer.read(cx);
 738            let rhs_multibuffer_snapshot = rhs_multibuffer.snapshot(cx);
 739            rhs_multibuffer_snapshot
 740                .buffers_with_paths()
 741                .filter_map(|(buffer, path)| {
 742                    let diff = rhs_multibuffer.diff_for(buffer.remote_id())?;
 743                    Some((path.clone(), diff))
 744                })
 745                .collect()
 746        };
 747
 748        self.lhs = Some(lhs);
 749
 750        self.sync_lhs_for_paths(all_paths, cx);
 751
 752        rhs_display_map.update(cx, |dm, cx| {
 753            dm.set_companion(Some((lhs_display_map, companion.clone())), cx);
 754        });
 755
 756        let lhs = self.lhs.as_ref().unwrap();
 757
 758        let shared_scroll_anchor = self
 759            .rhs_editor
 760            .read(cx)
 761            .scroll_manager
 762            .scroll_anchor_entity();
 763        lhs.editor.update(cx, |editor, _cx| {
 764            editor
 765                .scroll_manager
 766                .set_shared_scroll_anchor(shared_scroll_anchor);
 767        });
 768
 769        let this = cx.entity().downgrade();
 770        self.rhs_editor.update(cx, |editor, _cx| {
 771            let this = this.clone();
 772            editor.set_on_local_selections_changed(Some(Box::new(
 773                move |cursor_position, window, cx| {
 774                    let this = this.clone();
 775                    window.defer(cx, move |window, cx| {
 776                        this.update(cx, |this, cx| {
 777                            this.sync_cursor_to_other_side(true, cursor_position, window, cx);
 778                        })
 779                        .ok();
 780                    })
 781                },
 782            )));
 783        });
 784        lhs.editor.update(cx, |editor, _cx| {
 785            let this = this.clone();
 786            editor.set_on_local_selections_changed(Some(Box::new(
 787                move |cursor_position, window, cx| {
 788                    let this = this.clone();
 789                    window.defer(cx, move |window, cx| {
 790                        this.update(cx, |this, cx| {
 791                            this.sync_cursor_to_other_side(false, cursor_position, window, cx);
 792                        })
 793                        .ok();
 794                    })
 795                },
 796            )));
 797        });
 798
 799        // Copy soft wrap state from rhs (source of truth) to lhs
 800        let rhs_soft_wrap_override = self.rhs_editor.read(cx).soft_wrap_mode_override;
 801        lhs.editor.update(cx, |editor, cx| {
 802            editor.soft_wrap_mode_override = rhs_soft_wrap_override;
 803            cx.notify();
 804        });
 805
 806        cx.notify();
 807    }
 808
 809    fn activate_pane_left(
 810        &mut self,
 811        _: &ActivatePaneLeft,
 812        window: &mut Window,
 813        cx: &mut Context<Self>,
 814    ) {
 815        if let Some(lhs) = &self.lhs {
 816            if !lhs.was_last_focused {
 817                lhs.editor.read(cx).focus_handle(cx).focus(window, cx);
 818                lhs.editor.update(cx, |editor, cx| {
 819                    editor.request_autoscroll(Autoscroll::fit(), cx);
 820                });
 821            } else {
 822                cx.propagate();
 823            }
 824        } else {
 825            cx.propagate();
 826        }
 827    }
 828
 829    fn activate_pane_right(
 830        &mut self,
 831        _: &ActivatePaneRight,
 832        window: &mut Window,
 833        cx: &mut Context<Self>,
 834    ) {
 835        if let Some(lhs) = &self.lhs {
 836            if lhs.was_last_focused {
 837                self.rhs_editor.read(cx).focus_handle(cx).focus(window, cx);
 838                self.rhs_editor.update(cx, |editor, cx| {
 839                    editor.request_autoscroll(Autoscroll::fit(), cx);
 840                });
 841            } else {
 842                cx.propagate();
 843            }
 844        } else {
 845            cx.propagate();
 846        }
 847    }
 848
 849    fn sync_cursor_to_other_side(
 850        &mut self,
 851        from_rhs: bool,
 852        source_point: Point,
 853        window: &mut Window,
 854        cx: &mut Context<Self>,
 855    ) {
 856        let Some(lhs) = &self.lhs else {
 857            return;
 858        };
 859
 860        let (source_editor, target_editor) = if from_rhs {
 861            (&self.rhs_editor, &lhs.editor)
 862        } else {
 863            (&lhs.editor, &self.rhs_editor)
 864        };
 865
 866        let source_snapshot = source_editor.update(cx, |editor, cx| editor.snapshot(window, cx));
 867        let target_snapshot = target_editor.update(cx, |editor, cx| editor.snapshot(window, cx));
 868
 869        let display_point = source_snapshot
 870            .display_snapshot
 871            .point_to_display_point(source_point, Bias::Right);
 872        let display_point = target_snapshot.clip_point(display_point, Bias::Right);
 873        let target_point = target_snapshot.display_point_to_point(display_point, Bias::Right);
 874
 875        target_editor.update(cx, |editor, cx| {
 876            editor.set_suppress_selection_callback(true);
 877            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
 878                s.select_ranges([target_point..target_point]);
 879            });
 880            editor.set_suppress_selection_callback(false);
 881        });
 882    }
 883
 884    pub fn toggle_split(
 885        &mut self,
 886        _: &ToggleSplitDiff,
 887        window: &mut Window,
 888        cx: &mut Context<Self>,
 889    ) {
 890        match self.diff_view_style {
 891            DiffViewStyle::Unified => {
 892                self.diff_view_style = DiffViewStyle::Split;
 893                if !self.too_narrow_for_split {
 894                    self.split(window, cx);
 895                }
 896            }
 897            DiffViewStyle::Split => {
 898                self.diff_view_style = DiffViewStyle::Unified;
 899                if self.is_split() {
 900                    self.unsplit(window, cx);
 901                }
 902            }
 903        }
 904    }
 905
 906    fn intercept_toggle_breakpoint(
 907        &mut self,
 908        _: &ToggleBreakpoint,
 909        _window: &mut Window,
 910        cx: &mut Context<Self>,
 911    ) {
 912        // Only block breakpoint actions when the left (lhs) editor has focus
 913        if let Some(lhs) = &self.lhs {
 914            if lhs.was_last_focused {
 915                cx.stop_propagation();
 916            } else {
 917                cx.propagate();
 918            }
 919        } else {
 920            cx.propagate();
 921        }
 922    }
 923
 924    fn intercept_enable_breakpoint(
 925        &mut self,
 926        _: &EnableBreakpoint,
 927        _window: &mut Window,
 928        cx: &mut Context<Self>,
 929    ) {
 930        // Only block breakpoint actions when the left (lhs) editor has focus
 931        if let Some(lhs) = &self.lhs {
 932            if lhs.was_last_focused {
 933                cx.stop_propagation();
 934            } else {
 935                cx.propagate();
 936            }
 937        } else {
 938            cx.propagate();
 939        }
 940    }
 941
 942    fn intercept_disable_breakpoint(
 943        &mut self,
 944        _: &DisableBreakpoint,
 945        _window: &mut Window,
 946        cx: &mut Context<Self>,
 947    ) {
 948        // Only block breakpoint actions when the left (lhs) editor has focus
 949        if let Some(lhs) = &self.lhs {
 950            if lhs.was_last_focused {
 951                cx.stop_propagation();
 952            } else {
 953                cx.propagate();
 954            }
 955        } else {
 956            cx.propagate();
 957        }
 958    }
 959
 960    fn intercept_edit_log_breakpoint(
 961        &mut self,
 962        _: &EditLogBreakpoint,
 963        _window: &mut Window,
 964        cx: &mut Context<Self>,
 965    ) {
 966        // Only block breakpoint actions when the left (lhs) editor has focus
 967        if let Some(lhs) = &self.lhs {
 968            if lhs.was_last_focused {
 969                cx.stop_propagation();
 970            } else {
 971                cx.propagate();
 972            }
 973        } else {
 974            cx.propagate();
 975        }
 976    }
 977
 978    fn intercept_inline_assist(
 979        &mut self,
 980        _: &InlineAssist,
 981        _window: &mut Window,
 982        cx: &mut Context<Self>,
 983    ) {
 984        if self.lhs.is_some() {
 985            cx.stop_propagation();
 986        } else {
 987            cx.propagate();
 988        }
 989    }
 990
 991    fn toggle_soft_wrap(
 992        &mut self,
 993        _: &ToggleSoftWrap,
 994        window: &mut Window,
 995        cx: &mut Context<Self>,
 996    ) {
 997        if let Some(lhs) = &self.lhs {
 998            cx.stop_propagation();
 999
1000            let is_lhs_focused = lhs.was_last_focused;
1001            let (focused_editor, other_editor) = if is_lhs_focused {
1002                (&lhs.editor, &self.rhs_editor)
1003            } else {
1004                (&self.rhs_editor, &lhs.editor)
1005            };
1006
1007            // Toggle the focused editor
1008            focused_editor.update(cx, |editor, cx| {
1009                editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
1010            });
1011
1012            // Copy the soft wrap state from the focused editor to the other editor
1013            let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
1014            other_editor.update(cx, |editor, cx| {
1015                editor.soft_wrap_mode_override = soft_wrap_override;
1016                cx.notify();
1017            });
1018        } else {
1019            cx.propagate();
1020        }
1021    }
1022
1023    fn unsplit(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1024        let Some(lhs) = self.lhs.take() else {
1025            return;
1026        };
1027        self.rhs_editor.update(cx, |rhs, cx| {
1028            let rhs_snapshot = rhs.display_map.update(cx, |dm, cx| dm.snapshot(cx));
1029            let native_anchor = rhs.scroll_manager.native_anchor(&rhs_snapshot, cx);
1030            let rhs_display_map_id = rhs_snapshot.display_map_id;
1031            rhs.scroll_manager
1032                .scroll_anchor_entity()
1033                .update(cx, |shared, _| {
1034                    shared.scroll_anchor = native_anchor;
1035                    shared.display_map_id = Some(rhs_display_map_id);
1036                });
1037
1038            rhs.set_on_local_selections_changed(None);
1039            rhs.set_delegate_expand_excerpts(false);
1040            rhs.buffer().update(cx, |buffer, cx| {
1041                buffer.set_show_deleted_hunks(true, cx);
1042                buffer.set_use_extended_diff_range(false, cx);
1043            });
1044            rhs.display_map.update(cx, |dm, cx| {
1045                dm.set_companion(None, cx);
1046            });
1047        });
1048        lhs.editor.update(cx, |editor, _cx| {
1049            editor.set_on_local_selections_changed(None);
1050        });
1051        cx.notify();
1052    }
1053
1054    pub fn update_excerpts_for_path(
1055        &mut self,
1056        path: PathKey,
1057        buffer: Entity<Buffer>,
1058        ranges: impl IntoIterator<Item = Range<Point>> + Clone,
1059        context_line_count: u32,
1060        diff: Entity<BufferDiff>,
1061        cx: &mut Context<Self>,
1062    ) -> bool {
1063        let has_ranges = ranges.clone().into_iter().next().is_some();
1064        if self.lhs.is_none() {
1065            return self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1066                let added_a_new_excerpt = rhs_multibuffer.update_excerpts_for_path(
1067                    path,
1068                    buffer.clone(),
1069                    ranges,
1070                    context_line_count,
1071                    cx,
1072                );
1073                if has_ranges
1074                    && rhs_multibuffer
1075                        .diff_for(buffer.read(cx).remote_id())
1076                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1077                {
1078                    rhs_multibuffer.add_diff(diff, cx);
1079                }
1080                added_a_new_excerpt
1081            });
1082        }
1083
1084        let result = self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1085            let added_a_new_excerpt = rhs_multibuffer.update_excerpts_for_path(
1086                path.clone(),
1087                buffer.clone(),
1088                ranges,
1089                context_line_count,
1090                cx,
1091            );
1092            if has_ranges
1093                && rhs_multibuffer
1094                    .diff_for(buffer.read(cx).remote_id())
1095                    .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1096            {
1097                rhs_multibuffer.add_diff(diff.clone(), cx);
1098            }
1099            added_a_new_excerpt
1100        });
1101
1102        self.sync_lhs_for_paths(vec![(path, diff)], cx);
1103        result
1104    }
1105
1106    fn expand_excerpts(
1107        &mut self,
1108        excerpt_anchors: impl Iterator<Item = Anchor> + Clone,
1109        lines: u32,
1110        direction: ExpandExcerptDirection,
1111        cx: &mut Context<Self>,
1112    ) {
1113        if self.lhs.is_none() {
1114            self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1115                rhs_multibuffer.expand_excerpts(excerpt_anchors, lines, direction, cx);
1116            });
1117            return;
1118        }
1119
1120        let paths: Vec<_> = self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1121            let snapshot = rhs_multibuffer.snapshot(cx);
1122            let paths = excerpt_anchors
1123                .clone()
1124                .filter_map(|anchor| {
1125                    let (anchor, _) = snapshot.anchor_to_buffer_anchor(anchor)?;
1126                    let path = snapshot.path_for_buffer(anchor.buffer_id)?;
1127                    let diff = rhs_multibuffer.diff_for(anchor.buffer_id)?;
1128                    Some((path.clone(), diff))
1129                })
1130                .collect::<HashMap<_, _>>()
1131                .into_iter()
1132                .collect();
1133            rhs_multibuffer.expand_excerpts(excerpt_anchors, lines, direction, cx);
1134            paths
1135        });
1136
1137        self.sync_lhs_for_paths(paths, cx);
1138    }
1139
1140    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
1141        self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1142            rhs_multibuffer.remove_excerpts(path.clone(), cx);
1143        });
1144
1145        if let Some(lhs) = &self.lhs {
1146            lhs.multibuffer.update(cx, |lhs_multibuffer, cx| {
1147                lhs_multibuffer.remove_excerpts(path, cx);
1148            });
1149        }
1150    }
1151
1152    fn search_token(&self) -> SearchToken {
1153        SearchToken::new(self.focused_side() as u64)
1154    }
1155
1156    fn editor_for_token(&self, token: SearchToken) -> Option<&Entity<Editor>> {
1157        if token.value() == SplitSide::Left as u64 {
1158            return self.lhs.as_ref().map(|lhs| &lhs.editor);
1159        }
1160        Some(&self.rhs_editor)
1161    }
1162
1163    fn sync_lhs_for_paths(
1164        &self,
1165        paths: Vec<(PathKey, Entity<BufferDiff>)>,
1166        cx: &mut Context<Self>,
1167    ) {
1168        let Some(lhs) = &self.lhs else { return };
1169
1170        self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1171            for (path, diff) in paths {
1172                let main_buffer_id = diff.read(cx).buffer_id;
1173                let Some(main_buffer) = rhs_multibuffer.buffer(diff.read(cx).buffer_id) else {
1174                    lhs.multibuffer.update(cx, |lhs_multibuffer, lhs_cx| {
1175                        lhs_multibuffer.remove_excerpts(path, lhs_cx);
1176                    });
1177                    continue;
1178                };
1179                let main_buffer_snapshot = main_buffer.read(cx).snapshot();
1180
1181                let base_text_buffer = diff.read(cx).base_text_buffer().clone();
1182                let diff_snapshot = diff.read(cx).snapshot(cx);
1183                let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
1184
1185                let mut paired_ranges: Vec<(Range<Point>, ExcerptRange<text::Anchor>)> = Vec::new();
1186
1187                let mut have_excerpt = false;
1188                let mut did_merge = false;
1189                let rhs_multibuffer_snapshot = rhs_multibuffer.snapshot(cx);
1190                for info in rhs_multibuffer_snapshot.excerpts_for_buffer(main_buffer_id) {
1191                    have_excerpt = true;
1192                    let rhs_context = info.context.to_point(&main_buffer_snapshot);
1193                    let lhs_context = buffer_range_to_base_text_range(
1194                        &rhs_context,
1195                        &diff_snapshot,
1196                        &main_buffer_snapshot,
1197                    );
1198
1199                    if let Some((prev_lhs_context, prev_rhs_range)) = paired_ranges.last_mut()
1200                        && prev_lhs_context.end >= lhs_context.start
1201                    {
1202                        did_merge = true;
1203                        prev_lhs_context.end = lhs_context.end;
1204                        prev_rhs_range.context.end = info.context.end;
1205                        continue;
1206                    }
1207
1208                    paired_ranges.push((lhs_context, info));
1209                }
1210
1211                let (lhs_ranges, rhs_ranges): (Vec<_>, Vec<_>) = paired_ranges.into_iter().unzip();
1212                let lhs_ranges = lhs_ranges
1213                    .into_iter()
1214                    .map(|range| {
1215                        ExcerptRange::new(base_text_buffer_snapshot.anchor_range_outside(range))
1216                    })
1217                    .collect::<Vec<_>>();
1218
1219                lhs.multibuffer.update(cx, |lhs_multibuffer, lhs_cx| {
1220                    lhs_multibuffer.update_path_excerpts(
1221                        path.clone(),
1222                        base_text_buffer,
1223                        &base_text_buffer_snapshot,
1224                        &lhs_ranges,
1225                        lhs_cx,
1226                    );
1227                    if have_excerpt
1228                        && lhs_multibuffer
1229                            .diff_for(base_text_buffer_snapshot.remote_id())
1230                            .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1231                    {
1232                        lhs_multibuffer.add_inverted_diff(
1233                            diff.clone(),
1234                            main_buffer.clone(),
1235                            lhs_cx,
1236                        );
1237                    }
1238                });
1239
1240                if did_merge {
1241                    rhs_multibuffer.update_path_excerpts(
1242                        path,
1243                        main_buffer,
1244                        &main_buffer_snapshot,
1245                        &rhs_ranges,
1246                        cx,
1247                    );
1248                }
1249            }
1250        });
1251    }
1252
1253    fn width_changed(&mut self, width: Pixels, window: &mut Window, cx: &mut Context<Self>) {
1254        self.last_width = Some(width);
1255
1256        let min_ems = EditorSettings::get_global(cx).minimum_split_diff_width;
1257
1258        let style = self.rhs_editor.read(cx).create_style(cx);
1259        let font_id = window.text_system().resolve_font(&style.text.font());
1260        let font_size = style.text.font_size.to_pixels(window.rem_size());
1261        let em_advance = window
1262            .text_system()
1263            .em_advance(font_id, font_size)
1264            .unwrap_or(font_size);
1265        let min_width = em_advance * min_ems;
1266        let is_split = self.lhs.is_some();
1267
1268        self.too_narrow_for_split = min_ems > 0.0 && width < min_width;
1269
1270        match self.diff_view_style {
1271            DiffViewStyle::Unified => {}
1272            DiffViewStyle::Split => {
1273                if self.too_narrow_for_split && is_split {
1274                    self.unsplit(window, cx);
1275                } else if !self.too_narrow_for_split && !is_split {
1276                    self.split(window, cx);
1277                }
1278            }
1279        }
1280    }
1281}
1282
1283#[cfg(test)]
1284impl SplittableEditor {
1285    fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1286        use text::Bias;
1287
1288        use crate::display_map::Block;
1289        use crate::display_map::DisplayRow;
1290
1291        self.debug_print(cx);
1292        self.check_excerpt_invariants(quiesced, cx);
1293
1294        let lhs = self.lhs.as_ref().unwrap();
1295
1296        if quiesced {
1297            let lhs_snapshot = lhs
1298                .editor
1299                .update(cx, |editor, cx| editor.display_snapshot(cx));
1300            let rhs_snapshot = self
1301                .rhs_editor
1302                .update(cx, |editor, cx| editor.display_snapshot(cx));
1303
1304            let lhs_max_row = lhs_snapshot.max_point().row();
1305            let rhs_max_row = rhs_snapshot.max_point().row();
1306            assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1307
1308            let lhs_excerpt_block_rows = lhs_snapshot
1309                .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1310                .filter(|(_, block)| {
1311                    matches!(
1312                        block,
1313                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1314                    )
1315                })
1316                .map(|(row, _)| row)
1317                .collect::<Vec<_>>();
1318            let rhs_excerpt_block_rows = rhs_snapshot
1319                .blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
1320                .filter(|(_, block)| {
1321                    matches!(
1322                        block,
1323                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1324                    )
1325                })
1326                .map(|(row, _)| row)
1327                .collect::<Vec<_>>();
1328            assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1329
1330            for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1331                assert_eq!(
1332                    lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1333                    "mismatch in hunks"
1334                );
1335                assert_eq!(
1336                    lhs_hunk.status, rhs_hunk.status,
1337                    "mismatch in hunk statuses"
1338                );
1339
1340                let (lhs_point, rhs_point) =
1341                    if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1342                        use multi_buffer::ToPoint as _;
1343
1344                        let lhs_end = Point::new(lhs_hunk.row_range.end.0, 0);
1345                        let rhs_end = Point::new(rhs_hunk.row_range.end.0, 0);
1346
1347                        let lhs_excerpt_end = lhs_snapshot
1348                            .anchor_in_excerpt(lhs_hunk.excerpt_range.context.end)
1349                            .unwrap()
1350                            .to_point(&lhs_snapshot);
1351                        let lhs_exceeds = lhs_end >= lhs_excerpt_end;
1352                        let rhs_excerpt_end = rhs_snapshot
1353                            .anchor_in_excerpt(rhs_hunk.excerpt_range.context.end)
1354                            .unwrap()
1355                            .to_point(&rhs_snapshot);
1356                        let rhs_exceeds = rhs_end >= rhs_excerpt_end;
1357                        if lhs_exceeds != rhs_exceeds {
1358                            continue;
1359                        }
1360
1361                        (lhs_end, rhs_end)
1362                    } else {
1363                        (
1364                            Point::new(lhs_hunk.row_range.start.0, 0),
1365                            Point::new(rhs_hunk.row_range.start.0, 0),
1366                        )
1367                    };
1368                let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1369                let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1370                assert_eq!(
1371                    lhs_point.row(),
1372                    rhs_point.row(),
1373                    "mismatch in hunk position"
1374                );
1375            }
1376        }
1377    }
1378
1379    fn debug_print(&self, cx: &mut App) {
1380        use crate::DisplayRow;
1381        use crate::display_map::Block;
1382        use buffer_diff::DiffHunkStatusKind;
1383
1384        assert!(
1385            self.lhs.is_some(),
1386            "debug_print is only useful when lhs editor exists"
1387        );
1388
1389        let lhs = self.lhs.as_ref().unwrap();
1390
1391        // Get terminal width, default to 80 if unavailable
1392        let terminal_width = std::env::var("COLUMNS")
1393            .ok()
1394            .and_then(|s| s.parse::<usize>().ok())
1395            .unwrap_or(80);
1396
1397        // Each side gets half the terminal width minus the separator
1398        let separator = "";
1399        let side_width = (terminal_width - separator.len()) / 2;
1400
1401        // Get display snapshots for both editors
1402        let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1403            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1404        });
1405        let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1406            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1407        });
1408
1409        let lhs_max_row = lhs_snapshot.max_point().row().0;
1410        let rhs_max_row = rhs_snapshot.max_point().row().0;
1411        let max_row = lhs_max_row.max(rhs_max_row);
1412
1413        // Build a map from display row -> block type string
1414        // Each row of a multi-row block gets an entry with the same block type
1415        // For spacers, the ID is included in brackets
1416        fn build_block_map(
1417            snapshot: &crate::DisplaySnapshot,
1418            max_row: u32,
1419        ) -> std::collections::HashMap<u32, String> {
1420            let mut block_map = std::collections::HashMap::new();
1421            for (start_row, block) in
1422                snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1423            {
1424                let (block_type, height) = match block {
1425                    Block::Spacer {
1426                        id,
1427                        height,
1428                        is_below: _,
1429                    } => (format!("SPACER[{}]", id.0), *height),
1430                    Block::ExcerptBoundary { height, .. } => {
1431                        ("EXCERPT_BOUNDARY".to_string(), *height)
1432                    }
1433                    Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1434                    Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1435                    Block::Custom(custom) => {
1436                        ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1437                    }
1438                };
1439                for offset in 0..height {
1440                    block_map.insert(start_row.0 + offset, block_type.clone());
1441                }
1442            }
1443            block_map
1444        }
1445
1446        let lhs_blocks = build_block_map(&lhs_snapshot, lhs_max_row);
1447        let rhs_blocks = build_block_map(&rhs_snapshot, rhs_max_row);
1448
1449        fn display_width(s: &str) -> usize {
1450            unicode_width::UnicodeWidthStr::width(s)
1451        }
1452
1453        fn truncate_line(line: &str, max_width: usize) -> String {
1454            let line_width = display_width(line);
1455            if line_width <= max_width {
1456                return line.to_string();
1457            }
1458            if max_width < 9 {
1459                let mut result = String::new();
1460                let mut width = 0;
1461                for c in line.chars() {
1462                    let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1463                    if width + c_width > max_width {
1464                        break;
1465                    }
1466                    result.push(c);
1467                    width += c_width;
1468                }
1469                return result;
1470            }
1471            let ellipsis = "...";
1472            let target_prefix_width = 3;
1473            let target_suffix_width = 3;
1474
1475            let mut prefix = String::new();
1476            let mut prefix_width = 0;
1477            for c in line.chars() {
1478                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1479                if prefix_width + c_width > target_prefix_width {
1480                    break;
1481                }
1482                prefix.push(c);
1483                prefix_width += c_width;
1484            }
1485
1486            let mut suffix_chars: Vec<char> = Vec::new();
1487            let mut suffix_width = 0;
1488            for c in line.chars().rev() {
1489                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1490                if suffix_width + c_width > target_suffix_width {
1491                    break;
1492                }
1493                suffix_chars.push(c);
1494                suffix_width += c_width;
1495            }
1496            suffix_chars.reverse();
1497            let suffix: String = suffix_chars.into_iter().collect();
1498
1499            format!("{}{}{}", prefix, ellipsis, suffix)
1500        }
1501
1502        fn pad_to_width(s: &str, target_width: usize) -> String {
1503            let current_width = display_width(s);
1504            if current_width >= target_width {
1505                s.to_string()
1506            } else {
1507                format!("{}{}", s, " ".repeat(target_width - current_width))
1508            }
1509        }
1510
1511        // Helper to format a single row for one side
1512        // Format: "ln# diff bytes(cumul) text" or block info
1513        // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1514        fn format_row(
1515            row: u32,
1516            max_row: u32,
1517            snapshot: &crate::DisplaySnapshot,
1518            blocks: &std::collections::HashMap<u32, String>,
1519            row_infos: &[multi_buffer::RowInfo],
1520            cumulative_bytes: &[usize],
1521            side_width: usize,
1522        ) -> String {
1523            // Get row info if available
1524            let row_info = row_infos.get(row as usize);
1525
1526            // Line number prefix (3 chars + space)
1527            // Use buffer_row from RowInfo, which is None for block rows
1528            let line_prefix = if row > max_row {
1529                "    ".to_string()
1530            } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1531                format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1532            } else {
1533                "    ".to_string() // block rows have no line number
1534            };
1535            let content_width = side_width.saturating_sub(line_prefix.len());
1536
1537            if row > max_row {
1538                return format!("{}{}", line_prefix, " ".repeat(content_width));
1539            }
1540
1541            // Check if this row is a block row
1542            if let Some(block_type) = blocks.get(&row) {
1543                let block_str = format!("~~~[{}]~~~", block_type);
1544                let formatted = format!("{:^width$}", block_str, width = content_width);
1545                return format!(
1546                    "{}{}",
1547                    line_prefix,
1548                    truncate_line(&formatted, content_width)
1549                );
1550            }
1551
1552            // Get line text
1553            let line_text = snapshot.line(DisplayRow(row));
1554            let line_bytes = line_text.len();
1555
1556            // Diff status marker
1557            let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1558                Some(status) => match status.kind {
1559                    DiffHunkStatusKind::Added => "+",
1560                    DiffHunkStatusKind::Deleted => "-",
1561                    DiffHunkStatusKind::Modified => "~",
1562                },
1563                None => " ",
1564            };
1565
1566            // Cumulative bytes
1567            let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1568
1569            // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1570            let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1571            let text_width = content_width.saturating_sub(info_prefix.len());
1572            let truncated_text = truncate_line(&line_text, text_width);
1573
1574            let text_part = pad_to_width(&truncated_text, text_width);
1575            format!("{}{}{}", line_prefix, info_prefix, text_part)
1576        }
1577
1578        // Collect row infos for both sides
1579        let lhs_row_infos: Vec<_> = lhs_snapshot
1580            .row_infos(DisplayRow(0))
1581            .take((lhs_max_row + 1) as usize)
1582            .collect();
1583        let rhs_row_infos: Vec<_> = rhs_snapshot
1584            .row_infos(DisplayRow(0))
1585            .take((rhs_max_row + 1) as usize)
1586            .collect();
1587
1588        // Calculate cumulative bytes for each side (only counting non-block rows)
1589        let mut lhs_cumulative = Vec::with_capacity((lhs_max_row + 1) as usize);
1590        let mut cumulative = 0usize;
1591        for row in 0..=lhs_max_row {
1592            if !lhs_blocks.contains_key(&row) {
1593                cumulative += lhs_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1594            }
1595            lhs_cumulative.push(cumulative);
1596        }
1597
1598        let mut rhs_cumulative = Vec::with_capacity((rhs_max_row + 1) as usize);
1599        cumulative = 0;
1600        for row in 0..=rhs_max_row {
1601            if !rhs_blocks.contains_key(&row) {
1602                cumulative += rhs_snapshot.line(DisplayRow(row)).len() + 1;
1603            }
1604            rhs_cumulative.push(cumulative);
1605        }
1606
1607        // Print header
1608        eprintln!();
1609        eprintln!("{}", "".repeat(terminal_width));
1610        let header_left = format!("{:^width$}", "(LHS)", width = side_width);
1611        let header_right = format!("{:^width$}", "(RHS)", width = side_width);
1612        eprintln!("{}{}{}", header_left, separator, header_right);
1613        eprintln!(
1614            "{:^width$}{}{:^width$}",
1615            "ln# diff len(cum) text",
1616            separator,
1617            "ln# diff len(cum) text",
1618            width = side_width
1619        );
1620        eprintln!("{}", "".repeat(terminal_width));
1621
1622        // Print each row
1623        for row in 0..=max_row {
1624            let left = format_row(
1625                row,
1626                lhs_max_row,
1627                &lhs_snapshot,
1628                &lhs_blocks,
1629                &lhs_row_infos,
1630                &lhs_cumulative,
1631                side_width,
1632            );
1633            let right = format_row(
1634                row,
1635                rhs_max_row,
1636                &rhs_snapshot,
1637                &rhs_blocks,
1638                &rhs_row_infos,
1639                &rhs_cumulative,
1640                side_width,
1641            );
1642            eprintln!("{}{}{}", left, separator, right);
1643        }
1644
1645        eprintln!("{}", "".repeat(terminal_width));
1646        eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1647        eprintln!();
1648    }
1649
1650    fn check_excerpt_invariants(&self, quiesced: bool, cx: &gpui::App) {
1651        let lhs = self.lhs.as_ref().expect("should have lhs editor");
1652
1653        let rhs_snapshot = self.rhs_multibuffer.read(cx).snapshot(cx);
1654        let rhs_excerpts = rhs_snapshot.excerpts().collect::<Vec<_>>();
1655        let lhs_snapshot = lhs.multibuffer.read(cx).snapshot(cx);
1656        let lhs_excerpts = lhs_snapshot.excerpts().collect::<Vec<_>>();
1657        assert_eq!(lhs_excerpts.len(), rhs_excerpts.len());
1658
1659        for (lhs_excerpt, rhs_excerpt) in lhs_excerpts.into_iter().zip(rhs_excerpts) {
1660            assert_eq!(
1661                lhs_snapshot
1662                    .path_for_buffer(lhs_excerpt.context.start.buffer_id)
1663                    .unwrap(),
1664                rhs_snapshot
1665                    .path_for_buffer(rhs_excerpt.context.start.buffer_id)
1666                    .unwrap(),
1667                "corresponding excerpts should have the same path"
1668            );
1669            let diff = self
1670                .rhs_multibuffer
1671                .read(cx)
1672                .diff_for(rhs_excerpt.context.start.buffer_id)
1673                .expect("missing diff");
1674            assert_eq!(
1675                lhs_excerpt.context.start.buffer_id,
1676                diff.read(cx).base_text(cx).remote_id(),
1677                "corresponding lhs excerpt should show diff base text"
1678            );
1679
1680            if quiesced {
1681                let diff_snapshot = diff.read(cx).snapshot(cx);
1682                let lhs_buffer_snapshot = lhs_snapshot
1683                    .buffer_for_id(lhs_excerpt.context.start.buffer_id)
1684                    .unwrap();
1685                let rhs_buffer_snapshot = rhs_snapshot
1686                    .buffer_for_id(rhs_excerpt.context.start.buffer_id)
1687                    .unwrap();
1688                let lhs_range = lhs_excerpt.context.to_point(&lhs_buffer_snapshot);
1689                let rhs_range = rhs_excerpt.context.to_point(&rhs_buffer_snapshot);
1690                let expected_lhs_range = buffer_range_to_base_text_range(
1691                    &rhs_range,
1692                    &diff_snapshot,
1693                    &rhs_buffer_snapshot,
1694                );
1695                assert_eq!(
1696                    lhs_range, expected_lhs_range,
1697                    "corresponding lhs excerpt should have a matching range"
1698                )
1699            }
1700        }
1701    }
1702}
1703
1704impl Item for SplittableEditor {
1705    type Event = EditorEvent;
1706
1707    fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1708        self.rhs_editor.read(cx).tab_content_text(detail, cx)
1709    }
1710
1711    fn tab_tooltip_text(&self, cx: &App) -> Option<ui::SharedString> {
1712        self.rhs_editor.read(cx).tab_tooltip_text(cx)
1713    }
1714
1715    fn tab_icon(&self, window: &Window, cx: &App) -> Option<ui::Icon> {
1716        self.rhs_editor.read(cx).tab_icon(window, cx)
1717    }
1718
1719    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> gpui::AnyElement {
1720        self.rhs_editor.read(cx).tab_content(params, window, cx)
1721    }
1722
1723    fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
1724        Editor::to_item_events(event, f)
1725    }
1726
1727    fn for_each_project_item(
1728        &self,
1729        cx: &App,
1730        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1731    ) {
1732        self.rhs_editor.read(cx).for_each_project_item(cx, f)
1733    }
1734
1735    fn buffer_kind(&self, cx: &App) -> ItemBufferKind {
1736        self.rhs_editor.read(cx).buffer_kind(cx)
1737    }
1738
1739    fn is_dirty(&self, cx: &App) -> bool {
1740        self.rhs_editor.read(cx).is_dirty(cx)
1741    }
1742
1743    fn has_conflict(&self, cx: &App) -> bool {
1744        self.rhs_editor.read(cx).has_conflict(cx)
1745    }
1746
1747    fn has_deleted_file(&self, cx: &App) -> bool {
1748        self.rhs_editor.read(cx).has_deleted_file(cx)
1749    }
1750
1751    fn capability(&self, cx: &App) -> language::Capability {
1752        self.rhs_editor.read(cx).capability(cx)
1753    }
1754
1755    fn can_save(&self, cx: &App) -> bool {
1756        self.rhs_editor.read(cx).can_save(cx)
1757    }
1758
1759    fn can_save_as(&self, cx: &App) -> bool {
1760        self.rhs_editor.read(cx).can_save_as(cx)
1761    }
1762
1763    fn save(
1764        &mut self,
1765        options: SaveOptions,
1766        project: Entity<Project>,
1767        window: &mut Window,
1768        cx: &mut Context<Self>,
1769    ) -> gpui::Task<anyhow::Result<()>> {
1770        self.rhs_editor
1771            .update(cx, |editor, cx| editor.save(options, project, window, cx))
1772    }
1773
1774    fn save_as(
1775        &mut self,
1776        project: Entity<Project>,
1777        path: project::ProjectPath,
1778        window: &mut Window,
1779        cx: &mut Context<Self>,
1780    ) -> gpui::Task<anyhow::Result<()>> {
1781        self.rhs_editor
1782            .update(cx, |editor, cx| editor.save_as(project, path, window, cx))
1783    }
1784
1785    fn reload(
1786        &mut self,
1787        project: Entity<Project>,
1788        window: &mut Window,
1789        cx: &mut Context<Self>,
1790    ) -> gpui::Task<anyhow::Result<()>> {
1791        self.rhs_editor
1792            .update(cx, |editor, cx| editor.reload(project, window, cx))
1793    }
1794
1795    fn navigate(
1796        &mut self,
1797        data: Arc<dyn std::any::Any + Send>,
1798        window: &mut Window,
1799        cx: &mut Context<Self>,
1800    ) -> bool {
1801        self.focused_editor()
1802            .update(cx, |editor, cx| editor.navigate(data, window, cx))
1803    }
1804
1805    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1806        self.focused_editor().update(cx, |editor, cx| {
1807            editor.deactivated(window, cx);
1808        });
1809    }
1810
1811    fn added_to_workspace(
1812        &mut self,
1813        workspace: &mut Workspace,
1814        window: &mut Window,
1815        cx: &mut Context<Self>,
1816    ) {
1817        self.workspace = workspace.weak_handle();
1818        self.rhs_editor.update(cx, |rhs_editor, cx| {
1819            rhs_editor.added_to_workspace(workspace, window, cx);
1820        });
1821        if let Some(lhs) = &self.lhs {
1822            lhs.editor.update(cx, |lhs_editor, cx| {
1823                lhs_editor.added_to_workspace(workspace, window, cx);
1824            });
1825        }
1826    }
1827
1828    fn as_searchable(
1829        &self,
1830        handle: &Entity<Self>,
1831        _: &App,
1832    ) -> Option<Box<dyn SearchableItemHandle>> {
1833        Some(Box::new(handle.clone()))
1834    }
1835
1836    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1837        self.rhs_editor.read(cx).breadcrumb_location(cx)
1838    }
1839
1840    fn breadcrumbs(&self, cx: &App) -> Option<(Vec<HighlightedText>, Option<Font>)> {
1841        self.rhs_editor.read(cx).breadcrumbs(cx)
1842    }
1843
1844    fn pixel_position_of_cursor(&self, cx: &App) -> Option<gpui::Point<gpui::Pixels>> {
1845        self.focused_editor().read(cx).pixel_position_of_cursor(cx)
1846    }
1847
1848    fn act_as_type<'a>(
1849        &'a self,
1850        type_id: std::any::TypeId,
1851        self_handle: &'a Entity<Self>,
1852        _: &'a App,
1853    ) -> Option<gpui::AnyEntity> {
1854        if type_id == std::any::TypeId::of::<Self>() {
1855            Some(self_handle.clone().into())
1856        } else if type_id == std::any::TypeId::of::<Editor>() {
1857            Some(self.rhs_editor.clone().into())
1858        } else {
1859            None
1860        }
1861    }
1862}
1863
1864impl SearchableItem for SplittableEditor {
1865    type Match = Range<Anchor>;
1866
1867    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1868        self.rhs_editor.update(cx, |editor, cx| {
1869            editor.clear_matches(window, cx);
1870        });
1871        if let Some(lhs_editor) = self.lhs_editor() {
1872            lhs_editor.update(cx, |editor, cx| {
1873                editor.clear_matches(window, cx);
1874            })
1875        }
1876    }
1877
1878    fn update_matches(
1879        &mut self,
1880        matches: &[Self::Match],
1881        active_match_index: Option<usize>,
1882        token: SearchToken,
1883        window: &mut Window,
1884        cx: &mut Context<Self>,
1885    ) {
1886        let Some(target) = self.editor_for_token(token) else {
1887            return;
1888        };
1889        target.update(cx, |editor, cx| {
1890            editor.update_matches(matches, active_match_index, token, window, cx);
1891        });
1892    }
1893
1894    fn search_bar_visibility_changed(
1895        &mut self,
1896        visible: bool,
1897        window: &mut Window,
1898        cx: &mut Context<Self>,
1899    ) {
1900        if visible {
1901            let side = self.focused_side();
1902            self.searched_side = Some(side);
1903            match side {
1904                SplitSide::Left => {
1905                    self.rhs_editor.update(cx, |editor, cx| {
1906                        editor.clear_matches(window, cx);
1907                    });
1908                }
1909                SplitSide::Right => {
1910                    if let Some(lhs) = &self.lhs {
1911                        lhs.editor.update(cx, |editor, cx| {
1912                            editor.clear_matches(window, cx);
1913                        });
1914                    }
1915                }
1916            }
1917        } else {
1918            self.searched_side = None;
1919        }
1920    }
1921
1922    fn query_suggestion(
1923        &mut self,
1924        ignore_settings: bool,
1925        window: &mut Window,
1926        cx: &mut Context<Self>,
1927    ) -> String {
1928        self.focused_editor().update(cx, |editor, cx| {
1929            editor.query_suggestion(ignore_settings, window, cx)
1930        })
1931    }
1932
1933    fn activate_match(
1934        &mut self,
1935        index: usize,
1936        matches: &[Self::Match],
1937        token: SearchToken,
1938        window: &mut Window,
1939        cx: &mut Context<Self>,
1940    ) {
1941        let Some(target) = self.editor_for_token(token) else {
1942            return;
1943        };
1944        target.update(cx, |editor, cx| {
1945            editor.activate_match(index, matches, token, window, cx);
1946        });
1947    }
1948
1949    fn select_matches(
1950        &mut self,
1951        matches: &[Self::Match],
1952        token: SearchToken,
1953        window: &mut Window,
1954        cx: &mut Context<Self>,
1955    ) {
1956        let Some(target) = self.editor_for_token(token) else {
1957            return;
1958        };
1959        target.update(cx, |editor, cx| {
1960            editor.select_matches(matches, token, window, cx);
1961        });
1962    }
1963
1964    fn replace(
1965        &mut self,
1966        identifier: &Self::Match,
1967        query: &project::search::SearchQuery,
1968        token: SearchToken,
1969        window: &mut Window,
1970        cx: &mut Context<Self>,
1971    ) {
1972        let Some(target) = self.editor_for_token(token) else {
1973            return;
1974        };
1975        target.update(cx, |editor, cx| {
1976            editor.replace(identifier, query, token, window, cx);
1977        });
1978    }
1979
1980    fn find_matches(
1981        &mut self,
1982        query: Arc<project::search::SearchQuery>,
1983        window: &mut Window,
1984        cx: &mut Context<Self>,
1985    ) -> gpui::Task<Vec<Self::Match>> {
1986        self.focused_editor()
1987            .update(cx, |editor, cx| editor.find_matches(query, window, cx))
1988    }
1989
1990    fn find_matches_with_token(
1991        &mut self,
1992        query: Arc<project::search::SearchQuery>,
1993        window: &mut Window,
1994        cx: &mut Context<Self>,
1995    ) -> gpui::Task<(Vec<Self::Match>, SearchToken)> {
1996        let token = self.search_token();
1997        let editor = self.focused_editor().downgrade();
1998        cx.spawn_in(window, async move |_, cx| {
1999            let Some(matches) = editor
2000                .update_in(cx, |editor, window, cx| {
2001                    editor.find_matches(query, window, cx)
2002                })
2003                .ok()
2004            else {
2005                return (Vec::new(), token);
2006            };
2007            (matches.await, token)
2008        })
2009    }
2010
2011    fn active_match_index(
2012        &mut self,
2013        direction: workspace::searchable::Direction,
2014        matches: &[Self::Match],
2015        token: SearchToken,
2016        window: &mut Window,
2017        cx: &mut Context<Self>,
2018    ) -> Option<usize> {
2019        self.editor_for_token(token)?.update(cx, |editor, cx| {
2020            editor.active_match_index(direction, matches, token, window, cx)
2021        })
2022    }
2023}
2024
2025impl EventEmitter<EditorEvent> for SplittableEditor {}
2026impl EventEmitter<SearchEvent> for SplittableEditor {}
2027impl Focusable for SplittableEditor {
2028    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
2029        self.focused_editor().read(cx).focus_handle(cx)
2030    }
2031}
2032
2033impl Render for SplittableEditor {
2034    fn render(
2035        &mut self,
2036        _window: &mut ui::Window,
2037        cx: &mut ui::Context<Self>,
2038    ) -> impl ui::IntoElement {
2039        let is_split = self.lhs.is_some();
2040        let inner = if is_split {
2041            let style = self.rhs_editor.read(cx).create_style(cx);
2042            SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
2043        } else {
2044            self.rhs_editor.clone().into_any_element()
2045        };
2046
2047        let this = cx.entity().downgrade();
2048        let last_width = self.last_width;
2049
2050        div()
2051            .id("splittable-editor")
2052            .on_action(cx.listener(Self::toggle_split))
2053            .on_action(cx.listener(Self::activate_pane_left))
2054            .on_action(cx.listener(Self::activate_pane_right))
2055            .on_action(cx.listener(Self::intercept_toggle_breakpoint))
2056            .on_action(cx.listener(Self::intercept_enable_breakpoint))
2057            .on_action(cx.listener(Self::intercept_disable_breakpoint))
2058            .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
2059            .on_action(cx.listener(Self::intercept_inline_assist))
2060            .capture_action(cx.listener(Self::toggle_soft_wrap))
2061            .size_full()
2062            .child(inner)
2063            .child(
2064                canvas(
2065                    move |bounds, window, cx| {
2066                        let width = bounds.size.width;
2067                        if last_width == Some(width) {
2068                            return;
2069                        }
2070                        window.defer(cx, move |window, cx| {
2071                            this.update(cx, |this, cx| {
2072                                this.width_changed(width, window, cx);
2073                            })
2074                            .ok();
2075                        });
2076                    },
2077                    |_, _, _, _| {},
2078                )
2079                .absolute()
2080                .size_full(),
2081            )
2082    }
2083}
2084
2085#[cfg(test)]
2086mod tests {
2087    use std::{any::TypeId, sync::Arc};
2088
2089    use buffer_diff::BufferDiff;
2090    use collections::{HashMap, HashSet};
2091    use fs::FakeFs;
2092    use gpui::Element as _;
2093    use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
2094    use language::language_settings::SoftWrap;
2095    use language::{Buffer, Capability};
2096    use multi_buffer::{MultiBuffer, PathKey};
2097    use pretty_assertions::assert_eq;
2098    use project::Project;
2099    use rand::rngs::StdRng;
2100    use settings::{DiffViewStyle, SettingsStore};
2101    use ui::{VisualContext as _, div, px};
2102    use util::rel_path::rel_path;
2103    use workspace::{Item, MultiWorkspace};
2104
2105    use crate::display_map::{
2106        BlockPlacement, BlockProperties, BlockStyle, Crease, FoldPlaceholder,
2107    };
2108    use crate::inlays::Inlay;
2109    use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
2110    use crate::{Editor, SplittableEditor};
2111    use multi_buffer::MultiBufferOffset;
2112
2113    async fn init_test(
2114        cx: &mut gpui::TestAppContext,
2115        soft_wrap: SoftWrap,
2116        style: DiffViewStyle,
2117    ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
2118        cx.update(|cx| {
2119            let store = SettingsStore::test(cx);
2120            cx.set_global(store);
2121            theme_settings::init(theme::LoadThemes::JustBase, cx);
2122            crate::init(cx);
2123        });
2124        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
2125        let (multi_workspace, cx) =
2126            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2127        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2128        let rhs_multibuffer = cx.new(|cx| {
2129            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2130            multibuffer.set_all_diff_hunks_expanded(cx);
2131            multibuffer
2132        });
2133        let editor = cx.new_window_entity(|window, cx| {
2134            let editor = SplittableEditor::new(
2135                style,
2136                rhs_multibuffer.clone(),
2137                project.clone(),
2138                workspace,
2139                window,
2140                cx,
2141            );
2142            editor.update_editors(cx, |editor, cx| {
2143                editor.set_soft_wrap_mode(soft_wrap, cx);
2144            });
2145            editor
2146        });
2147        (editor, cx)
2148    }
2149
2150    fn buffer_with_diff(
2151        base_text: &str,
2152        current_text: &str,
2153        cx: &mut VisualTestContext,
2154    ) -> (Entity<Buffer>, Entity<BufferDiff>) {
2155        let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2156        let diff = cx.new(|cx| {
2157            BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
2158        });
2159        (buffer, diff)
2160    }
2161
2162    #[track_caller]
2163    fn assert_split_content(
2164        editor: &Entity<SplittableEditor>,
2165        expected_rhs: String,
2166        expected_lhs: String,
2167        cx: &mut VisualTestContext,
2168    ) {
2169        assert_split_content_with_widths(
2170            editor,
2171            px(3000.0),
2172            px(3000.0),
2173            expected_rhs,
2174            expected_lhs,
2175            cx,
2176        );
2177    }
2178
2179    #[track_caller]
2180    fn assert_split_content_with_widths(
2181        editor: &Entity<SplittableEditor>,
2182        rhs_width: Pixels,
2183        lhs_width: Pixels,
2184        expected_rhs: String,
2185        expected_lhs: String,
2186        cx: &mut VisualTestContext,
2187    ) {
2188        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2189            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2190            (editor.rhs_editor.clone(), lhs.editor.clone())
2191        });
2192
2193        // Make sure both sides learn if the other has soft-wrapped
2194        let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2195        cx.run_until_parked();
2196        let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2197        cx.run_until_parked();
2198
2199        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2200        let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2201
2202        if rhs_content != expected_rhs || lhs_content != expected_lhs {
2203            editor.update(cx, |editor, cx| editor.debug_print(cx));
2204        }
2205
2206        assert_eq!(rhs_content, expected_rhs, "rhs");
2207        assert_eq!(lhs_content, expected_lhs, "lhs");
2208    }
2209
2210    #[gpui::test(iterations = 25)]
2211    async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2212        use multi_buffer::ExpandExcerptDirection;
2213        use rand::prelude::*;
2214        use util::RandomCharIter;
2215
2216        let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2217        let operations = std::env::var("OPERATIONS")
2218            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2219            .unwrap_or(10);
2220        let rng = &mut rng;
2221        for _ in 0..operations {
2222            let buffers = editor.update(cx, |editor, cx| {
2223                editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2224            });
2225
2226            if buffers.is_empty() {
2227                log::info!("creating initial buffer");
2228                let len = rng.random_range(200..1000);
2229                let base_text: String = RandomCharIter::new(&mut *rng).take(len).collect();
2230                let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
2231                let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2232                let diff =
2233                    cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_snapshot, cx));
2234                let edit_count = rng.random_range(3..8);
2235                buffer.update(cx, |buffer, cx| {
2236                    buffer.randomly_edit(rng, edit_count, cx);
2237                });
2238                let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2239                diff.update(cx, |diff, cx| {
2240                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
2241                });
2242                let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2243                let ranges = diff_snapshot
2244                    .hunks(&buffer_snapshot)
2245                    .map(|hunk| hunk.range)
2246                    .collect::<Vec<_>>();
2247                let context_lines = rng.random_range(0..2);
2248                editor.update(cx, |editor, cx| {
2249                    let path = PathKey::for_buffer(&buffer, cx);
2250                    editor.update_excerpts_for_path(path, buffer, ranges, context_lines, diff, cx);
2251                });
2252                editor.update(cx, |editor, cx| {
2253                    editor.check_invariants(true, cx);
2254                });
2255                continue;
2256            }
2257
2258            let mut quiesced = false;
2259
2260            match rng.random_range(0..100) {
2261                0..=14 if buffers.len() < 6 => {
2262                    log::info!("creating new buffer and setting excerpts");
2263                    let len = rng.random_range(200..1000);
2264                    let base_text: String = RandomCharIter::new(&mut *rng).take(len).collect();
2265                    let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
2266                    let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2267                    let diff = cx
2268                        .new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_snapshot, cx));
2269                    let edit_count = rng.random_range(3..8);
2270                    buffer.update(cx, |buffer, cx| {
2271                        buffer.randomly_edit(rng, edit_count, cx);
2272                    });
2273                    let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2274                    diff.update(cx, |diff, cx| {
2275                        diff.recalculate_diff_sync(&buffer_snapshot, cx);
2276                    });
2277                    let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2278                    let ranges = diff_snapshot
2279                        .hunks(&buffer_snapshot)
2280                        .map(|hunk| hunk.range)
2281                        .collect::<Vec<_>>();
2282                    let context_lines = rng.random_range(0..2);
2283                    editor.update(cx, |editor, cx| {
2284                        let path = PathKey::for_buffer(&buffer, cx);
2285                        editor.update_excerpts_for_path(
2286                            path,
2287                            buffer,
2288                            ranges,
2289                            context_lines,
2290                            diff,
2291                            cx,
2292                        );
2293                    });
2294                }
2295                15..=29 => {
2296                    log::info!("randomly editing multibuffer");
2297                    let edit_count = rng.random_range(1..5);
2298                    editor.update(cx, |editor, cx| {
2299                        editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2300                            multibuffer.randomly_edit(rng, edit_count, cx);
2301                        });
2302                    });
2303                }
2304                30..=44 => {
2305                    log::info!("randomly editing individual buffer");
2306                    let buffer = buffers.iter().choose(rng).unwrap();
2307                    let edit_count = rng.random_range(1..3);
2308                    buffer.update(cx, |buffer, cx| {
2309                        buffer.randomly_edit(rng, edit_count, cx);
2310                    });
2311                }
2312                45..=54 => {
2313                    log::info!("recalculating diff and resetting excerpts for single buffer");
2314                    let buffer = buffers.iter().choose(rng).unwrap();
2315                    let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2316                    let diff = editor.update(cx, |editor, cx| {
2317                        editor
2318                            .rhs_multibuffer
2319                            .read(cx)
2320                            .diff_for(buffer.read(cx).remote_id())
2321                            .unwrap()
2322                    });
2323                    diff.update(cx, |diff, cx| {
2324                        diff.recalculate_diff_sync(&buffer_snapshot, cx);
2325                    });
2326                    cx.run_until_parked();
2327                    let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2328                    let ranges = diff_snapshot
2329                        .hunks(&buffer_snapshot)
2330                        .map(|hunk| hunk.range)
2331                        .collect::<Vec<_>>();
2332                    let context_lines = rng.random_range(0..2);
2333                    let buffer = buffer.clone();
2334                    editor.update(cx, |editor, cx| {
2335                        let path = PathKey::for_buffer(&buffer, cx);
2336                        editor.update_excerpts_for_path(
2337                            path,
2338                            buffer,
2339                            ranges,
2340                            context_lines,
2341                            diff,
2342                            cx,
2343                        );
2344                    });
2345                }
2346                55..=64 => {
2347                    log::info!("randomly undoing/redoing in single buffer");
2348                    let buffer = buffers.iter().choose(rng).unwrap();
2349                    buffer.update(cx, |buffer, cx| {
2350                        buffer.randomly_undo_redo(rng, cx);
2351                    });
2352                }
2353                65..=74 => {
2354                    log::info!("removing excerpts for a random path");
2355                    let ids = editor.update(cx, |editor, cx| {
2356                        let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
2357                        snapshot.all_buffer_ids().collect::<Vec<_>>()
2358                    });
2359                    if let Some(id) = ids.choose(rng) {
2360                        editor.update(cx, |editor, cx| {
2361                            let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
2362                            let path = snapshot.path_for_buffer(*id).unwrap();
2363                            editor.remove_excerpts_for_path(path.clone(), cx);
2364                        });
2365                    }
2366                }
2367                75..=79 => {
2368                    log::info!("unsplit and resplit");
2369                    editor.update_in(cx, |editor, window, cx| {
2370                        editor.unsplit(window, cx);
2371                    });
2372                    cx.run_until_parked();
2373                    editor.update_in(cx, |editor, window, cx| {
2374                        editor.split(window, cx);
2375                    });
2376                }
2377                80..=89 => {
2378                    let snapshot = editor.update(cx, |editor, cx| {
2379                        editor.rhs_multibuffer.read(cx).snapshot(cx)
2380                    });
2381                    let excerpts = snapshot.excerpts().collect::<Vec<_>>();
2382                    if !excerpts.is_empty() {
2383                        let count = rng.random_range(1..=excerpts.len().min(3));
2384                        let chosen: Vec<_> =
2385                            excerpts.choose_multiple(rng, count).cloned().collect();
2386                        let line_count = rng.random_range(1..5);
2387                        log::info!("expanding {count} excerpts by {line_count} lines");
2388                        editor.update(cx, |editor, cx| {
2389                            editor.expand_excerpts(
2390                                chosen.into_iter().map(|excerpt| {
2391                                    snapshot.anchor_in_excerpt(excerpt.context.start).unwrap()
2392                                }),
2393                                line_count,
2394                                ExpandExcerptDirection::UpAndDown,
2395                                cx,
2396                            );
2397                        });
2398                    }
2399                }
2400                _ => {
2401                    log::info!("quiescing");
2402                    for buffer in buffers {
2403                        let buffer_snapshot =
2404                            buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2405                        let diff = editor.update(cx, |editor, cx| {
2406                            editor
2407                                .rhs_multibuffer
2408                                .read(cx)
2409                                .diff_for(buffer.read(cx).remote_id())
2410                                .unwrap()
2411                        });
2412                        diff.update(cx, |diff, cx| {
2413                            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2414                        });
2415                        cx.run_until_parked();
2416                        let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2417                        let ranges = diff_snapshot
2418                            .hunks(&buffer_snapshot)
2419                            .map(|hunk| hunk.range)
2420                            .collect::<Vec<_>>();
2421                        editor.update(cx, |editor, cx| {
2422                            let path = PathKey::for_buffer(&buffer, cx);
2423                            editor.update_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2424                        });
2425                    }
2426                    quiesced = true;
2427                }
2428            }
2429
2430            editor.update(cx, |editor, cx| {
2431                editor.check_invariants(quiesced, cx);
2432            });
2433        }
2434    }
2435
2436    #[gpui::test]
2437    async fn test_expand_excerpt_with_hunk_before_excerpt_start(cx: &mut gpui::TestAppContext) {
2438        use rope::Point;
2439
2440        let (editor, cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
2441
2442        let base_text = "aaaaaaa rest_of_line\nsecond_line\nthird_line\nfourth_line";
2443        let current_text = "aaaaaaa rest_of_line\nsecond_line\nMODIFIED\nfourth_line";
2444        let (buffer, diff) = buffer_with_diff(base_text, current_text, cx);
2445
2446        let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2447        diff.update(cx, |diff, cx| {
2448            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2449        });
2450        cx.run_until_parked();
2451
2452        let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2453        let ranges = diff_snapshot
2454            .hunks(&buffer_snapshot)
2455            .map(|hunk| hunk.range)
2456            .collect::<Vec<_>>();
2457
2458        editor.update(cx, |editor, cx| {
2459            let path = PathKey::for_buffer(&buffer, cx);
2460            editor.update_excerpts_for_path(path, buffer.clone(), ranges, 0, diff.clone(), cx);
2461        });
2462        cx.run_until_parked();
2463
2464        buffer.update(cx, |buffer, cx| {
2465            buffer.edit(
2466                [(Point::new(0, 7)..Point::new(1, 7), "\nnew_line\n")],
2467                None,
2468                cx,
2469            );
2470        });
2471
2472        let excerpts = editor.update(cx, |editor, cx| {
2473            let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
2474            snapshot
2475                .excerpts()
2476                .map(|excerpt| snapshot.anchor_in_excerpt(excerpt.context.start).unwrap())
2477                .collect::<Vec<_>>()
2478        });
2479        editor.update(cx, |editor, cx| {
2480            editor.expand_excerpts(
2481                excerpts.into_iter(),
2482                2,
2483                multi_buffer::ExpandExcerptDirection::UpAndDown,
2484                cx,
2485            );
2486        });
2487    }
2488
2489    #[gpui::test]
2490    async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2491        use rope::Point;
2492        use unindent::Unindent as _;
2493
2494        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2495
2496        let base_text = "
2497            aaa
2498            bbb
2499            ccc
2500            ddd
2501            eee
2502            fff
2503        "
2504        .unindent();
2505        let current_text = "
2506            aaa
2507            ddd
2508            eee
2509            fff
2510        "
2511        .unindent();
2512
2513        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2514
2515        editor.update(cx, |editor, cx| {
2516            let path = PathKey::for_buffer(&buffer, cx);
2517            editor.update_excerpts_for_path(
2518                path,
2519                buffer.clone(),
2520                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2521                0,
2522                diff.clone(),
2523                cx,
2524            );
2525        });
2526
2527        cx.run_until_parked();
2528
2529        assert_split_content(
2530            &editor,
2531            "
2532            § <no file>
2533            § -----
2534            aaa
2535            § spacer
2536            § spacer
2537            ddd
2538            eee
2539            fff"
2540            .unindent(),
2541            "
2542            § <no file>
2543            § -----
2544            aaa
2545            bbb
2546            ccc
2547            ddd
2548            eee
2549            fff"
2550            .unindent(),
2551            &mut cx,
2552        );
2553
2554        buffer.update(cx, |buffer, cx| {
2555            buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2556        });
2557
2558        cx.run_until_parked();
2559
2560        assert_split_content(
2561            &editor,
2562            "
2563            § <no file>
2564            § -----
2565            aaa
2566            § spacer
2567            § spacer
2568            ddd
2569            eee
2570            FFF"
2571            .unindent(),
2572            "
2573            § <no file>
2574            § -----
2575            aaa
2576            bbb
2577            ccc
2578            ddd
2579            eee
2580            fff"
2581            .unindent(),
2582            &mut cx,
2583        );
2584
2585        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2586        diff.update(cx, |diff, cx| {
2587            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2588        });
2589
2590        cx.run_until_parked();
2591
2592        assert_split_content(
2593            &editor,
2594            "
2595            § <no file>
2596            § -----
2597            aaa
2598            § spacer
2599            § spacer
2600            ddd
2601            eee
2602            FFF"
2603            .unindent(),
2604            "
2605            § <no file>
2606            § -----
2607            aaa
2608            bbb
2609            ccc
2610            ddd
2611            eee
2612            fff"
2613            .unindent(),
2614            &mut cx,
2615        );
2616    }
2617
2618    #[gpui::test]
2619    async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2620        use rope::Point;
2621        use unindent::Unindent as _;
2622
2623        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2624
2625        let base_text1 = "
2626            aaa
2627            bbb
2628            ccc
2629            ddd
2630            eee"
2631        .unindent();
2632
2633        let base_text2 = "
2634            fff
2635            ggg
2636            hhh
2637            iii
2638            jjj"
2639        .unindent();
2640
2641        let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2642        let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2643
2644        editor.update(cx, |editor, cx| {
2645            let path1 = PathKey::for_buffer(&buffer1, cx);
2646            editor.update_excerpts_for_path(
2647                path1,
2648                buffer1.clone(),
2649                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2650                0,
2651                diff1.clone(),
2652                cx,
2653            );
2654            let path2 = PathKey::for_buffer(&buffer2, cx);
2655            editor.update_excerpts_for_path(
2656                path2,
2657                buffer2.clone(),
2658                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2659                1,
2660                diff2.clone(),
2661                cx,
2662            );
2663        });
2664
2665        cx.run_until_parked();
2666
2667        buffer1.update(cx, |buffer, cx| {
2668            buffer.edit(
2669                [
2670                    (Point::new(0, 0)..Point::new(1, 0), ""),
2671                    (Point::new(3, 0)..Point::new(4, 0), ""),
2672                ],
2673                None,
2674                cx,
2675            );
2676        });
2677        buffer2.update(cx, |buffer, cx| {
2678            buffer.edit(
2679                [
2680                    (Point::new(0, 0)..Point::new(1, 0), ""),
2681                    (Point::new(3, 0)..Point::new(4, 0), ""),
2682                ],
2683                None,
2684                cx,
2685            );
2686        });
2687
2688        cx.run_until_parked();
2689
2690        assert_split_content(
2691            &editor,
2692            "
2693            § <no file>
2694            § -----
2695            § spacer
2696            bbb
2697            ccc
2698            § spacer
2699            eee
2700            § <no file>
2701            § -----
2702            § spacer
2703            ggg
2704            hhh
2705            § spacer
2706            jjj"
2707            .unindent(),
2708            "
2709            § <no file>
2710            § -----
2711            aaa
2712            bbb
2713            ccc
2714            ddd
2715            eee
2716            § <no file>
2717            § -----
2718            fff
2719            ggg
2720            hhh
2721            iii
2722            jjj"
2723            .unindent(),
2724            &mut cx,
2725        );
2726
2727        let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2728        diff1.update(cx, |diff, cx| {
2729            diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2730        });
2731        let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2732        diff2.update(cx, |diff, cx| {
2733            diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2734        });
2735
2736        cx.run_until_parked();
2737
2738        assert_split_content(
2739            &editor,
2740            "
2741            § <no file>
2742            § -----
2743            § spacer
2744            bbb
2745            ccc
2746            § spacer
2747            eee
2748            § <no file>
2749            § -----
2750            § spacer
2751            ggg
2752            hhh
2753            § spacer
2754            jjj"
2755            .unindent(),
2756            "
2757            § <no file>
2758            § -----
2759            aaa
2760            bbb
2761            ccc
2762            ddd
2763            eee
2764            § <no file>
2765            § -----
2766            fff
2767            ggg
2768            hhh
2769            iii
2770            jjj"
2771            .unindent(),
2772            &mut cx,
2773        );
2774    }
2775
2776    #[gpui::test]
2777    async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2778        use rope::Point;
2779        use unindent::Unindent as _;
2780
2781        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2782
2783        let base_text = "
2784            aaa
2785            bbb
2786            ccc
2787            ddd
2788        "
2789        .unindent();
2790
2791        let current_text = "
2792            aaa
2793            NEW1
2794            NEW2
2795            ccc
2796            ddd
2797        "
2798        .unindent();
2799
2800        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2801
2802        editor.update(cx, |editor, cx| {
2803            let path = PathKey::for_buffer(&buffer, cx);
2804            editor.update_excerpts_for_path(
2805                path,
2806                buffer.clone(),
2807                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2808                0,
2809                diff.clone(),
2810                cx,
2811            );
2812        });
2813
2814        cx.run_until_parked();
2815
2816        assert_split_content(
2817            &editor,
2818            "
2819            § <no file>
2820            § -----
2821            aaa
2822            NEW1
2823            NEW2
2824            ccc
2825            ddd"
2826            .unindent(),
2827            "
2828            § <no file>
2829            § -----
2830            aaa
2831            bbb
2832            § spacer
2833            ccc
2834            ddd"
2835            .unindent(),
2836            &mut cx,
2837        );
2838
2839        buffer.update(cx, |buffer, cx| {
2840            buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2841        });
2842
2843        cx.run_until_parked();
2844
2845        assert_split_content(
2846            &editor,
2847            "
2848            § <no file>
2849            § -----
2850            aaa
2851            NEW1
2852            ccc
2853            ddd"
2854            .unindent(),
2855            "
2856            § <no file>
2857            § -----
2858            aaa
2859            bbb
2860            ccc
2861            ddd"
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(
2874            &editor,
2875            "
2876            § <no file>
2877            § -----
2878            aaa
2879            NEW1
2880            ccc
2881            ddd"
2882            .unindent(),
2883            "
2884            § <no file>
2885            § -----
2886            aaa
2887            bbb
2888            ccc
2889            ddd"
2890            .unindent(),
2891            &mut cx,
2892        );
2893    }
2894
2895    #[gpui::test]
2896    async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2897        use rope::Point;
2898        use unindent::Unindent as _;
2899
2900        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2901
2902        let base_text = "
2903            aaa
2904            bbb
2905
2906
2907
2908
2909
2910            ccc
2911            ddd
2912        "
2913        .unindent();
2914        let current_text = "
2915            aaa
2916            bbb
2917
2918
2919
2920
2921
2922            CCC
2923            ddd
2924        "
2925        .unindent();
2926
2927        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
2928
2929        editor.update(cx, |editor, cx| {
2930            let path = PathKey::for_buffer(&buffer, cx);
2931            editor.update_excerpts_for_path(
2932                path,
2933                buffer.clone(),
2934                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2935                0,
2936                diff.clone(),
2937                cx,
2938            );
2939        });
2940
2941        cx.run_until_parked();
2942
2943        buffer.update(cx, |buffer, cx| {
2944            buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2945        });
2946
2947        cx.run_until_parked();
2948
2949        assert_split_content(
2950            &editor,
2951            "
2952            § <no file>
2953            § -----
2954            aaa
2955            bbb
2956
2957
2958
2959
2960
2961
2962            CCC
2963            ddd"
2964            .unindent(),
2965            "
2966            § <no file>
2967            § -----
2968            aaa
2969            bbb
2970            § spacer
2971
2972
2973
2974
2975
2976            ccc
2977            ddd"
2978            .unindent(),
2979            &mut cx,
2980        );
2981
2982        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2983        diff.update(cx, |diff, cx| {
2984            diff.recalculate_diff_sync(&buffer_snapshot, cx);
2985        });
2986
2987        cx.run_until_parked();
2988
2989        assert_split_content(
2990            &editor,
2991            "
2992            § <no file>
2993            § -----
2994            aaa
2995            bbb
2996
2997
2998
2999
3000
3001
3002            CCC
3003            ddd"
3004            .unindent(),
3005            "
3006            § <no file>
3007            § -----
3008            aaa
3009            bbb
3010
3011
3012
3013
3014
3015            ccc
3016            § spacer
3017            ddd"
3018            .unindent(),
3019            &mut cx,
3020        );
3021    }
3022
3023    #[gpui::test]
3024    async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
3025        use git::Restore;
3026        use rope::Point;
3027        use unindent::Unindent as _;
3028
3029        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3030
3031        let base_text = "
3032            aaa
3033            bbb
3034            ccc
3035            ddd
3036            eee
3037        "
3038        .unindent();
3039        let current_text = "
3040            aaa
3041            ddd
3042            eee
3043        "
3044        .unindent();
3045
3046        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3047
3048        editor.update(cx, |editor, cx| {
3049            let path = PathKey::for_buffer(&buffer, cx);
3050            editor.update_excerpts_for_path(
3051                path,
3052                buffer.clone(),
3053                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3054                0,
3055                diff.clone(),
3056                cx,
3057            );
3058        });
3059
3060        cx.run_until_parked();
3061
3062        assert_split_content(
3063            &editor,
3064            "
3065            § <no file>
3066            § -----
3067            aaa
3068            § spacer
3069            § spacer
3070            ddd
3071            eee"
3072            .unindent(),
3073            "
3074            § <no file>
3075            § -----
3076            aaa
3077            bbb
3078            ccc
3079            ddd
3080            eee"
3081            .unindent(),
3082            &mut cx,
3083        );
3084
3085        let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
3086        cx.update_window_entity(&rhs_editor, |editor, window, cx| {
3087            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
3088                s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
3089            });
3090            editor.git_restore(&Restore, window, cx);
3091        });
3092
3093        cx.run_until_parked();
3094
3095        assert_split_content(
3096            &editor,
3097            "
3098            § <no file>
3099            § -----
3100            aaa
3101            bbb
3102            ccc
3103            ddd
3104            eee"
3105            .unindent(),
3106            "
3107            § <no file>
3108            § -----
3109            aaa
3110            bbb
3111            ccc
3112            ddd
3113            eee"
3114            .unindent(),
3115            &mut cx,
3116        );
3117
3118        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3119        diff.update(cx, |diff, cx| {
3120            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3121        });
3122
3123        cx.run_until_parked();
3124
3125        assert_split_content(
3126            &editor,
3127            "
3128            § <no file>
3129            § -----
3130            aaa
3131            bbb
3132            ccc
3133            ddd
3134            eee"
3135            .unindent(),
3136            "
3137            § <no file>
3138            § -----
3139            aaa
3140            bbb
3141            ccc
3142            ddd
3143            eee"
3144            .unindent(),
3145            &mut cx,
3146        );
3147    }
3148
3149    #[gpui::test]
3150    async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
3151        use rope::Point;
3152        use unindent::Unindent as _;
3153
3154        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3155
3156        let base_text = "
3157            aaa
3158            old1
3159            old2
3160            old3
3161            old4
3162            zzz
3163        "
3164        .unindent();
3165
3166        let current_text = "
3167            aaa
3168            new1
3169            new2
3170            new3
3171            new4
3172            zzz
3173        "
3174        .unindent();
3175
3176        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3177
3178        editor.update(cx, |editor, cx| {
3179            let path = PathKey::for_buffer(&buffer, cx);
3180            editor.update_excerpts_for_path(
3181                path,
3182                buffer.clone(),
3183                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3184                0,
3185                diff.clone(),
3186                cx,
3187            );
3188        });
3189
3190        cx.run_until_parked();
3191
3192        buffer.update(cx, |buffer, cx| {
3193            buffer.edit(
3194                [
3195                    (Point::new(2, 0)..Point::new(3, 0), ""),
3196                    (Point::new(4, 0)..Point::new(5, 0), ""),
3197                ],
3198                None,
3199                cx,
3200            );
3201        });
3202        cx.run_until_parked();
3203
3204        assert_split_content(
3205            &editor,
3206            "
3207            § <no file>
3208            § -----
3209            aaa
3210            new1
3211            new3
3212            § spacer
3213            § spacer
3214            zzz"
3215            .unindent(),
3216            "
3217            § <no file>
3218            § -----
3219            aaa
3220            old1
3221            old2
3222            old3
3223            old4
3224            zzz"
3225            .unindent(),
3226            &mut cx,
3227        );
3228
3229        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3230        diff.update(cx, |diff, cx| {
3231            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3232        });
3233
3234        cx.run_until_parked();
3235
3236        assert_split_content(
3237            &editor,
3238            "
3239            § <no file>
3240            § -----
3241            aaa
3242            new1
3243            new3
3244            § spacer
3245            § spacer
3246            zzz"
3247            .unindent(),
3248            "
3249            § <no file>
3250            § -----
3251            aaa
3252            old1
3253            old2
3254            old3
3255            old4
3256            zzz"
3257            .unindent(),
3258            &mut cx,
3259        );
3260    }
3261
3262    #[gpui::test]
3263    async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
3264        use rope::Point;
3265        use unindent::Unindent as _;
3266
3267        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3268
3269        let text = "aaaa bbbb cccc dddd eeee ffff";
3270
3271        let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
3272        let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
3273
3274        editor.update(cx, |editor, cx| {
3275            let end = Point::new(0, text.len() as u32);
3276            let path1 = PathKey::for_buffer(&buffer1, cx);
3277            editor.update_excerpts_for_path(
3278                path1,
3279                buffer1.clone(),
3280                vec![Point::new(0, 0)..end],
3281                0,
3282                diff1.clone(),
3283                cx,
3284            );
3285            let path2 = PathKey::for_buffer(&buffer2, cx);
3286            editor.update_excerpts_for_path(
3287                path2,
3288                buffer2.clone(),
3289                vec![Point::new(0, 0)..end],
3290                0,
3291                diff2.clone(),
3292                cx,
3293            );
3294        });
3295
3296        cx.run_until_parked();
3297
3298        assert_split_content_with_widths(
3299            &editor,
3300            px(200.0),
3301            px(400.0),
3302            "
3303            § <no file>
3304            § -----
3305            aaaa bbbb\x20
3306            cccc dddd\x20
3307            eeee ffff
3308            § <no file>
3309            § -----
3310            aaaa bbbb\x20
3311            cccc dddd\x20
3312            eeee ffff"
3313                .unindent(),
3314            "
3315            § <no file>
3316            § -----
3317            aaaa bbbb cccc dddd eeee ffff
3318            § spacer
3319            § spacer
3320            § <no file>
3321            § -----
3322            aaaa bbbb cccc dddd eeee ffff
3323            § spacer
3324            § spacer"
3325                .unindent(),
3326            &mut cx,
3327        );
3328    }
3329
3330    #[gpui::test]
3331    async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
3332        use rope::Point;
3333        use unindent::Unindent as _;
3334
3335        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3336
3337        let base_text = "
3338            aaaa bbbb cccc dddd eeee ffff
3339            old line one
3340            old line two
3341        "
3342        .unindent();
3343
3344        let current_text = "
3345            aaaa bbbb cccc dddd eeee ffff
3346            new line
3347        "
3348        .unindent();
3349
3350        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3351
3352        editor.update(cx, |editor, cx| {
3353            let path = PathKey::for_buffer(&buffer, cx);
3354            editor.update_excerpts_for_path(
3355                path,
3356                buffer.clone(),
3357                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3358                0,
3359                diff.clone(),
3360                cx,
3361            );
3362        });
3363
3364        cx.run_until_parked();
3365
3366        assert_split_content_with_widths(
3367            &editor,
3368            px(200.0),
3369            px(400.0),
3370            "
3371            § <no file>
3372            § -----
3373            aaaa bbbb\x20
3374            cccc dddd\x20
3375            eeee ffff
3376            new line
3377            § spacer"
3378                .unindent(),
3379            "
3380            § <no file>
3381            § -----
3382            aaaa bbbb cccc dddd eeee ffff
3383            § spacer
3384            § spacer
3385            old line one
3386            old line two"
3387                .unindent(),
3388            &mut cx,
3389        );
3390    }
3391
3392    #[gpui::test]
3393    async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3394        use rope::Point;
3395        use unindent::Unindent as _;
3396
3397        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3398
3399        let base_text = "
3400            aaaa bbbb cccc dddd eeee ffff
3401            deleted line one
3402            deleted line two
3403            after
3404        "
3405        .unindent();
3406
3407        let current_text = "
3408            aaaa bbbb cccc dddd eeee ffff
3409            after
3410        "
3411        .unindent();
3412
3413        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3414
3415        editor.update(cx, |editor, cx| {
3416            let path = PathKey::for_buffer(&buffer, cx);
3417            editor.update_excerpts_for_path(
3418                path,
3419                buffer.clone(),
3420                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3421                0,
3422                diff.clone(),
3423                cx,
3424            );
3425        });
3426
3427        cx.run_until_parked();
3428
3429        assert_split_content_with_widths(
3430            &editor,
3431            px(400.0),
3432            px(200.0),
3433            "
3434            § <no file>
3435            § -----
3436            aaaa bbbb cccc dddd eeee ffff
3437            § spacer
3438            § spacer
3439            § spacer
3440            § spacer
3441            § spacer
3442            § spacer
3443            after"
3444                .unindent(),
3445            "
3446            § <no file>
3447            § -----
3448            aaaa bbbb\x20
3449            cccc dddd\x20
3450            eeee ffff
3451            deleted line\x20
3452            one
3453            deleted line\x20
3454            two
3455            after"
3456                .unindent(),
3457            &mut cx,
3458        );
3459    }
3460
3461    #[gpui::test]
3462    async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3463        use rope::Point;
3464        use unindent::Unindent as _;
3465
3466        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3467
3468        let text = "
3469            aaaa bbbb cccc dddd eeee ffff
3470            short
3471        "
3472        .unindent();
3473
3474        let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3475
3476        editor.update(cx, |editor, cx| {
3477            let path = PathKey::for_buffer(&buffer, cx);
3478            editor.update_excerpts_for_path(
3479                path,
3480                buffer.clone(),
3481                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3482                0,
3483                diff.clone(),
3484                cx,
3485            );
3486        });
3487
3488        cx.run_until_parked();
3489
3490        assert_split_content_with_widths(
3491            &editor,
3492            px(400.0),
3493            px(200.0),
3494            "
3495            § <no file>
3496            § -----
3497            aaaa bbbb cccc dddd eeee ffff
3498            § spacer
3499            § spacer
3500            short"
3501                .unindent(),
3502            "
3503            § <no file>
3504            § -----
3505            aaaa bbbb\x20
3506            cccc dddd\x20
3507            eeee ffff
3508            short"
3509                .unindent(),
3510            &mut cx,
3511        );
3512
3513        buffer.update(cx, |buffer, cx| {
3514            buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3515        });
3516
3517        cx.run_until_parked();
3518
3519        assert_split_content_with_widths(
3520            &editor,
3521            px(400.0),
3522            px(200.0),
3523            "
3524            § <no file>
3525            § -----
3526            aaaa bbbb cccc dddd eeee ffff
3527            § spacer
3528            § spacer
3529            modified"
3530                .unindent(),
3531            "
3532            § <no file>
3533            § -----
3534            aaaa bbbb\x20
3535            cccc dddd\x20
3536            eeee ffff
3537            short"
3538                .unindent(),
3539            &mut cx,
3540        );
3541
3542        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3543        diff.update(cx, |diff, cx| {
3544            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3545        });
3546
3547        cx.run_until_parked();
3548
3549        assert_split_content_with_widths(
3550            &editor,
3551            px(400.0),
3552            px(200.0),
3553            "
3554            § <no file>
3555            § -----
3556            aaaa bbbb cccc dddd eeee ffff
3557            § spacer
3558            § spacer
3559            modified"
3560                .unindent(),
3561            "
3562            § <no file>
3563            § -----
3564            aaaa bbbb\x20
3565            cccc dddd\x20
3566            eeee ffff
3567            short"
3568                .unindent(),
3569            &mut cx,
3570        );
3571    }
3572
3573    #[gpui::test]
3574    async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3575        use rope::Point;
3576        use unindent::Unindent as _;
3577
3578        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3579
3580        let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3581
3582        let current_text = "
3583            aaa
3584            bbb
3585            ccc
3586        "
3587        .unindent();
3588
3589        let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3590        let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3591
3592        editor.update(cx, |editor, cx| {
3593            let path1 = PathKey::for_buffer(&buffer1, cx);
3594            editor.update_excerpts_for_path(
3595                path1,
3596                buffer1.clone(),
3597                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3598                0,
3599                diff1.clone(),
3600                cx,
3601            );
3602
3603            let path2 = PathKey::for_buffer(&buffer2, cx);
3604            editor.update_excerpts_for_path(
3605                path2,
3606                buffer2.clone(),
3607                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3608                1,
3609                diff2.clone(),
3610                cx,
3611            );
3612        });
3613
3614        cx.run_until_parked();
3615
3616        assert_split_content(
3617            &editor,
3618            "
3619            § <no file>
3620            § -----
3621            xxx
3622            yyy
3623            § <no file>
3624            § -----
3625            aaa
3626            bbb
3627            ccc"
3628            .unindent(),
3629            "
3630            § <no file>
3631            § -----
3632            xxx
3633            yyy
3634            § <no file>
3635            § -----
3636            § spacer
3637            § spacer
3638            § spacer"
3639                .unindent(),
3640            &mut cx,
3641        );
3642
3643        buffer1.update(cx, |buffer, cx| {
3644            buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3645        });
3646
3647        cx.run_until_parked();
3648
3649        assert_split_content(
3650            &editor,
3651            "
3652            § <no file>
3653            § -----
3654            xxxz
3655            yyy
3656            § <no file>
3657            § -----
3658            aaa
3659            bbb
3660            ccc"
3661            .unindent(),
3662            "
3663            § <no file>
3664            § -----
3665            xxx
3666            yyy
3667            § <no file>
3668            § -----
3669            § spacer
3670            § spacer
3671            § spacer"
3672                .unindent(),
3673            &mut cx,
3674        );
3675    }
3676
3677    #[gpui::test]
3678    async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3679        use rope::Point;
3680        use unindent::Unindent as _;
3681
3682        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3683
3684        let base_text = "
3685            aaa
3686            bbb
3687            ccc
3688        "
3689        .unindent();
3690
3691        let current_text = "
3692            NEW1
3693            NEW2
3694            ccc
3695        "
3696        .unindent();
3697
3698        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3699
3700        editor.update(cx, |editor, cx| {
3701            let path = PathKey::for_buffer(&buffer, cx);
3702            editor.update_excerpts_for_path(
3703                path,
3704                buffer.clone(),
3705                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3706                0,
3707                diff.clone(),
3708                cx,
3709            );
3710        });
3711
3712        cx.run_until_parked();
3713
3714        assert_split_content(
3715            &editor,
3716            "
3717            § <no file>
3718            § -----
3719            NEW1
3720            NEW2
3721            ccc"
3722            .unindent(),
3723            "
3724            § <no file>
3725            § -----
3726            aaa
3727            bbb
3728            ccc"
3729            .unindent(),
3730            &mut cx,
3731        );
3732
3733        buffer.update(cx, |buffer, cx| {
3734            buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3735        });
3736
3737        cx.run_until_parked();
3738
3739        assert_split_content(
3740            &editor,
3741            "
3742            § <no file>
3743            § -----
3744            NEW1
3745            NEW
3746            ccc"
3747            .unindent(),
3748            "
3749            § <no file>
3750            § -----
3751            aaa
3752            bbb
3753            ccc"
3754            .unindent(),
3755            &mut cx,
3756        );
3757    }
3758
3759    #[gpui::test]
3760    async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3761        use rope::Point;
3762        use unindent::Unindent as _;
3763
3764        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3765
3766        let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3767
3768        let current_text = "
3769            aaaa bbbb cccc dddd eeee ffff
3770            added line
3771        "
3772        .unindent();
3773
3774        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3775
3776        editor.update(cx, |editor, cx| {
3777            let path = PathKey::for_buffer(&buffer, cx);
3778            editor.update_excerpts_for_path(
3779                path,
3780                buffer.clone(),
3781                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3782                0,
3783                diff.clone(),
3784                cx,
3785            );
3786        });
3787
3788        cx.run_until_parked();
3789
3790        assert_split_content_with_widths(
3791            &editor,
3792            px(400.0),
3793            px(200.0),
3794            "
3795            § <no file>
3796            § -----
3797            aaaa bbbb cccc dddd eeee ffff
3798            § spacer
3799            § spacer
3800            added line"
3801                .unindent(),
3802            "
3803            § <no file>
3804            § -----
3805            aaaa bbbb\x20
3806            cccc dddd\x20
3807            eeee ffff
3808            § spacer"
3809                .unindent(),
3810            &mut cx,
3811        );
3812
3813        assert_split_content_with_widths(
3814            &editor,
3815            px(200.0),
3816            px(400.0),
3817            "
3818            § <no file>
3819            § -----
3820            aaaa bbbb\x20
3821            cccc dddd\x20
3822            eeee ffff
3823            added line"
3824                .unindent(),
3825            "
3826            § <no file>
3827            § -----
3828            aaaa bbbb cccc dddd eeee ffff
3829            § spacer
3830            § spacer
3831            § spacer"
3832                .unindent(),
3833            &mut cx,
3834        );
3835    }
3836
3837    #[gpui::test]
3838    #[ignore]
3839    async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3840        use rope::Point;
3841        use unindent::Unindent as _;
3842
3843        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3844
3845        let base_text = "
3846            aaa
3847            bbb
3848            ccc
3849            ddd
3850            eee
3851        "
3852        .unindent();
3853
3854        let current_text = "
3855            aaa
3856            NEW
3857            eee
3858        "
3859        .unindent();
3860
3861        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
3862
3863        editor.update(cx, |editor, cx| {
3864            let path = PathKey::for_buffer(&buffer, cx);
3865            editor.update_excerpts_for_path(
3866                path,
3867                buffer.clone(),
3868                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3869                0,
3870                diff.clone(),
3871                cx,
3872            );
3873        });
3874
3875        cx.run_until_parked();
3876
3877        assert_split_content(
3878            &editor,
3879            "
3880            § <no file>
3881            § -----
3882            aaa
3883            NEW
3884            § spacer
3885            § spacer
3886            eee"
3887            .unindent(),
3888            "
3889            § <no file>
3890            § -----
3891            aaa
3892            bbb
3893            ccc
3894            ddd
3895            eee"
3896            .unindent(),
3897            &mut cx,
3898        );
3899
3900        buffer.update(cx, |buffer, cx| {
3901            buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3902        });
3903
3904        cx.run_until_parked();
3905
3906        assert_split_content(
3907            &editor,
3908            "
3909            § <no file>
3910            § -----
3911            aaa
3912            § spacer
3913            § spacer
3914            § spacer
3915            NEWeee"
3916                .unindent(),
3917            "
3918            § <no file>
3919            § -----
3920            aaa
3921            bbb
3922            ccc
3923            ddd
3924            eee"
3925            .unindent(),
3926            &mut cx,
3927        );
3928
3929        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3930        diff.update(cx, |diff, cx| {
3931            diff.recalculate_diff_sync(&buffer_snapshot, cx);
3932        });
3933
3934        cx.run_until_parked();
3935
3936        assert_split_content(
3937            &editor,
3938            "
3939            § <no file>
3940            § -----
3941            aaa
3942            NEWeee
3943            § spacer
3944            § spacer
3945            § spacer"
3946                .unindent(),
3947            "
3948            § <no file>
3949            § -----
3950            aaa
3951            bbb
3952            ccc
3953            ddd
3954            eee"
3955            .unindent(),
3956            &mut cx,
3957        );
3958    }
3959
3960    #[gpui::test]
3961    async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3962        use rope::Point;
3963        use unindent::Unindent as _;
3964
3965        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3966
3967        let base_text = "";
3968        let current_text = "
3969            aaaa bbbb cccc dddd eeee ffff
3970            bbb
3971            ccc
3972        "
3973        .unindent();
3974
3975        let (buffer, diff) = buffer_with_diff(base_text, &current_text, &mut cx);
3976
3977        editor.update(cx, |editor, cx| {
3978            let path = PathKey::for_buffer(&buffer, cx);
3979            editor.update_excerpts_for_path(
3980                path,
3981                buffer.clone(),
3982                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3983                0,
3984                diff.clone(),
3985                cx,
3986            );
3987        });
3988
3989        cx.run_until_parked();
3990
3991        assert_split_content(
3992            &editor,
3993            "
3994            § <no file>
3995            § -----
3996            aaaa bbbb cccc dddd eeee ffff
3997            bbb
3998            ccc"
3999            .unindent(),
4000            "
4001            § <no file>
4002            § -----
4003            § spacer
4004            § spacer
4005            § spacer"
4006                .unindent(),
4007            &mut cx,
4008        );
4009
4010        assert_split_content_with_widths(
4011            &editor,
4012            px(200.0),
4013            px(200.0),
4014            "
4015            § <no file>
4016            § -----
4017            aaaa bbbb\x20
4018            cccc dddd\x20
4019            eeee ffff
4020            bbb
4021            ccc"
4022            .unindent(),
4023            "
4024            § <no file>
4025            § -----
4026            § spacer
4027            § spacer
4028            § spacer
4029            § spacer
4030            § spacer"
4031                .unindent(),
4032            &mut cx,
4033        );
4034    }
4035
4036    #[gpui::test]
4037    async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
4038        use rope::Point;
4039        use unindent::Unindent as _;
4040
4041        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
4042
4043        let base_text = "
4044            aaa
4045            bbb
4046            ccc
4047        "
4048        .unindent();
4049
4050        let current_text = "
4051            aaa
4052            bbb
4053            xxx
4054            yyy
4055            ccc
4056        "
4057        .unindent();
4058
4059        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4060
4061        editor.update(cx, |editor, cx| {
4062            let path = PathKey::for_buffer(&buffer, cx);
4063            editor.update_excerpts_for_path(
4064                path,
4065                buffer.clone(),
4066                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4067                0,
4068                diff.clone(),
4069                cx,
4070            );
4071        });
4072
4073        cx.run_until_parked();
4074
4075        assert_split_content(
4076            &editor,
4077            "
4078            § <no file>
4079            § -----
4080            aaa
4081            bbb
4082            xxx
4083            yyy
4084            ccc"
4085            .unindent(),
4086            "
4087            § <no file>
4088            § -----
4089            aaa
4090            bbb
4091            § spacer
4092            § spacer
4093            ccc"
4094            .unindent(),
4095            &mut cx,
4096        );
4097
4098        buffer.update(cx, |buffer, cx| {
4099            buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
4100        });
4101
4102        cx.run_until_parked();
4103
4104        assert_split_content(
4105            &editor,
4106            "
4107            § <no file>
4108            § -----
4109            aaa
4110            bbb
4111            xxx
4112            yyy
4113            zzz
4114            ccc"
4115            .unindent(),
4116            "
4117            § <no file>
4118            § -----
4119            aaa
4120            bbb
4121            § spacer
4122            § spacer
4123            § spacer
4124            ccc"
4125            .unindent(),
4126            &mut cx,
4127        );
4128    }
4129
4130    #[gpui::test]
4131    async fn test_scrolling(cx: &mut gpui::TestAppContext) {
4132        use crate::test::editor_content_with_blocks_and_size;
4133        use gpui::size;
4134        use rope::Point;
4135
4136        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4137
4138        let long_line = "x".repeat(200);
4139        let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
4140        lines[25] = long_line;
4141        let content = lines.join("\n");
4142
4143        let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
4144
4145        editor.update(cx, |editor, cx| {
4146            let path = PathKey::for_buffer(&buffer, cx);
4147            editor.update_excerpts_for_path(
4148                path,
4149                buffer.clone(),
4150                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4151                0,
4152                diff.clone(),
4153                cx,
4154            );
4155        });
4156
4157        cx.run_until_parked();
4158
4159        let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
4160            let lhs = editor.lhs.as_ref().expect("should have lhs editor");
4161            (editor.rhs_editor.clone(), lhs.editor.clone())
4162        });
4163
4164        rhs_editor.update_in(cx, |e, window, cx| {
4165            e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
4166        });
4167
4168        let rhs_pos =
4169            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4170        let lhs_pos =
4171            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4172        assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
4173        assert_eq!(
4174            lhs_pos.y, rhs_pos.y,
4175            "LHS should have same scroll position as RHS after set_scroll_position"
4176        );
4177
4178        let draw_size = size(px(300.), px(300.));
4179
4180        rhs_editor.update_in(cx, |e, window, cx| {
4181            e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
4182                s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
4183            });
4184        });
4185
4186        let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
4187        cx.run_until_parked();
4188        let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
4189        cx.run_until_parked();
4190
4191        let rhs_pos =
4192            rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4193        let lhs_pos =
4194            lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4195
4196        assert!(
4197            rhs_pos.y > 0.,
4198            "RHS should have scrolled vertically to show cursor at row 25"
4199        );
4200        assert!(
4201            rhs_pos.x > 0.,
4202            "RHS should have scrolled horizontally to show cursor at column 150"
4203        );
4204        assert_eq!(
4205            lhs_pos.y, rhs_pos.y,
4206            "LHS should have same vertical scroll position as RHS after autoscroll"
4207        );
4208        assert_eq!(
4209            lhs_pos.x, rhs_pos.x,
4210            "LHS should have same horizontal scroll position as RHS after autoscroll"
4211        )
4212    }
4213
4214    #[gpui::test]
4215    async fn test_edit_line_before_soft_wrapped_line_preceding_hunk(cx: &mut gpui::TestAppContext) {
4216        use rope::Point;
4217        use unindent::Unindent as _;
4218
4219        let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
4220
4221        let base_text = "
4222            first line
4223            aaaa bbbb cccc dddd eeee ffff
4224            original
4225        "
4226        .unindent();
4227
4228        let current_text = "
4229            first line
4230            aaaa bbbb cccc dddd eeee ffff
4231            modified
4232        "
4233        .unindent();
4234
4235        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4236
4237        editor.update(cx, |editor, cx| {
4238            let path = PathKey::for_buffer(&buffer, cx);
4239            editor.update_excerpts_for_path(
4240                path,
4241                buffer.clone(),
4242                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4243                0,
4244                diff.clone(),
4245                cx,
4246            );
4247        });
4248
4249        cx.run_until_parked();
4250
4251        assert_split_content_with_widths(
4252            &editor,
4253            px(400.0),
4254            px(200.0),
4255            "
4256                    § <no file>
4257                    § -----
4258                    first line
4259                    aaaa bbbb cccc dddd eeee ffff
4260                    § spacer
4261                    § spacer
4262                    modified"
4263                .unindent(),
4264            "
4265                    § <no file>
4266                    § -----
4267                    first line
4268                    aaaa bbbb\x20
4269                    cccc dddd\x20
4270                    eeee ffff
4271                    original"
4272                .unindent(),
4273            &mut cx,
4274        );
4275
4276        buffer.update(cx, |buffer, cx| {
4277            buffer.edit(
4278                [(Point::new(0, 0)..Point::new(0, 10), "edited first")],
4279                None,
4280                cx,
4281            );
4282        });
4283
4284        cx.run_until_parked();
4285
4286        assert_split_content_with_widths(
4287            &editor,
4288            px(400.0),
4289            px(200.0),
4290            "
4291                    § <no file>
4292                    § -----
4293                    edited first
4294                    aaaa bbbb cccc dddd eeee ffff
4295                    § spacer
4296                    § spacer
4297                    modified"
4298                .unindent(),
4299            "
4300                    § <no file>
4301                    § -----
4302                    first line
4303                    aaaa bbbb\x20
4304                    cccc dddd\x20
4305                    eeee ffff
4306                    original"
4307                .unindent(),
4308            &mut cx,
4309        );
4310
4311        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4312        diff.update(cx, |diff, cx| {
4313            diff.recalculate_diff_sync(&buffer_snapshot, cx);
4314        });
4315
4316        cx.run_until_parked();
4317
4318        assert_split_content_with_widths(
4319            &editor,
4320            px(400.0),
4321            px(200.0),
4322            "
4323                    § <no file>
4324                    § -----
4325                    edited first
4326                    aaaa bbbb cccc dddd eeee ffff
4327                    § spacer
4328                    § spacer
4329                    modified"
4330                .unindent(),
4331            "
4332                    § <no file>
4333                    § -----
4334                    first line
4335                    aaaa bbbb\x20
4336                    cccc dddd\x20
4337                    eeee ffff
4338                    original"
4339                .unindent(),
4340            &mut cx,
4341        );
4342    }
4343
4344    #[gpui::test]
4345    async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
4346        use rope::Point;
4347        use unindent::Unindent as _;
4348
4349        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4350
4351        let base_text = "
4352            bbb
4353            ccc
4354        "
4355        .unindent();
4356        let current_text = "
4357            aaa
4358            bbb
4359            ccc
4360        "
4361        .unindent();
4362
4363        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4364
4365        editor.update(cx, |editor, cx| {
4366            let path = PathKey::for_buffer(&buffer, cx);
4367            editor.update_excerpts_for_path(
4368                path,
4369                buffer.clone(),
4370                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4371                0,
4372                diff.clone(),
4373                cx,
4374            );
4375        });
4376
4377        cx.run_until_parked();
4378
4379        assert_split_content(
4380            &editor,
4381            "
4382            § <no file>
4383            § -----
4384            aaa
4385            bbb
4386            ccc"
4387            .unindent(),
4388            "
4389            § <no file>
4390            § -----
4391            § spacer
4392            bbb
4393            ccc"
4394            .unindent(),
4395            &mut cx,
4396        );
4397
4398        let block_ids = editor.update(cx, |splittable_editor, cx| {
4399            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4400                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4401                let anchor = snapshot.anchor_before(Point::new(2, 0));
4402                rhs_editor.insert_blocks(
4403                    [BlockProperties {
4404                        placement: BlockPlacement::Above(anchor),
4405                        height: Some(1),
4406                        style: BlockStyle::Fixed,
4407                        render: Arc::new(|_| div().into_any()),
4408                        priority: 0,
4409                    }],
4410                    None,
4411                    cx,
4412                )
4413            })
4414        });
4415
4416        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4417        let lhs_editor =
4418            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4419
4420        cx.update(|_, cx| {
4421            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4422                "custom block".to_string()
4423            });
4424        });
4425
4426        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
4427            let display_map = lhs_editor.display_map.read(cx);
4428            let companion = display_map.companion().unwrap().read(cx);
4429            let mapping = companion
4430                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4431            *mapping.borrow().get(&block_ids[0]).unwrap()
4432        });
4433
4434        cx.update(|_, cx| {
4435            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
4436                "custom block".to_string()
4437            });
4438        });
4439
4440        cx.run_until_parked();
4441
4442        assert_split_content(
4443            &editor,
4444            "
4445            § <no file>
4446            § -----
4447            aaa
4448            bbb
4449            § custom block
4450            ccc"
4451            .unindent(),
4452            "
4453            § <no file>
4454            § -----
4455            § spacer
4456            bbb
4457            § custom block
4458            ccc"
4459            .unindent(),
4460            &mut cx,
4461        );
4462
4463        editor.update(cx, |splittable_editor, cx| {
4464            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4465                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
4466            });
4467        });
4468
4469        cx.run_until_parked();
4470
4471        assert_split_content(
4472            &editor,
4473            "
4474            § <no file>
4475            § -----
4476            aaa
4477            bbb
4478            ccc"
4479            .unindent(),
4480            "
4481            § <no file>
4482            § -----
4483            § spacer
4484            bbb
4485            ccc"
4486            .unindent(),
4487            &mut cx,
4488        );
4489    }
4490
4491    #[gpui::test]
4492    async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
4493        use rope::Point;
4494        use unindent::Unindent as _;
4495
4496        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4497
4498        let base_text = "
4499            bbb
4500            ccc
4501        "
4502        .unindent();
4503        let current_text = "
4504            aaa
4505            bbb
4506            ccc
4507        "
4508        .unindent();
4509
4510        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4511
4512        editor.update(cx, |editor, cx| {
4513            let path = PathKey::for_buffer(&buffer, cx);
4514            editor.update_excerpts_for_path(
4515                path,
4516                buffer.clone(),
4517                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4518                0,
4519                diff.clone(),
4520                cx,
4521            );
4522        });
4523
4524        cx.run_until_parked();
4525
4526        assert_split_content(
4527            &editor,
4528            "
4529            § <no file>
4530            § -----
4531            aaa
4532            bbb
4533            ccc"
4534            .unindent(),
4535            "
4536            § <no file>
4537            § -----
4538            § spacer
4539            bbb
4540            ccc"
4541            .unindent(),
4542            &mut cx,
4543        );
4544
4545        let block_ids = editor.update(cx, |splittable_editor, cx| {
4546            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4547                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4548                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4549                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4550                rhs_editor.insert_blocks(
4551                    [
4552                        BlockProperties {
4553                            placement: BlockPlacement::Above(anchor1),
4554                            height: Some(1),
4555                            style: BlockStyle::Fixed,
4556                            render: Arc::new(|_| div().into_any()),
4557                            priority: 0,
4558                        },
4559                        BlockProperties {
4560                            placement: BlockPlacement::Above(anchor2),
4561                            height: Some(1),
4562                            style: BlockStyle::Fixed,
4563                            render: Arc::new(|_| div().into_any()),
4564                            priority: 0,
4565                        },
4566                    ],
4567                    None,
4568                    cx,
4569                )
4570            })
4571        });
4572
4573        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4574        let lhs_editor =
4575            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4576
4577        cx.update(|_, cx| {
4578            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4579                "custom block 1".to_string()
4580            });
4581            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4582                "custom block 2".to_string()
4583            });
4584        });
4585
4586        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4587            let display_map = lhs_editor.display_map.read(cx);
4588            let companion = display_map.companion().unwrap().read(cx);
4589            let mapping = companion
4590                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4591            (
4592                *mapping.borrow().get(&block_ids[0]).unwrap(),
4593                *mapping.borrow().get(&block_ids[1]).unwrap(),
4594            )
4595        });
4596
4597        cx.update(|_, cx| {
4598            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4599                "custom block 1".to_string()
4600            });
4601            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4602                "custom block 2".to_string()
4603            });
4604        });
4605
4606        cx.run_until_parked();
4607
4608        assert_split_content(
4609            &editor,
4610            "
4611            § <no file>
4612            § -----
4613            aaa
4614            bbb
4615            § custom block 1
4616            ccc
4617            § custom block 2"
4618                .unindent(),
4619            "
4620            § <no file>
4621            § -----
4622            § spacer
4623            bbb
4624            § custom block 1
4625            ccc
4626            § custom block 2"
4627                .unindent(),
4628            &mut cx,
4629        );
4630
4631        editor.update(cx, |splittable_editor, cx| {
4632            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4633                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4634            });
4635        });
4636
4637        cx.run_until_parked();
4638
4639        assert_split_content(
4640            &editor,
4641            "
4642            § <no file>
4643            § -----
4644            aaa
4645            bbb
4646            ccc
4647            § custom block 2"
4648                .unindent(),
4649            "
4650            § <no file>
4651            § -----
4652            § spacer
4653            bbb
4654            ccc
4655            § custom block 2"
4656                .unindent(),
4657            &mut cx,
4658        );
4659
4660        editor.update_in(cx, |splittable_editor, window, cx| {
4661            splittable_editor.unsplit(window, cx);
4662        });
4663
4664        cx.run_until_parked();
4665
4666        editor.update_in(cx, |splittable_editor, window, cx| {
4667            splittable_editor.split(window, cx);
4668        });
4669
4670        cx.run_until_parked();
4671
4672        let lhs_editor =
4673            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4674
4675        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4676            let display_map = lhs_editor.display_map.read(cx);
4677            let companion = display_map.companion().unwrap().read(cx);
4678            let mapping = companion
4679                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4680            *mapping.borrow().get(&block_ids[1]).unwrap()
4681        });
4682
4683        cx.update(|_, cx| {
4684            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4685                "custom block 2".to_string()
4686            });
4687        });
4688
4689        cx.run_until_parked();
4690
4691        assert_split_content(
4692            &editor,
4693            "
4694            § <no file>
4695            § -----
4696            aaa
4697            bbb
4698            ccc
4699            § custom block 2"
4700                .unindent(),
4701            "
4702            § <no file>
4703            § -----
4704            § spacer
4705            bbb
4706            ccc
4707            § custom block 2"
4708                .unindent(),
4709            &mut cx,
4710        );
4711    }
4712
4713    #[gpui::test]
4714    async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
4715        use rope::Point;
4716        use unindent::Unindent as _;
4717
4718        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4719
4720        let base_text = "
4721            bbb
4722            ccc
4723        "
4724        .unindent();
4725        let current_text = "
4726            aaa
4727            bbb
4728            ccc
4729        "
4730        .unindent();
4731
4732        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
4733
4734        editor.update(cx, |editor, cx| {
4735            let path = PathKey::for_buffer(&buffer, cx);
4736            editor.update_excerpts_for_path(
4737                path,
4738                buffer.clone(),
4739                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4740                0,
4741                diff.clone(),
4742                cx,
4743            );
4744        });
4745
4746        cx.run_until_parked();
4747
4748        editor.update_in(cx, |splittable_editor, window, cx| {
4749            splittable_editor.unsplit(window, cx);
4750        });
4751
4752        cx.run_until_parked();
4753
4754        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4755
4756        let block_ids = editor.update(cx, |splittable_editor, cx| {
4757            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4758                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4759                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4760                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4761                rhs_editor.insert_blocks(
4762                    [
4763                        BlockProperties {
4764                            placement: BlockPlacement::Above(anchor1),
4765                            height: Some(1),
4766                            style: BlockStyle::Fixed,
4767                            render: Arc::new(|_| div().into_any()),
4768                            priority: 0,
4769                        },
4770                        BlockProperties {
4771                            placement: BlockPlacement::Above(anchor2),
4772                            height: Some(1),
4773                            style: BlockStyle::Fixed,
4774                            render: Arc::new(|_| div().into_any()),
4775                            priority: 0,
4776                        },
4777                    ],
4778                    None,
4779                    cx,
4780                )
4781            })
4782        });
4783
4784        cx.update(|_, cx| {
4785            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4786                "custom block 1".to_string()
4787            });
4788            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4789                "custom block 2".to_string()
4790            });
4791        });
4792
4793        cx.run_until_parked();
4794
4795        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
4796        assert_eq!(
4797            rhs_content,
4798            "
4799            § <no file>
4800            § -----
4801            aaa
4802            bbb
4803            § custom block 1
4804            ccc
4805            § custom block 2"
4806                .unindent(),
4807            "rhs content before split"
4808        );
4809
4810        editor.update_in(cx, |splittable_editor, window, cx| {
4811            splittable_editor.split(window, cx);
4812        });
4813
4814        cx.run_until_parked();
4815
4816        let lhs_editor =
4817            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4818
4819        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4820            let display_map = lhs_editor.display_map.read(cx);
4821            let companion = display_map.companion().unwrap().read(cx);
4822            let mapping = companion
4823                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4824            (
4825                *mapping.borrow().get(&block_ids[0]).unwrap(),
4826                *mapping.borrow().get(&block_ids[1]).unwrap(),
4827            )
4828        });
4829
4830        cx.update(|_, cx| {
4831            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4832                "custom block 1".to_string()
4833            });
4834            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4835                "custom block 2".to_string()
4836            });
4837        });
4838
4839        cx.run_until_parked();
4840
4841        assert_split_content(
4842            &editor,
4843            "
4844            § <no file>
4845            § -----
4846            aaa
4847            bbb
4848            § custom block 1
4849            ccc
4850            § custom block 2"
4851                .unindent(),
4852            "
4853            § <no file>
4854            § -----
4855            § spacer
4856            bbb
4857            § custom block 1
4858            ccc
4859            § custom block 2"
4860                .unindent(),
4861            &mut cx,
4862        );
4863
4864        editor.update(cx, |splittable_editor, cx| {
4865            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4866                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4867            });
4868        });
4869
4870        cx.run_until_parked();
4871
4872        assert_split_content(
4873            &editor,
4874            "
4875            § <no file>
4876            § -----
4877            aaa
4878            bbb
4879            ccc
4880            § custom block 2"
4881                .unindent(),
4882            "
4883            § <no file>
4884            § -----
4885            § spacer
4886            bbb
4887            ccc
4888            § custom block 2"
4889                .unindent(),
4890            &mut cx,
4891        );
4892
4893        editor.update_in(cx, |splittable_editor, window, cx| {
4894            splittable_editor.unsplit(window, cx);
4895        });
4896
4897        cx.run_until_parked();
4898
4899        editor.update_in(cx, |splittable_editor, window, cx| {
4900            splittable_editor.split(window, cx);
4901        });
4902
4903        cx.run_until_parked();
4904
4905        let lhs_editor =
4906            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4907
4908        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4909            let display_map = lhs_editor.display_map.read(cx);
4910            let companion = display_map.companion().unwrap().read(cx);
4911            let mapping = companion
4912                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4913            *mapping.borrow().get(&block_ids[1]).unwrap()
4914        });
4915
4916        cx.update(|_, cx| {
4917            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4918                "custom block 2".to_string()
4919            });
4920        });
4921
4922        cx.run_until_parked();
4923
4924        assert_split_content(
4925            &editor,
4926            "
4927            § <no file>
4928            § -----
4929            aaa
4930            bbb
4931            ccc
4932            § custom block 2"
4933                .unindent(),
4934            "
4935            § <no file>
4936            § -----
4937            § spacer
4938            bbb
4939            ccc
4940            § custom block 2"
4941                .unindent(),
4942            &mut cx,
4943        );
4944
4945        let new_block_ids = editor.update(cx, |splittable_editor, cx| {
4946            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4947                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4948                let anchor = snapshot.anchor_before(Point::new(2, 0));
4949                rhs_editor.insert_blocks(
4950                    [BlockProperties {
4951                        placement: BlockPlacement::Above(anchor),
4952                        height: Some(1),
4953                        style: BlockStyle::Fixed,
4954                        render: Arc::new(|_| div().into_any()),
4955                        priority: 0,
4956                    }],
4957                    None,
4958                    cx,
4959                )
4960            })
4961        });
4962
4963        cx.update(|_, cx| {
4964            set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
4965                "custom block 3".to_string()
4966            });
4967        });
4968
4969        let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4970            let display_map = lhs_editor.display_map.read(cx);
4971            let companion = display_map.companion().unwrap().read(cx);
4972            let mapping = companion
4973                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4974            *mapping.borrow().get(&new_block_ids[0]).unwrap()
4975        });
4976
4977        cx.update(|_, cx| {
4978            set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
4979                "custom block 3".to_string()
4980            });
4981        });
4982
4983        cx.run_until_parked();
4984
4985        assert_split_content(
4986            &editor,
4987            "
4988            § <no file>
4989            § -----
4990            aaa
4991            bbb
4992            § custom block 3
4993            ccc
4994            § custom block 2"
4995                .unindent(),
4996            "
4997            § <no file>
4998            § -----
4999            § spacer
5000            bbb
5001            § custom block 3
5002            ccc
5003            § custom block 2"
5004                .unindent(),
5005            &mut cx,
5006        );
5007
5008        editor.update(cx, |splittable_editor, cx| {
5009            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5010                rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
5011            });
5012        });
5013
5014        cx.run_until_parked();
5015
5016        assert_split_content(
5017            &editor,
5018            "
5019            § <no file>
5020            § -----
5021            aaa
5022            bbb
5023            ccc
5024            § custom block 2"
5025                .unindent(),
5026            "
5027            § <no file>
5028            § -----
5029            § spacer
5030            bbb
5031            ccc
5032            § custom block 2"
5033                .unindent(),
5034            &mut cx,
5035        );
5036    }
5037
5038    #[gpui::test]
5039    async fn test_buffer_folding_sync(cx: &mut gpui::TestAppContext) {
5040        use rope::Point;
5041        use unindent::Unindent as _;
5042
5043        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5044
5045        let base_text1 = "
5046            aaa
5047            bbb
5048            ccc"
5049        .unindent();
5050        let current_text1 = "
5051            aaa
5052            bbb
5053            ccc"
5054        .unindent();
5055
5056        let base_text2 = "
5057            ddd
5058            eee
5059            fff"
5060        .unindent();
5061        let current_text2 = "
5062            ddd
5063            eee
5064            fff"
5065        .unindent();
5066
5067        let (buffer1, diff1) = buffer_with_diff(&base_text1, &current_text1, &mut cx);
5068        let (buffer2, diff2) = buffer_with_diff(&base_text2, &current_text2, &mut cx);
5069
5070        let buffer1_id = buffer1.read_with(cx, |buffer, _| buffer.remote_id());
5071        let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
5072
5073        editor.update(cx, |editor, cx| {
5074            let path1 = PathKey::for_buffer(&buffer1, cx);
5075            editor.update_excerpts_for_path(
5076                path1,
5077                buffer1.clone(),
5078                vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
5079                0,
5080                diff1.clone(),
5081                cx,
5082            );
5083            let path2 = PathKey::for_buffer(&buffer2, cx);
5084            editor.update_excerpts_for_path(
5085                path2,
5086                buffer2.clone(),
5087                vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
5088                1,
5089                diff2.clone(),
5090                cx,
5091            );
5092        });
5093
5094        cx.run_until_parked();
5095
5096        editor.update(cx, |editor, cx| {
5097            editor.rhs_editor.update(cx, |rhs_editor, cx| {
5098                rhs_editor.fold_buffer(buffer1_id, cx);
5099            });
5100        });
5101
5102        cx.run_until_parked();
5103
5104        let rhs_buffer1_folded = editor.read_with(cx, |editor, cx| {
5105            editor.rhs_editor.read(cx).is_buffer_folded(buffer1_id, cx)
5106        });
5107        assert!(
5108            rhs_buffer1_folded,
5109            "buffer1 should be folded in rhs before split"
5110        );
5111
5112        editor.update_in(cx, |editor, window, cx| {
5113            editor.split(window, cx);
5114        });
5115
5116        cx.run_until_parked();
5117
5118        let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
5119            (
5120                editor.rhs_editor.clone(),
5121                editor.lhs.as_ref().unwrap().editor.clone(),
5122            )
5123        });
5124
5125        let rhs_buffer1_folded =
5126            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
5127        assert!(
5128            rhs_buffer1_folded,
5129            "buffer1 should be folded in rhs after split"
5130        );
5131
5132        let base_buffer1_id = diff1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
5133        let lhs_buffer1_folded = lhs_editor.read_with(cx, |editor, cx| {
5134            editor.is_buffer_folded(base_buffer1_id, cx)
5135        });
5136        assert!(
5137            lhs_buffer1_folded,
5138            "buffer1 should be folded in lhs after split"
5139        );
5140
5141        assert_split_content(
5142            &editor,
5143            "
5144            § <no file>
5145            § -----
5146            § <no file>
5147            § -----
5148            ddd
5149            eee
5150            fff"
5151            .unindent(),
5152            "
5153            § <no file>
5154            § -----
5155            § <no file>
5156            § -----
5157            ddd
5158            eee
5159            fff"
5160            .unindent(),
5161            &mut cx,
5162        );
5163
5164        editor.update(cx, |editor, cx| {
5165            editor.rhs_editor.update(cx, |rhs_editor, cx| {
5166                rhs_editor.fold_buffer(buffer2_id, cx);
5167            });
5168        });
5169
5170        cx.run_until_parked();
5171
5172        let rhs_buffer2_folded =
5173            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer2_id, cx));
5174        assert!(rhs_buffer2_folded, "buffer2 should be folded in rhs");
5175
5176        let base_buffer2_id = diff2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
5177        let lhs_buffer2_folded = lhs_editor.read_with(cx, |editor, cx| {
5178            editor.is_buffer_folded(base_buffer2_id, cx)
5179        });
5180        assert!(lhs_buffer2_folded, "buffer2 should be folded in lhs");
5181
5182        let rhs_buffer1_still_folded =
5183            rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
5184        assert!(
5185            rhs_buffer1_still_folded,
5186            "buffer1 should still be folded in rhs"
5187        );
5188
5189        let lhs_buffer1_still_folded = lhs_editor.read_with(cx, |editor, cx| {
5190            editor.is_buffer_folded(base_buffer1_id, cx)
5191        });
5192        assert!(
5193            lhs_buffer1_still_folded,
5194            "buffer1 should still be folded in lhs"
5195        );
5196
5197        assert_split_content(
5198            &editor,
5199            "
5200            § <no file>
5201            § -----
5202            § <no file>
5203            § -----"
5204                .unindent(),
5205            "
5206            § <no file>
5207            § -----
5208            § <no file>
5209            § -----"
5210                .unindent(),
5211            &mut cx,
5212        );
5213    }
5214
5215    #[gpui::test]
5216    async fn test_custom_block_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5217        use rope::Point;
5218        use unindent::Unindent as _;
5219
5220        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5221
5222        let base_text = "
5223            ddd
5224            eee
5225        "
5226        .unindent();
5227        let current_text = "
5228            aaa
5229            bbb
5230            ccc
5231            ddd
5232            eee
5233        "
5234        .unindent();
5235
5236        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5237
5238        editor.update(cx, |editor, cx| {
5239            let path = PathKey::for_buffer(&buffer, cx);
5240            editor.update_excerpts_for_path(
5241                path,
5242                buffer.clone(),
5243                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5244                0,
5245                diff.clone(),
5246                cx,
5247            );
5248        });
5249
5250        cx.run_until_parked();
5251
5252        assert_split_content(
5253            &editor,
5254            "
5255            § <no file>
5256            § -----
5257            aaa
5258            bbb
5259            ccc
5260            ddd
5261            eee"
5262            .unindent(),
5263            "
5264            § <no file>
5265            § -----
5266            § spacer
5267            § spacer
5268            § spacer
5269            ddd
5270            eee"
5271            .unindent(),
5272            &mut cx,
5273        );
5274
5275        let block_ids = editor.update(cx, |splittable_editor, cx| {
5276            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5277                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5278                let anchor = snapshot.anchor_before(Point::new(2, 0));
5279                rhs_editor.insert_blocks(
5280                    [BlockProperties {
5281                        placement: BlockPlacement::Above(anchor),
5282                        height: Some(1),
5283                        style: BlockStyle::Fixed,
5284                        render: Arc::new(|_| div().into_any()),
5285                        priority: 0,
5286                    }],
5287                    None,
5288                    cx,
5289                )
5290            })
5291        });
5292
5293        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5294        let lhs_editor =
5295            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5296
5297        cx.update(|_, cx| {
5298            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5299                "custom block".to_string()
5300            });
5301        });
5302
5303        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5304            let display_map = lhs_editor.display_map.read(cx);
5305            let companion = display_map.companion().unwrap().read(cx);
5306            let mapping = companion
5307                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5308            *mapping.borrow().get(&block_ids[0]).unwrap()
5309        });
5310
5311        cx.update(|_, cx| {
5312            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5313                "custom block".to_string()
5314            });
5315        });
5316
5317        cx.run_until_parked();
5318
5319        assert_split_content(
5320            &editor,
5321            "
5322            § <no file>
5323            § -----
5324            aaa
5325            bbb
5326            § custom block
5327            ccc
5328            ddd
5329            eee"
5330            .unindent(),
5331            "
5332            § <no file>
5333            § -----
5334            § spacer
5335            § spacer
5336            § spacer
5337            § custom block
5338            ddd
5339            eee"
5340            .unindent(),
5341            &mut cx,
5342        );
5343
5344        editor.update(cx, |splittable_editor, cx| {
5345            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5346                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5347            });
5348        });
5349
5350        cx.run_until_parked();
5351
5352        assert_split_content(
5353            &editor,
5354            "
5355            § <no file>
5356            § -----
5357            aaa
5358            bbb
5359            ccc
5360            ddd
5361            eee"
5362            .unindent(),
5363            "
5364            § <no file>
5365            § -----
5366            § spacer
5367            § spacer
5368            § spacer
5369            ddd
5370            eee"
5371            .unindent(),
5372            &mut cx,
5373        );
5374    }
5375
5376    #[gpui::test]
5377    async fn test_custom_block_below_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5378        use rope::Point;
5379        use unindent::Unindent as _;
5380
5381        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5382
5383        let base_text = "
5384            ddd
5385            eee
5386        "
5387        .unindent();
5388        let current_text = "
5389            aaa
5390            bbb
5391            ccc
5392            ddd
5393            eee
5394        "
5395        .unindent();
5396
5397        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5398
5399        editor.update(cx, |editor, cx| {
5400            let path = PathKey::for_buffer(&buffer, cx);
5401            editor.update_excerpts_for_path(
5402                path,
5403                buffer.clone(),
5404                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5405                0,
5406                diff.clone(),
5407                cx,
5408            );
5409        });
5410
5411        cx.run_until_parked();
5412
5413        assert_split_content(
5414            &editor,
5415            "
5416            § <no file>
5417            § -----
5418            aaa
5419            bbb
5420            ccc
5421            ddd
5422            eee"
5423            .unindent(),
5424            "
5425            § <no file>
5426            § -----
5427            § spacer
5428            § spacer
5429            § spacer
5430            ddd
5431            eee"
5432            .unindent(),
5433            &mut cx,
5434        );
5435
5436        let block_ids = editor.update(cx, |splittable_editor, cx| {
5437            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5438                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5439                let anchor = snapshot.anchor_after(Point::new(1, 3));
5440                rhs_editor.insert_blocks(
5441                    [BlockProperties {
5442                        placement: BlockPlacement::Below(anchor),
5443                        height: Some(1),
5444                        style: BlockStyle::Fixed,
5445                        render: Arc::new(|_| div().into_any()),
5446                        priority: 0,
5447                    }],
5448                    None,
5449                    cx,
5450                )
5451            })
5452        });
5453
5454        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5455        let lhs_editor =
5456            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5457
5458        cx.update(|_, cx| {
5459            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5460                "custom block".to_string()
5461            });
5462        });
5463
5464        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5465            let display_map = lhs_editor.display_map.read(cx);
5466            let companion = display_map.companion().unwrap().read(cx);
5467            let mapping = companion
5468                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5469            *mapping.borrow().get(&block_ids[0]).unwrap()
5470        });
5471
5472        cx.update(|_, cx| {
5473            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5474                "custom block".to_string()
5475            });
5476        });
5477
5478        cx.run_until_parked();
5479
5480        assert_split_content(
5481            &editor,
5482            "
5483            § <no file>
5484            § -----
5485            aaa
5486            bbb
5487            § custom block
5488            ccc
5489            ddd
5490            eee"
5491            .unindent(),
5492            "
5493            § <no file>
5494            § -----
5495            § spacer
5496            § spacer
5497            § spacer
5498            § custom block
5499            ddd
5500            eee"
5501            .unindent(),
5502            &mut cx,
5503        );
5504
5505        editor.update(cx, |splittable_editor, cx| {
5506            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5507                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5508            });
5509        });
5510
5511        cx.run_until_parked();
5512
5513        assert_split_content(
5514            &editor,
5515            "
5516            § <no file>
5517            § -----
5518            aaa
5519            bbb
5520            ccc
5521            ddd
5522            eee"
5523            .unindent(),
5524            "
5525            § <no file>
5526            § -----
5527            § spacer
5528            § spacer
5529            § spacer
5530            ddd
5531            eee"
5532            .unindent(),
5533            &mut cx,
5534        );
5535    }
5536
5537    #[gpui::test]
5538    async fn test_custom_block_resize_syncs_balancing_block(cx: &mut gpui::TestAppContext) {
5539        use rope::Point;
5540        use unindent::Unindent as _;
5541
5542        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5543
5544        let base_text = "
5545            bbb
5546            ccc
5547        "
5548        .unindent();
5549        let current_text = "
5550            aaa
5551            bbb
5552            ccc
5553        "
5554        .unindent();
5555
5556        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5557
5558        editor.update(cx, |editor, cx| {
5559            let path = PathKey::for_buffer(&buffer, cx);
5560            editor.update_excerpts_for_path(
5561                path,
5562                buffer.clone(),
5563                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5564                0,
5565                diff.clone(),
5566                cx,
5567            );
5568        });
5569
5570        cx.run_until_parked();
5571
5572        let block_ids = editor.update(cx, |splittable_editor, cx| {
5573            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5574                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5575                let anchor = snapshot.anchor_before(Point::new(2, 0));
5576                rhs_editor.insert_blocks(
5577                    [BlockProperties {
5578                        placement: BlockPlacement::Above(anchor),
5579                        height: Some(1),
5580                        style: BlockStyle::Fixed,
5581                        render: Arc::new(|_| div().into_any()),
5582                        priority: 0,
5583                    }],
5584                    None,
5585                    cx,
5586                )
5587            })
5588        });
5589
5590        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5591        let lhs_editor =
5592            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5593
5594        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5595            let display_map = lhs_editor.display_map.read(cx);
5596            let companion = display_map.companion().unwrap().read(cx);
5597            let mapping = companion
5598                .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5599            *mapping.borrow().get(&block_ids[0]).unwrap()
5600        });
5601
5602        cx.run_until_parked();
5603
5604        let get_block_height = |editor: &Entity<crate::Editor>,
5605                                block_id: crate::CustomBlockId,
5606                                cx: &mut VisualTestContext| {
5607            editor.update_in(cx, |editor, window, cx| {
5608                let snapshot = editor.snapshot(window, cx);
5609                snapshot
5610                    .block_for_id(crate::BlockId::Custom(block_id))
5611                    .map(|block| block.height())
5612            })
5613        };
5614
5615        assert_eq!(
5616            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5617            Some(1)
5618        );
5619        assert_eq!(
5620            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5621            Some(1)
5622        );
5623
5624        editor.update(cx, |splittable_editor, cx| {
5625            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5626                let mut heights = HashMap::default();
5627                heights.insert(block_ids[0], 3);
5628                rhs_editor.resize_blocks(heights, None, cx);
5629            });
5630        });
5631
5632        cx.run_until_parked();
5633
5634        assert_eq!(
5635            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5636            Some(3)
5637        );
5638        assert_eq!(
5639            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5640            Some(3)
5641        );
5642
5643        editor.update(cx, |splittable_editor, cx| {
5644            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5645                let mut heights = HashMap::default();
5646                heights.insert(block_ids[0], 5);
5647                rhs_editor.resize_blocks(heights, None, cx);
5648            });
5649        });
5650
5651        cx.run_until_parked();
5652
5653        assert_eq!(
5654            get_block_height(&rhs_editor, block_ids[0], &mut cx),
5655            Some(5)
5656        );
5657        assert_eq!(
5658            get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5659            Some(5)
5660        );
5661    }
5662
5663    #[gpui::test]
5664    async fn test_edit_spanning_excerpt_boundaries_then_resplit(cx: &mut gpui::TestAppContext) {
5665        use rope::Point;
5666        use unindent::Unindent as _;
5667
5668        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5669
5670        let base_text = "
5671            aaa
5672            bbb
5673            ccc
5674            ddd
5675            eee
5676            fff
5677            ggg
5678            hhh
5679            iii
5680            jjj
5681            kkk
5682            lll
5683        "
5684        .unindent();
5685        let current_text = base_text.clone();
5686
5687        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5688
5689        editor.update(cx, |editor, cx| {
5690            let path = PathKey::for_buffer(&buffer, cx);
5691            editor.update_excerpts_for_path(
5692                path,
5693                buffer.clone(),
5694                vec![
5695                    Point::new(0, 0)..Point::new(3, 3),
5696                    Point::new(5, 0)..Point::new(8, 3),
5697                    Point::new(10, 0)..Point::new(11, 3),
5698                ],
5699                0,
5700                diff.clone(),
5701                cx,
5702            );
5703        });
5704
5705        cx.run_until_parked();
5706
5707        buffer.update(cx, |buffer, cx| {
5708            buffer.edit([(Point::new(1, 0)..Point::new(10, 0), "")], None, cx);
5709        });
5710
5711        cx.run_until_parked();
5712
5713        editor.update_in(cx, |splittable_editor, window, cx| {
5714            splittable_editor.unsplit(window, cx);
5715        });
5716
5717        cx.run_until_parked();
5718
5719        editor.update_in(cx, |splittable_editor, window, cx| {
5720            splittable_editor.split(window, cx);
5721        });
5722
5723        cx.run_until_parked();
5724    }
5725
5726    #[gpui::test]
5727    async fn test_range_folds_removed_on_split(cx: &mut gpui::TestAppContext) {
5728        use rope::Point;
5729        use unindent::Unindent as _;
5730
5731        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5732
5733        let base_text = "
5734            aaa
5735            bbb
5736            ccc
5737            ddd
5738            eee"
5739        .unindent();
5740        let current_text = "
5741            aaa
5742            bbb
5743            ccc
5744            ddd
5745            eee"
5746        .unindent();
5747
5748        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5749
5750        editor.update(cx, |editor, cx| {
5751            let path = PathKey::for_buffer(&buffer, cx);
5752            editor.update_excerpts_for_path(
5753                path,
5754                buffer.clone(),
5755                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5756                0,
5757                diff.clone(),
5758                cx,
5759            );
5760        });
5761
5762        cx.run_until_parked();
5763
5764        editor.update_in(cx, |editor, window, cx| {
5765            editor.rhs_editor.update(cx, |rhs_editor, cx| {
5766                rhs_editor.fold_creases(
5767                    vec![Crease::simple(
5768                        Point::new(1, 0)..Point::new(3, 0),
5769                        FoldPlaceholder::test(),
5770                    )],
5771                    false,
5772                    window,
5773                    cx,
5774                );
5775            });
5776        });
5777
5778        cx.run_until_parked();
5779
5780        editor.update_in(cx, |editor, window, cx| {
5781            editor.split(window, cx);
5782        });
5783
5784        cx.run_until_parked();
5785
5786        let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
5787            (
5788                editor.rhs_editor.clone(),
5789                editor.lhs.as_ref().unwrap().editor.clone(),
5790            )
5791        });
5792
5793        let rhs_has_folds_after_split = rhs_editor.update(cx, |editor, cx| {
5794            let snapshot = editor.display_snapshot(cx);
5795            snapshot
5796                .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5797                .next()
5798                .is_some()
5799        });
5800        assert!(
5801            !rhs_has_folds_after_split,
5802            "rhs should not have range folds after split"
5803        );
5804
5805        let lhs_has_folds = lhs_editor.update(cx, |editor, cx| {
5806            let snapshot = editor.display_snapshot(cx);
5807            snapshot
5808                .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5809                .next()
5810                .is_some()
5811        });
5812        assert!(!lhs_has_folds, "lhs should not have any range folds");
5813    }
5814
5815    #[gpui::test]
5816    async fn test_multiline_inlays_create_spacers(cx: &mut gpui::TestAppContext) {
5817        use rope::Point;
5818        use unindent::Unindent as _;
5819
5820        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5821
5822        let base_text = "
5823            aaa
5824            bbb
5825            ccc
5826            ddd
5827        "
5828        .unindent();
5829        let current_text = base_text.clone();
5830
5831        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
5832
5833        editor.update(cx, |editor, cx| {
5834            let path = PathKey::for_buffer(&buffer, cx);
5835            editor.update_excerpts_for_path(
5836                path,
5837                buffer.clone(),
5838                vec![Point::new(0, 0)..Point::new(3, 3)],
5839                0,
5840                diff.clone(),
5841                cx,
5842            );
5843        });
5844
5845        cx.run_until_parked();
5846
5847        let rhs_editor = editor.read_with(cx, |e, _| e.rhs_editor.clone());
5848        rhs_editor.update(cx, |rhs_editor, cx| {
5849            let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5850            rhs_editor.splice_inlays(
5851                &[],
5852                vec![
5853                    Inlay::edit_prediction(
5854                        0,
5855                        snapshot.anchor_after(Point::new(0, 3)),
5856                        "\nINLAY_WITHIN",
5857                    ),
5858                    Inlay::edit_prediction(
5859                        1,
5860                        snapshot.anchor_after(Point::new(1, 3)),
5861                        "\nINLAY_MID_1\nINLAY_MID_2",
5862                    ),
5863                    Inlay::edit_prediction(
5864                        2,
5865                        snapshot.anchor_after(Point::new(3, 3)),
5866                        "\nINLAY_END_1\nINLAY_END_2",
5867                    ),
5868                ],
5869                cx,
5870            );
5871        });
5872
5873        cx.run_until_parked();
5874
5875        assert_split_content(
5876            &editor,
5877            "
5878            § <no file>
5879            § -----
5880            aaa
5881            INLAY_WITHIN
5882            bbb
5883            INLAY_MID_1
5884            INLAY_MID_2
5885            ccc
5886            ddd
5887            INLAY_END_1
5888            INLAY_END_2"
5889                .unindent(),
5890            "
5891            § <no file>
5892            § -----
5893            aaa
5894            § spacer
5895            bbb
5896            § spacer
5897            § spacer
5898            ccc
5899            ddd
5900            § spacer
5901            § spacer"
5902                .unindent(),
5903            &mut cx,
5904        );
5905    }
5906
5907    #[gpui::test]
5908    async fn test_split_after_removing_folded_buffer(cx: &mut gpui::TestAppContext) {
5909        use rope::Point;
5910        use unindent::Unindent as _;
5911
5912        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5913
5914        let base_text_a = "
5915            aaa
5916            bbb
5917            ccc
5918        "
5919        .unindent();
5920        let current_text_a = "
5921            aaa
5922            bbb modified
5923            ccc
5924        "
5925        .unindent();
5926
5927        let base_text_b = "
5928            xxx
5929            yyy
5930            zzz
5931        "
5932        .unindent();
5933        let current_text_b = "
5934            xxx
5935            yyy modified
5936            zzz
5937        "
5938        .unindent();
5939
5940        let (buffer_a, diff_a) = buffer_with_diff(&base_text_a, &current_text_a, &mut cx);
5941        let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, &current_text_b, &mut cx);
5942
5943        let path_a = cx.read(|cx| PathKey::for_buffer(&buffer_a, cx));
5944        let path_b = cx.read(|cx| PathKey::for_buffer(&buffer_b, cx));
5945
5946        editor.update(cx, |editor, cx| {
5947            editor.update_excerpts_for_path(
5948                path_a.clone(),
5949                buffer_a.clone(),
5950                vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
5951                0,
5952                diff_a.clone(),
5953                cx,
5954            );
5955            editor.update_excerpts_for_path(
5956                path_b.clone(),
5957                buffer_b.clone(),
5958                vec![Point::new(0, 0)..buffer_b.read(cx).max_point()],
5959                0,
5960                diff_b.clone(),
5961                cx,
5962            );
5963        });
5964
5965        cx.run_until_parked();
5966
5967        let buffer_a_id = buffer_a.read_with(cx, |buffer, _| buffer.remote_id());
5968        editor.update(cx, |editor, cx| {
5969            editor.rhs_editor().update(cx, |right_editor, cx| {
5970                right_editor.fold_buffer(buffer_a_id, cx)
5971            });
5972        });
5973
5974        cx.run_until_parked();
5975
5976        editor.update(cx, |editor, cx| {
5977            editor.remove_excerpts_for_path(path_a.clone(), cx);
5978        });
5979        cx.run_until_parked();
5980
5981        editor.update_in(cx, |editor, window, cx| editor.split(window, cx));
5982        cx.run_until_parked();
5983
5984        editor.update(cx, |editor, cx| {
5985            editor.update_excerpts_for_path(
5986                path_a.clone(),
5987                buffer_a.clone(),
5988                vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
5989                0,
5990                diff_a.clone(),
5991                cx,
5992            );
5993            assert!(
5994                !editor
5995                    .lhs_editor()
5996                    .unwrap()
5997                    .read(cx)
5998                    .is_buffer_folded(buffer_a_id, cx)
5999            );
6000            assert!(
6001                !editor
6002                    .rhs_editor()
6003                    .read(cx)
6004                    .is_buffer_folded(buffer_a_id, cx)
6005            );
6006        });
6007    }
6008
6009    #[gpui::test]
6010    async fn test_two_path_keys_for_one_buffer(cx: &mut gpui::TestAppContext) {
6011        use multi_buffer::PathKey;
6012        use rope::Point;
6013        use unindent::Unindent as _;
6014
6015        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
6016
6017        let base_text = "
6018            aaa
6019            bbb
6020            ccc
6021        "
6022        .unindent();
6023        let current_text = "
6024            aaa
6025            bbb modified
6026            ccc
6027        "
6028        .unindent();
6029
6030        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
6031
6032        let path_key_1 = PathKey {
6033            sort_prefix: Some(0),
6034            path: rel_path("file1.txt").into(),
6035        };
6036        let path_key_2 = PathKey {
6037            sort_prefix: Some(1),
6038            path: rel_path("file1.txt").into(),
6039        };
6040
6041        editor.update(cx, |editor, cx| {
6042            editor.update_excerpts_for_path(
6043                path_key_1.clone(),
6044                buffer.clone(),
6045                vec![Point::new(0, 0)..Point::new(1, 0)],
6046                0,
6047                diff.clone(),
6048                cx,
6049            );
6050            editor.update_excerpts_for_path(
6051                path_key_2.clone(),
6052                buffer.clone(),
6053                vec![Point::new(1, 0)..buffer.read(cx).max_point()],
6054                1,
6055                diff.clone(),
6056                cx,
6057            );
6058        });
6059
6060        cx.run_until_parked();
6061    }
6062
6063    #[gpui::test]
6064    async fn test_act_as_type(cx: &mut gpui::TestAppContext) {
6065        let (splittable_editor, cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
6066        let editor = splittable_editor.read_with(cx, |editor, cx| {
6067            editor.act_as_type(TypeId::of::<Editor>(), &splittable_editor, cx)
6068        });
6069
6070        assert!(
6071            editor.is_some(),
6072            "SplittableEditor should be able to act as Editor"
6073        );
6074    }
6075}