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 mut 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            for (lhs, rhs) in
 617                lhs.update_path_excerpts_from_rhs(path, &self.rhs_multibuffer, diff.clone(), cx)
 618            {
 619                companion.add_excerpt_mapping(lhs, rhs);
 620            }
 621            companion.add_buffer_mapping(
 622                diff.read(cx).base_text(cx).remote_id(),
 623                diff.read(cx).buffer_id,
 624            );
 625        }
 626
 627        let companion = cx.new(|_| companion);
 628
 629        rhs_display_map.update(cx, |dm, cx| {
 630            dm.set_companion(Some((lhs_display_map, companion.clone())), cx);
 631        });
 632
 633        let shared_scroll_anchor = self
 634            .rhs_editor
 635            .read(cx)
 636            .scroll_manager
 637            .scroll_anchor_entity();
 638        lhs.editor.update(cx, |editor, _cx| {
 639            editor
 640                .scroll_manager
 641                .set_shared_scroll_anchor(shared_scroll_anchor);
 642        });
 643
 644        let this = cx.entity().downgrade();
 645        self.rhs_editor.update(cx, |editor, _cx| {
 646            let this = this.clone();
 647            editor.set_on_local_selections_changed(Some(Box::new(
 648                move |cursor_position, window, cx| {
 649                    let this = this.clone();
 650                    window.defer(cx, move |window, cx| {
 651                        this.update(cx, |this, cx| {
 652                            this.sync_cursor_to_other_side(true, cursor_position, window, cx);
 653                        })
 654                        .ok();
 655                    })
 656                },
 657            )));
 658        });
 659        lhs.editor.update(cx, |editor, _cx| {
 660            let this = this.clone();
 661            editor.set_on_local_selections_changed(Some(Box::new(
 662                move |cursor_position, window, cx| {
 663                    let this = this.clone();
 664                    window.defer(cx, move |window, cx| {
 665                        this.update(cx, |this, cx| {
 666                            this.sync_cursor_to_other_side(false, cursor_position, window, cx);
 667                        })
 668                        .ok();
 669                    })
 670                },
 671            )));
 672        });
 673
 674        // Copy soft wrap state from rhs (source of truth) to lhs
 675        let rhs_soft_wrap_override = self.rhs_editor.read(cx).soft_wrap_mode_override;
 676        lhs.editor.update(cx, |editor, cx| {
 677            editor.soft_wrap_mode_override = rhs_soft_wrap_override;
 678            cx.notify();
 679        });
 680
 681        self.lhs = Some(lhs);
 682
 683        cx.notify();
 684    }
 685
 686    fn activate_pane_left(
 687        &mut self,
 688        _: &ActivatePaneLeft,
 689        window: &mut Window,
 690        cx: &mut Context<Self>,
 691    ) {
 692        if let Some(lhs) = &self.lhs {
 693            if !lhs.was_last_focused {
 694                lhs.editor.read(cx).focus_handle(cx).focus(window, cx);
 695                lhs.editor.update(cx, |editor, cx| {
 696                    editor.request_autoscroll(Autoscroll::fit(), cx);
 697                });
 698            } else {
 699                cx.propagate();
 700            }
 701        } else {
 702            cx.propagate();
 703        }
 704    }
 705
 706    fn activate_pane_right(
 707        &mut self,
 708        _: &ActivatePaneRight,
 709        window: &mut Window,
 710        cx: &mut Context<Self>,
 711    ) {
 712        if let Some(lhs) = &self.lhs {
 713            if lhs.was_last_focused {
 714                self.rhs_editor.read(cx).focus_handle(cx).focus(window, cx);
 715                self.rhs_editor.update(cx, |editor, cx| {
 716                    editor.request_autoscroll(Autoscroll::fit(), cx);
 717                });
 718            } else {
 719                cx.propagate();
 720            }
 721        } else {
 722            cx.propagate();
 723        }
 724    }
 725
 726    fn sync_cursor_to_other_side(
 727        &mut self,
 728        from_rhs: bool,
 729        source_point: Point,
 730        window: &mut Window,
 731        cx: &mut Context<Self>,
 732    ) {
 733        let Some(lhs) = &self.lhs else {
 734            return;
 735        };
 736
 737        let (source_editor, target_editor) = if from_rhs {
 738            (&self.rhs_editor, &lhs.editor)
 739        } else {
 740            (&lhs.editor, &self.rhs_editor)
 741        };
 742
 743        let source_snapshot = source_editor.update(cx, |editor, cx| editor.snapshot(window, cx));
 744        let target_snapshot = target_editor.update(cx, |editor, cx| editor.snapshot(window, cx));
 745
 746        let display_point = source_snapshot
 747            .display_snapshot
 748            .point_to_display_point(source_point, Bias::Right);
 749        let display_point = target_snapshot.clip_point(display_point, Bias::Right);
 750        let target_point = target_snapshot.display_point_to_point(display_point, Bias::Right);
 751
 752        target_editor.update(cx, |editor, cx| {
 753            editor.set_suppress_selection_callback(true);
 754            editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
 755                s.select_ranges([target_point..target_point]);
 756            });
 757            editor.set_suppress_selection_callback(false);
 758        });
 759    }
 760
 761    fn toggle_split(&mut self, _: &ToggleDiffView, window: &mut Window, cx: &mut Context<Self>) {
 762        if self.lhs.is_some() {
 763            self.unsplit(window, cx);
 764        } else {
 765            self.split(window, cx);
 766        }
 767    }
 768
 769    fn intercept_toggle_breakpoint(
 770        &mut self,
 771        _: &ToggleBreakpoint,
 772        _window: &mut Window,
 773        cx: &mut Context<Self>,
 774    ) {
 775        // Only block breakpoint actions when the left (lhs) editor has focus
 776        if let Some(lhs) = &self.lhs {
 777            if lhs.was_last_focused {
 778                cx.stop_propagation();
 779            } else {
 780                cx.propagate();
 781            }
 782        } else {
 783            cx.propagate();
 784        }
 785    }
 786
 787    fn intercept_enable_breakpoint(
 788        &mut self,
 789        _: &EnableBreakpoint,
 790        _window: &mut Window,
 791        cx: &mut Context<Self>,
 792    ) {
 793        // Only block breakpoint actions when the left (lhs) editor has focus
 794        if let Some(lhs) = &self.lhs {
 795            if lhs.was_last_focused {
 796                cx.stop_propagation();
 797            } else {
 798                cx.propagate();
 799            }
 800        } else {
 801            cx.propagate();
 802        }
 803    }
 804
 805    fn intercept_disable_breakpoint(
 806        &mut self,
 807        _: &DisableBreakpoint,
 808        _window: &mut Window,
 809        cx: &mut Context<Self>,
 810    ) {
 811        // Only block breakpoint actions when the left (lhs) editor has focus
 812        if let Some(lhs) = &self.lhs {
 813            if lhs.was_last_focused {
 814                cx.stop_propagation();
 815            } else {
 816                cx.propagate();
 817            }
 818        } else {
 819            cx.propagate();
 820        }
 821    }
 822
 823    fn intercept_edit_log_breakpoint(
 824        &mut self,
 825        _: &EditLogBreakpoint,
 826        _window: &mut Window,
 827        cx: &mut Context<Self>,
 828    ) {
 829        // Only block breakpoint actions when the left (lhs) editor has focus
 830        if let Some(lhs) = &self.lhs {
 831            if lhs.was_last_focused {
 832                cx.stop_propagation();
 833            } else {
 834                cx.propagate();
 835            }
 836        } else {
 837            cx.propagate();
 838        }
 839    }
 840
 841    fn intercept_inline_assist(
 842        &mut self,
 843        _: &InlineAssist,
 844        _window: &mut Window,
 845        cx: &mut Context<Self>,
 846    ) {
 847        if self.lhs.is_some() {
 848            cx.stop_propagation();
 849        } else {
 850            cx.propagate();
 851        }
 852    }
 853
 854    fn toggle_soft_wrap(
 855        &mut self,
 856        _: &ToggleSoftWrap,
 857        window: &mut Window,
 858        cx: &mut Context<Self>,
 859    ) {
 860        if let Some(lhs) = &self.lhs {
 861            cx.stop_propagation();
 862
 863            let is_lhs_focused = lhs.was_last_focused;
 864            let (focused_editor, other_editor) = if is_lhs_focused {
 865                (&lhs.editor, &self.rhs_editor)
 866            } else {
 867                (&self.rhs_editor, &lhs.editor)
 868            };
 869
 870            // Toggle the focused editor
 871            focused_editor.update(cx, |editor, cx| {
 872                editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
 873            });
 874
 875            // Copy the soft wrap state from the focused editor to the other editor
 876            let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
 877            other_editor.update(cx, |editor, cx| {
 878                editor.soft_wrap_mode_override = soft_wrap_override;
 879                cx.notify();
 880            });
 881        } else {
 882            cx.propagate();
 883        }
 884    }
 885
 886    fn unsplit(&mut self, _: &mut Window, cx: &mut Context<Self>) {
 887        let Some(lhs) = self.lhs.take() else {
 888            return;
 889        };
 890        self.rhs_editor.update(cx, |rhs, cx| {
 891            let rhs_snapshot = rhs.display_map.update(cx, |dm, cx| dm.snapshot(cx));
 892            let native_anchor = rhs.scroll_manager.native_anchor(&rhs_snapshot, cx);
 893            let rhs_display_map_id = rhs_snapshot.display_map_id;
 894            rhs.scroll_manager
 895                .scroll_anchor_entity()
 896                .update(cx, |shared, _| {
 897                    shared.scroll_anchor = native_anchor;
 898                    shared.display_map_id = Some(rhs_display_map_id);
 899                });
 900
 901            rhs.set_on_local_selections_changed(None);
 902            rhs.set_delegate_expand_excerpts(false);
 903            rhs.buffer().update(cx, |buffer, cx| {
 904                buffer.set_show_deleted_hunks(true, cx);
 905                buffer.set_use_extended_diff_range(false, cx);
 906            });
 907            rhs.display_map.update(cx, |dm, cx| {
 908                dm.set_companion(None, cx);
 909            });
 910        });
 911        lhs.editor.update(cx, |editor, _cx| {
 912            editor.set_on_local_selections_changed(None);
 913        });
 914        cx.notify();
 915    }
 916
 917    pub fn set_excerpts_for_path(
 918        &mut self,
 919        path: PathKey,
 920        buffer: Entity<Buffer>,
 921        ranges: impl IntoIterator<Item = Range<Point>> + Clone,
 922        context_line_count: u32,
 923        diff: Entity<BufferDiff>,
 924        cx: &mut Context<Self>,
 925    ) -> (Vec<Range<Anchor>>, bool) {
 926        let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
 927        let lhs_display_map = self
 928            .lhs
 929            .as_ref()
 930            .map(|s| s.editor.read(cx).display_map.clone());
 931
 932        let (anchors, added_a_new_excerpt) =
 933            self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
 934                let (anchors, added_a_new_excerpt) = rhs_multibuffer.set_excerpts_for_path(
 935                    path.clone(),
 936                    buffer.clone(),
 937                    ranges,
 938                    context_line_count,
 939                    cx,
 940                );
 941                if !anchors.is_empty()
 942                    && rhs_multibuffer
 943                        .diff_for(buffer.read(cx).remote_id())
 944                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
 945                {
 946                    rhs_multibuffer.add_diff(diff.clone(), cx);
 947                }
 948                (anchors, added_a_new_excerpt)
 949            });
 950
 951        if let Some(lhs) = &mut self.lhs {
 952            if let Some(lhs_display_map) = &lhs_display_map {
 953                lhs.sync_path_excerpts(
 954                    path,
 955                    &self.rhs_multibuffer,
 956                    diff,
 957                    &rhs_display_map,
 958                    lhs_display_map,
 959                    cx,
 960                );
 961            }
 962        }
 963
 964        (anchors, added_a_new_excerpt)
 965    }
 966
 967    fn expand_excerpts(
 968        &mut self,
 969        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
 970        lines: u32,
 971        direction: ExpandExcerptDirection,
 972        cx: &mut Context<Self>,
 973    ) {
 974        let mut corresponding_paths = HashMap::default();
 975        self.rhs_multibuffer.update(cx, |multibuffer, cx| {
 976            let snapshot = multibuffer.snapshot(cx);
 977            if self.lhs.is_some() {
 978                corresponding_paths = excerpt_ids
 979                    .clone()
 980                    .map(|excerpt_id| {
 981                        let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
 982                        let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
 983                        let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
 984                        (path, diff)
 985                    })
 986                    .collect::<HashMap<_, _>>();
 987            }
 988            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
 989        });
 990
 991        if let Some(lhs) = &mut self.lhs {
 992            let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
 993            let lhs_display_map = lhs.editor.read(cx).display_map.clone();
 994            for (path, diff) in corresponding_paths {
 995                lhs.sync_path_excerpts(
 996                    path,
 997                    &self.rhs_multibuffer,
 998                    diff,
 999                    &rhs_display_map,
1000                    &lhs_display_map,
1001                    cx,
1002                );
1003            }
1004        }
1005    }
1006
1007    pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
1008        self.rhs_multibuffer.update(cx, |buffer, cx| {
1009            buffer.remove_excerpts_for_path(path.clone(), cx)
1010        });
1011        if let Some(lhs) = &self.lhs {
1012            let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
1013            let lhs_display_map = lhs.editor.read(cx).display_map.clone();
1014            lhs.remove_mappings_for_path(
1015                &path,
1016                &self.rhs_multibuffer,
1017                &rhs_display_map,
1018                &lhs_display_map,
1019                cx,
1020            );
1021            lhs.multibuffer
1022                .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
1023        }
1024    }
1025}
1026
1027#[cfg(test)]
1028impl SplittableEditor {
1029    fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1030        use multi_buffer::MultiBufferRow;
1031        use text::Bias;
1032
1033        use crate::display_map::Block;
1034        use crate::display_map::DisplayRow;
1035
1036        self.debug_print(cx);
1037
1038        let lhs = self.lhs.as_ref().unwrap();
1039        let rhs_excerpts = self.rhs_multibuffer.read(cx).excerpt_ids();
1040        let lhs_excerpts = lhs.multibuffer.read(cx).excerpt_ids();
1041        assert_eq!(
1042            lhs_excerpts.len(),
1043            rhs_excerpts.len(),
1044            "mismatch in excerpt count"
1045        );
1046
1047        if quiesced {
1048            let rhs_snapshot = lhs
1049                .editor
1050                .update(cx, |editor, cx| editor.display_snapshot(cx));
1051            let lhs_snapshot = self
1052                .rhs_editor
1053                .update(cx, |editor, cx| editor.display_snapshot(cx));
1054
1055            let lhs_max_row = lhs_snapshot.max_point().row();
1056            let rhs_max_row = rhs_snapshot.max_point().row();
1057            assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1058
1059            let lhs_excerpt_block_rows = lhs_snapshot
1060                .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1061                .filter(|(_, block)| {
1062                    matches!(
1063                        block,
1064                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1065                    )
1066                })
1067                .map(|(row, _)| row)
1068                .collect::<Vec<_>>();
1069            let rhs_excerpt_block_rows = rhs_snapshot
1070                .blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
1071                .filter(|(_, block)| {
1072                    matches!(
1073                        block,
1074                        Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1075                    )
1076                })
1077                .map(|(row, _)| row)
1078                .collect::<Vec<_>>();
1079            assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1080
1081            for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1082                assert_eq!(
1083                    lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1084                    "mismatch in hunks"
1085                );
1086                assert_eq!(
1087                    lhs_hunk.status, rhs_hunk.status,
1088                    "mismatch in hunk statuses"
1089                );
1090
1091                let (lhs_point, rhs_point) =
1092                    if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1093                        (
1094                            Point::new(lhs_hunk.row_range.end.0, 0),
1095                            Point::new(rhs_hunk.row_range.end.0, 0),
1096                        )
1097                    } else {
1098                        (
1099                            Point::new(lhs_hunk.row_range.start.0, 0),
1100                            Point::new(rhs_hunk.row_range.start.0, 0),
1101                        )
1102                    };
1103                let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1104                let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1105                assert_eq!(
1106                    lhs_point.row(),
1107                    rhs_point.row(),
1108                    "mismatch in hunk position"
1109                );
1110            }
1111
1112            // Filtering out empty lines is a bit of a hack, to work around a case where
1113            // the base text has a trailing newline but the current text doesn't, or vice versa.
1114            // In this case, we get the additional newline on one side, but that line is not
1115            // marked as added/deleted by rowinfos.
1116            self.check_sides_match(cx, |snapshot| {
1117                snapshot
1118                    .buffer_snapshot()
1119                    .text()
1120                    .split("\n")
1121                    .zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
1122                    .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
1123                    .map(|(line, _)| line.to_owned())
1124                    .collect::<Vec<_>>()
1125            });
1126        }
1127    }
1128
1129    #[track_caller]
1130    fn check_sides_match<T: std::fmt::Debug + PartialEq>(
1131        &self,
1132        cx: &mut App,
1133        mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
1134    ) {
1135        let lhs = self.lhs.as_ref().expect("requires split");
1136        let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1137            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1138        });
1139        let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1140            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1141        });
1142
1143        let rhs_t = extract(&rhs_snapshot);
1144        let lhs_t = extract(&lhs_snapshot);
1145
1146        if rhs_t != lhs_t {
1147            self.debug_print(cx);
1148            pretty_assertions::assert_eq!(rhs_t, lhs_t);
1149        }
1150    }
1151
1152    fn debug_print(&self, cx: &mut App) {
1153        use crate::DisplayRow;
1154        use crate::display_map::Block;
1155        use buffer_diff::DiffHunkStatusKind;
1156
1157        assert!(
1158            self.lhs.is_some(),
1159            "debug_print is only useful when lhs editor exists"
1160        );
1161
1162        let lhs = self.lhs.as_ref().unwrap();
1163
1164        // Get terminal width, default to 80 if unavailable
1165        let terminal_width = std::env::var("COLUMNS")
1166            .ok()
1167            .and_then(|s| s.parse::<usize>().ok())
1168            .unwrap_or(80);
1169
1170        // Each side gets half the terminal width minus the separator
1171        let separator = "";
1172        let side_width = (terminal_width - separator.len()) / 2;
1173
1174        // Get display snapshots for both editors
1175        let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1176            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1177        });
1178        let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1179            editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1180        });
1181
1182        let lhs_max_row = lhs_snapshot.max_point().row().0;
1183        let rhs_max_row = rhs_snapshot.max_point().row().0;
1184        let max_row = lhs_max_row.max(rhs_max_row);
1185
1186        // Build a map from display row -> block type string
1187        // Each row of a multi-row block gets an entry with the same block type
1188        // For spacers, the ID is included in brackets
1189        fn build_block_map(
1190            snapshot: &crate::DisplaySnapshot,
1191            max_row: u32,
1192        ) -> std::collections::HashMap<u32, String> {
1193            let mut block_map = std::collections::HashMap::new();
1194            for (start_row, block) in
1195                snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1196            {
1197                let (block_type, height) = match block {
1198                    Block::Spacer {
1199                        id,
1200                        height,
1201                        is_below: _,
1202                    } => (format!("SPACER[{}]", id.0), *height),
1203                    Block::ExcerptBoundary { height, .. } => {
1204                        ("EXCERPT_BOUNDARY".to_string(), *height)
1205                    }
1206                    Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1207                    Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1208                    Block::Custom(custom) => {
1209                        ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1210                    }
1211                };
1212                for offset in 0..height {
1213                    block_map.insert(start_row.0 + offset, block_type.clone());
1214                }
1215            }
1216            block_map
1217        }
1218
1219        let lhs_blocks = build_block_map(&lhs_snapshot, lhs_max_row);
1220        let rhs_blocks = build_block_map(&rhs_snapshot, rhs_max_row);
1221
1222        fn display_width(s: &str) -> usize {
1223            unicode_width::UnicodeWidthStr::width(s)
1224        }
1225
1226        fn truncate_line(line: &str, max_width: usize) -> String {
1227            let line_width = display_width(line);
1228            if line_width <= max_width {
1229                return line.to_string();
1230            }
1231            if max_width < 9 {
1232                let mut result = String::new();
1233                let mut width = 0;
1234                for c in line.chars() {
1235                    let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1236                    if width + c_width > max_width {
1237                        break;
1238                    }
1239                    result.push(c);
1240                    width += c_width;
1241                }
1242                return result;
1243            }
1244            let ellipsis = "...";
1245            let target_prefix_width = 3;
1246            let target_suffix_width = 3;
1247
1248            let mut prefix = String::new();
1249            let mut prefix_width = 0;
1250            for c in line.chars() {
1251                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1252                if prefix_width + c_width > target_prefix_width {
1253                    break;
1254                }
1255                prefix.push(c);
1256                prefix_width += c_width;
1257            }
1258
1259            let mut suffix_chars: Vec<char> = Vec::new();
1260            let mut suffix_width = 0;
1261            for c in line.chars().rev() {
1262                let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1263                if suffix_width + c_width > target_suffix_width {
1264                    break;
1265                }
1266                suffix_chars.push(c);
1267                suffix_width += c_width;
1268            }
1269            suffix_chars.reverse();
1270            let suffix: String = suffix_chars.into_iter().collect();
1271
1272            format!("{}{}{}", prefix, ellipsis, suffix)
1273        }
1274
1275        fn pad_to_width(s: &str, target_width: usize) -> String {
1276            let current_width = display_width(s);
1277            if current_width >= target_width {
1278                s.to_string()
1279            } else {
1280                format!("{}{}", s, " ".repeat(target_width - current_width))
1281            }
1282        }
1283
1284        // Helper to format a single row for one side
1285        // Format: "ln# diff bytes(cumul) text" or block info
1286        // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1287        fn format_row(
1288            row: u32,
1289            max_row: u32,
1290            snapshot: &crate::DisplaySnapshot,
1291            blocks: &std::collections::HashMap<u32, String>,
1292            row_infos: &[multi_buffer::RowInfo],
1293            cumulative_bytes: &[usize],
1294            side_width: usize,
1295        ) -> String {
1296            // Get row info if available
1297            let row_info = row_infos.get(row as usize);
1298
1299            // Line number prefix (3 chars + space)
1300            // Use buffer_row from RowInfo, which is None for block rows
1301            let line_prefix = if row > max_row {
1302                "    ".to_string()
1303            } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1304                format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1305            } else {
1306                "    ".to_string() // block rows have no line number
1307            };
1308            let content_width = side_width.saturating_sub(line_prefix.len());
1309
1310            if row > max_row {
1311                return format!("{}{}", line_prefix, " ".repeat(content_width));
1312            }
1313
1314            // Check if this row is a block row
1315            if let Some(block_type) = blocks.get(&row) {
1316                let block_str = format!("~~~[{}]~~~", block_type);
1317                let formatted = format!("{:^width$}", block_str, width = content_width);
1318                return format!(
1319                    "{}{}",
1320                    line_prefix,
1321                    truncate_line(&formatted, content_width)
1322                );
1323            }
1324
1325            // Get line text
1326            let line_text = snapshot.line(DisplayRow(row));
1327            let line_bytes = line_text.len();
1328
1329            // Diff status marker
1330            let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1331                Some(status) => match status.kind {
1332                    DiffHunkStatusKind::Added => "+",
1333                    DiffHunkStatusKind::Deleted => "-",
1334                    DiffHunkStatusKind::Modified => "~",
1335                },
1336                None => " ",
1337            };
1338
1339            // Cumulative bytes
1340            let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1341
1342            // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1343            let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1344            let text_width = content_width.saturating_sub(info_prefix.len());
1345            let truncated_text = truncate_line(&line_text, text_width);
1346
1347            let text_part = pad_to_width(&truncated_text, text_width);
1348            format!("{}{}{}", line_prefix, info_prefix, text_part)
1349        }
1350
1351        // Collect row infos for both sides
1352        let lhs_row_infos: Vec<_> = lhs_snapshot
1353            .row_infos(DisplayRow(0))
1354            .take((lhs_max_row + 1) as usize)
1355            .collect();
1356        let rhs_row_infos: Vec<_> = rhs_snapshot
1357            .row_infos(DisplayRow(0))
1358            .take((rhs_max_row + 1) as usize)
1359            .collect();
1360
1361        // Calculate cumulative bytes for each side (only counting non-block rows)
1362        let mut lhs_cumulative = Vec::with_capacity((lhs_max_row + 1) as usize);
1363        let mut cumulative = 0usize;
1364        for row in 0..=lhs_max_row {
1365            if !lhs_blocks.contains_key(&row) {
1366                cumulative += lhs_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1367            }
1368            lhs_cumulative.push(cumulative);
1369        }
1370
1371        let mut rhs_cumulative = Vec::with_capacity((rhs_max_row + 1) as usize);
1372        cumulative = 0;
1373        for row in 0..=rhs_max_row {
1374            if !rhs_blocks.contains_key(&row) {
1375                cumulative += rhs_snapshot.line(DisplayRow(row)).len() + 1;
1376            }
1377            rhs_cumulative.push(cumulative);
1378        }
1379
1380        // Print header
1381        eprintln!();
1382        eprintln!("{}", "".repeat(terminal_width));
1383        let header_left = format!("{:^width$}", "(LHS)", width = side_width);
1384        let header_right = format!("{:^width$}", "(RHS)", width = side_width);
1385        eprintln!("{}{}{}", header_left, separator, header_right);
1386        eprintln!(
1387            "{:^width$}{}{:^width$}",
1388            "ln# diff len(cum) text",
1389            separator,
1390            "ln# diff len(cum) text",
1391            width = side_width
1392        );
1393        eprintln!("{}", "".repeat(terminal_width));
1394
1395        // Print each row
1396        for row in 0..=max_row {
1397            let left = format_row(
1398                row,
1399                lhs_max_row,
1400                &lhs_snapshot,
1401                &lhs_blocks,
1402                &lhs_row_infos,
1403                &lhs_cumulative,
1404                side_width,
1405            );
1406            let right = format_row(
1407                row,
1408                rhs_max_row,
1409                &rhs_snapshot,
1410                &rhs_blocks,
1411                &rhs_row_infos,
1412                &rhs_cumulative,
1413                side_width,
1414            );
1415            eprintln!("{}{}{}", left, separator, right);
1416        }
1417
1418        eprintln!("{}", "".repeat(terminal_width));
1419        eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1420        eprintln!();
1421    }
1422
1423    fn randomly_edit_excerpts(
1424        &mut self,
1425        rng: &mut impl rand::Rng,
1426        mutation_count: usize,
1427        cx: &mut Context<Self>,
1428    ) {
1429        use collections::HashSet;
1430        use rand::prelude::*;
1431        use std::env;
1432        use util::RandomCharIter;
1433
1434        let max_buffers = env::var("MAX_BUFFERS")
1435            .map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
1436            .unwrap_or(4);
1437
1438        for _ in 0..mutation_count {
1439            let paths = self
1440                .rhs_multibuffer
1441                .read(cx)
1442                .paths()
1443                .cloned()
1444                .collect::<Vec<_>>();
1445            let excerpt_ids = self.rhs_multibuffer.read(cx).excerpt_ids();
1446
1447            if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
1448                let mut excerpts = HashSet::default();
1449                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
1450                    excerpts.extend(excerpt_ids.choose(rng).copied());
1451                }
1452
1453                let line_count = rng.random_range(1..5);
1454
1455                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
1456
1457                self.expand_excerpts(
1458                    excerpts.iter().cloned(),
1459                    line_count,
1460                    ExpandExcerptDirection::UpAndDown,
1461                    cx,
1462                );
1463                continue;
1464            }
1465
1466            if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
1467                let len = rng.random_range(100..500);
1468                let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
1469                let buffer = cx.new(|cx| Buffer::local(text, cx));
1470                log::info!(
1471                    "Creating new buffer {} with text: {:?}",
1472                    buffer.read(cx).remote_id(),
1473                    buffer.read(cx).text()
1474                );
1475                let buffer_snapshot = buffer.read(cx).snapshot();
1476                let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
1477                // Create some initial diff hunks.
1478                buffer.update(cx, |buffer, cx| {
1479                    buffer.randomly_edit(rng, 1, cx);
1480                });
1481                let buffer_snapshot = buffer.read(cx).text_snapshot();
1482                diff.update(cx, |diff, cx| {
1483                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
1484                });
1485                let path = PathKey::for_buffer(&buffer, cx);
1486                let ranges = diff.update(cx, |diff, cx| {
1487                    diff.snapshot(cx)
1488                        .hunks(&buffer_snapshot)
1489                        .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
1490                        .collect::<Vec<_>>()
1491                });
1492                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1493            } else {
1494                log::info!("removing excerpts");
1495                let remove_count = rng.random_range(1..=paths.len());
1496                let paths_to_remove = paths
1497                    .choose_multiple(rng, remove_count)
1498                    .cloned()
1499                    .collect::<Vec<_>>();
1500                for path in paths_to_remove {
1501                    self.remove_excerpts_for_path(path.clone(), cx);
1502                }
1503            }
1504        }
1505    }
1506}
1507
1508impl Item for SplittableEditor {
1509    type Event = EditorEvent;
1510
1511    fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1512        self.rhs_editor.read(cx).tab_content_text(detail, cx)
1513    }
1514
1515    fn tab_tooltip_text(&self, cx: &App) -> Option<ui::SharedString> {
1516        self.rhs_editor.read(cx).tab_tooltip_text(cx)
1517    }
1518
1519    fn tab_icon(&self, window: &Window, cx: &App) -> Option<ui::Icon> {
1520        self.rhs_editor.read(cx).tab_icon(window, cx)
1521    }
1522
1523    fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> gpui::AnyElement {
1524        self.rhs_editor.read(cx).tab_content(params, window, cx)
1525    }
1526
1527    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
1528        Editor::to_item_events(event, f)
1529    }
1530
1531    fn for_each_project_item(
1532        &self,
1533        cx: &App,
1534        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1535    ) {
1536        self.rhs_editor.read(cx).for_each_project_item(cx, f)
1537    }
1538
1539    fn buffer_kind(&self, cx: &App) -> ItemBufferKind {
1540        self.rhs_editor.read(cx).buffer_kind(cx)
1541    }
1542
1543    fn is_dirty(&self, cx: &App) -> bool {
1544        self.rhs_editor.read(cx).is_dirty(cx)
1545    }
1546
1547    fn has_conflict(&self, cx: &App) -> bool {
1548        self.rhs_editor.read(cx).has_conflict(cx)
1549    }
1550
1551    fn has_deleted_file(&self, cx: &App) -> bool {
1552        self.rhs_editor.read(cx).has_deleted_file(cx)
1553    }
1554
1555    fn capability(&self, cx: &App) -> language::Capability {
1556        self.rhs_editor.read(cx).capability(cx)
1557    }
1558
1559    fn can_save(&self, cx: &App) -> bool {
1560        self.rhs_editor.read(cx).can_save(cx)
1561    }
1562
1563    fn can_save_as(&self, cx: &App) -> bool {
1564        self.rhs_editor.read(cx).can_save_as(cx)
1565    }
1566
1567    fn save(
1568        &mut self,
1569        options: SaveOptions,
1570        project: Entity<Project>,
1571        window: &mut Window,
1572        cx: &mut Context<Self>,
1573    ) -> gpui::Task<anyhow::Result<()>> {
1574        self.rhs_editor
1575            .update(cx, |editor, cx| editor.save(options, project, window, cx))
1576    }
1577
1578    fn save_as(
1579        &mut self,
1580        project: Entity<Project>,
1581        path: project::ProjectPath,
1582        window: &mut Window,
1583        cx: &mut Context<Self>,
1584    ) -> gpui::Task<anyhow::Result<()>> {
1585        self.rhs_editor
1586            .update(cx, |editor, cx| editor.save_as(project, path, window, cx))
1587    }
1588
1589    fn reload(
1590        &mut self,
1591        project: Entity<Project>,
1592        window: &mut Window,
1593        cx: &mut Context<Self>,
1594    ) -> gpui::Task<anyhow::Result<()>> {
1595        self.rhs_editor
1596            .update(cx, |editor, cx| editor.reload(project, window, cx))
1597    }
1598
1599    fn navigate(
1600        &mut self,
1601        data: Arc<dyn std::any::Any + Send>,
1602        window: &mut Window,
1603        cx: &mut Context<Self>,
1604    ) -> bool {
1605        self.last_selected_editor()
1606            .update(cx, |editor, cx| editor.navigate(data, window, cx))
1607    }
1608
1609    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1610        self.last_selected_editor().update(cx, |editor, cx| {
1611            editor.deactivated(window, cx);
1612        });
1613    }
1614
1615    fn added_to_workspace(
1616        &mut self,
1617        workspace: &mut Workspace,
1618        window: &mut Window,
1619        cx: &mut Context<Self>,
1620    ) {
1621        self.workspace = workspace.weak_handle();
1622        self.rhs_editor.update(cx, |rhs_editor, cx| {
1623            rhs_editor.added_to_workspace(workspace, window, cx);
1624        });
1625        if let Some(lhs) = &self.lhs {
1626            lhs.editor.update(cx, |lhs_editor, cx| {
1627                lhs_editor.added_to_workspace(workspace, window, cx);
1628            });
1629        }
1630    }
1631
1632    fn as_searchable(
1633        &self,
1634        handle: &Entity<Self>,
1635        _: &App,
1636    ) -> Option<Box<dyn SearchableItemHandle>> {
1637        Some(Box::new(handle.clone()))
1638    }
1639
1640    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1641        self.rhs_editor.read(cx).breadcrumb_location(cx)
1642    }
1643
1644    fn breadcrumbs(&self, cx: &App) -> Option<Vec<BreadcrumbText>> {
1645        self.rhs_editor.read(cx).breadcrumbs(cx)
1646    }
1647
1648    fn pixel_position_of_cursor(&self, cx: &App) -> Option<gpui::Point<gpui::Pixels>> {
1649        self.last_selected_editor()
1650            .read(cx)
1651            .pixel_position_of_cursor(cx)
1652    }
1653}
1654
1655impl SearchableItem for SplittableEditor {
1656    type Match = Range<Anchor>;
1657
1658    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1659        self.last_selected_editor().update(cx, |editor, cx| {
1660            editor.clear_matches(window, cx);
1661        });
1662    }
1663
1664    fn update_matches(
1665        &mut self,
1666        matches: &[Self::Match],
1667        active_match_index: Option<usize>,
1668        window: &mut Window,
1669        cx: &mut Context<Self>,
1670    ) {
1671        self.last_selected_editor().update(cx, |editor, cx| {
1672            editor.update_matches(matches, active_match_index, window, cx);
1673        });
1674    }
1675
1676    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1677        self.last_selected_editor()
1678            .update(cx, |editor, cx| editor.query_suggestion(window, cx))
1679    }
1680
1681    fn activate_match(
1682        &mut self,
1683        index: usize,
1684        matches: &[Self::Match],
1685        window: &mut Window,
1686        cx: &mut Context<Self>,
1687    ) {
1688        self.last_selected_editor().update(cx, |editor, cx| {
1689            editor.activate_match(index, matches, window, cx);
1690        });
1691    }
1692
1693    fn select_matches(
1694        &mut self,
1695        matches: &[Self::Match],
1696        window: &mut Window,
1697        cx: &mut Context<Self>,
1698    ) {
1699        self.last_selected_editor().update(cx, |editor, cx| {
1700            editor.select_matches(matches, window, cx);
1701        });
1702    }
1703
1704    fn replace(
1705        &mut self,
1706        identifier: &Self::Match,
1707        query: &project::search::SearchQuery,
1708        window: &mut Window,
1709        cx: &mut Context<Self>,
1710    ) {
1711        self.last_selected_editor().update(cx, |editor, cx| {
1712            editor.replace(identifier, query, window, cx);
1713        });
1714    }
1715
1716    fn find_matches(
1717        &mut self,
1718        query: Arc<project::search::SearchQuery>,
1719        window: &mut Window,
1720        cx: &mut Context<Self>,
1721    ) -> gpui::Task<Vec<Self::Match>> {
1722        self.last_selected_editor()
1723            .update(cx, |editor, cx| editor.find_matches(query, window, cx))
1724    }
1725
1726    fn active_match_index(
1727        &mut self,
1728        direction: workspace::searchable::Direction,
1729        matches: &[Self::Match],
1730        window: &mut Window,
1731        cx: &mut Context<Self>,
1732    ) -> Option<usize> {
1733        self.last_selected_editor().update(cx, |editor, cx| {
1734            editor.active_match_index(direction, matches, window, cx)
1735        })
1736    }
1737}
1738
1739impl EventEmitter<EditorEvent> for SplittableEditor {}
1740impl EventEmitter<SearchEvent> for SplittableEditor {}
1741impl Focusable for SplittableEditor {
1742    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1743        self.last_selected_editor().read(cx).focus_handle(cx)
1744    }
1745}
1746
1747// impl Item for SplittableEditor {
1748//     type Event = EditorEvent;
1749
1750//     fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1751//         self.rhs_editor().tab_content_text(detail, cx)
1752//     }
1753
1754//     fn as_searchable(&self, _this: &Entity<Self>, cx: &App) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
1755//         Some(Box::new(self.last_selected_editor().clone()))
1756//     }
1757// }
1758
1759impl Render for SplittableEditor {
1760    fn render(
1761        &mut self,
1762        _window: &mut ui::Window,
1763        cx: &mut ui::Context<Self>,
1764    ) -> impl ui::IntoElement {
1765        let inner = if self.lhs.is_some() {
1766            let style = self.rhs_editor.read(cx).create_style(cx);
1767            SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
1768        } else {
1769            self.rhs_editor.clone().into_any_element()
1770        };
1771        div()
1772            .id("splittable-editor")
1773            .on_action(cx.listener(Self::toggle_split))
1774            .on_action(cx.listener(Self::activate_pane_left))
1775            .on_action(cx.listener(Self::activate_pane_right))
1776            .on_action(cx.listener(Self::intercept_toggle_breakpoint))
1777            .on_action(cx.listener(Self::intercept_enable_breakpoint))
1778            .on_action(cx.listener(Self::intercept_disable_breakpoint))
1779            .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
1780            .on_action(cx.listener(Self::intercept_inline_assist))
1781            .capture_action(cx.listener(Self::toggle_soft_wrap))
1782            .size_full()
1783            .child(inner)
1784    }
1785}
1786
1787impl LhsEditor {
1788    fn update_path_excerpts_from_rhs(
1789        &mut self,
1790        path_key: PathKey,
1791        rhs_multibuffer: &Entity<MultiBuffer>,
1792        diff: Entity<BufferDiff>,
1793        cx: &mut App,
1794    ) -> Vec<(ExcerptId, ExcerptId)> {
1795        let rhs_multibuffer_ref = rhs_multibuffer.read(cx);
1796        let rhs_excerpt_ids: Vec<ExcerptId> =
1797            rhs_multibuffer_ref.excerpts_for_path(&path_key).collect();
1798
1799        let Some(excerpt_id) = rhs_multibuffer_ref.excerpts_for_path(&path_key).next() else {
1800            self.multibuffer.update(cx, |multibuffer, cx| {
1801                multibuffer.remove_excerpts_for_path(path_key, cx);
1802            });
1803            return Vec::new();
1804        };
1805
1806        let rhs_multibuffer_snapshot = rhs_multibuffer_ref.snapshot(cx);
1807        let main_buffer = rhs_multibuffer_snapshot
1808            .buffer_for_excerpt(excerpt_id)
1809            .unwrap();
1810        let base_text_buffer = diff.read(cx).base_text_buffer();
1811        let diff_snapshot = diff.read(cx).snapshot(cx);
1812        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
1813        let new = rhs_multibuffer_ref
1814            .excerpts_for_buffer(main_buffer.remote_id(), cx)
1815            .into_iter()
1816            .map(|(_, excerpt_range)| {
1817                let point_range_to_base_text_point_range = |range: Range<Point>| {
1818                    let start = diff_snapshot
1819                        .buffer_point_to_base_text_range(
1820                            Point::new(range.start.row, 0),
1821                            main_buffer,
1822                        )
1823                        .start;
1824                    let end = diff_snapshot
1825                        .buffer_point_to_base_text_range(Point::new(range.end.row, 0), main_buffer)
1826                        .end;
1827                    let end_column = diff_snapshot.base_text().line_len(end.row);
1828                    Point::new(start.row, 0)..Point::new(end.row, end_column)
1829                };
1830                let rhs = excerpt_range.primary.to_point(main_buffer);
1831                let context = excerpt_range.context.to_point(main_buffer);
1832                ExcerptRange {
1833                    primary: point_range_to_base_text_point_range(rhs),
1834                    context: point_range_to_base_text_point_range(context),
1835                }
1836            })
1837            .collect();
1838
1839        self.editor.update(cx, |editor, cx| {
1840            editor.buffer().update(cx, |buffer, cx| {
1841                let (ids, _) = buffer.update_path_excerpts(
1842                    path_key.clone(),
1843                    base_text_buffer.clone(),
1844                    &base_text_buffer_snapshot,
1845                    new,
1846                    cx,
1847                );
1848                if !ids.is_empty()
1849                    && buffer
1850                        .diff_for(base_text_buffer.read(cx).remote_id())
1851                        .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1852                {
1853                    buffer.add_inverted_diff(diff, cx);
1854                }
1855            })
1856        });
1857
1858        let lhs_excerpt_ids: Vec<ExcerptId> = self
1859            .multibuffer
1860            .read(cx)
1861            .excerpts_for_path(&path_key)
1862            .collect();
1863
1864        debug_assert_eq!(rhs_excerpt_ids.len(), lhs_excerpt_ids.len());
1865
1866        lhs_excerpt_ids.into_iter().zip(rhs_excerpt_ids).collect()
1867    }
1868
1869    fn sync_path_excerpts(
1870        &mut self,
1871        path_key: PathKey,
1872        rhs_multibuffer: &Entity<MultiBuffer>,
1873        diff: Entity<BufferDiff>,
1874        rhs_display_map: &Entity<DisplayMap>,
1875        lhs_display_map: &Entity<DisplayMap>,
1876        cx: &mut App,
1877    ) {
1878        self.remove_mappings_for_path(
1879            &path_key,
1880            rhs_multibuffer,
1881            rhs_display_map,
1882            lhs_display_map,
1883            cx,
1884        );
1885
1886        let mappings =
1887            self.update_path_excerpts_from_rhs(path_key, rhs_multibuffer, diff.clone(), cx);
1888
1889        let lhs_buffer_id = diff.read(cx).base_text(cx).remote_id();
1890        let rhs_buffer_id = diff.read(cx).buffer_id;
1891
1892        if let Some(companion) = rhs_display_map.read(cx).companion().cloned() {
1893            companion.update(cx, |c, _| {
1894                for (lhs, rhs) in mappings {
1895                    c.add_excerpt_mapping(lhs, rhs);
1896                }
1897                c.add_buffer_mapping(lhs_buffer_id, rhs_buffer_id);
1898            });
1899        }
1900    }
1901
1902    fn remove_mappings_for_path(
1903        &self,
1904        path_key: &PathKey,
1905        rhs_multibuffer: &Entity<MultiBuffer>,
1906        rhs_display_map: &Entity<DisplayMap>,
1907        _lhs_display_map: &Entity<DisplayMap>,
1908        cx: &mut App,
1909    ) {
1910        let rhs_excerpt_ids: Vec<ExcerptId> = rhs_multibuffer
1911            .read(cx)
1912            .excerpts_for_path(path_key)
1913            .collect();
1914        let lhs_excerpt_ids: Vec<ExcerptId> = self
1915            .multibuffer
1916            .read(cx)
1917            .excerpts_for_path(path_key)
1918            .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}