Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:28:59.041Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

split.rs

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