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