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