split.rs

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