split.rs

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