Skip to repository content6496 lines · 200.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:31:37.827Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
split.rs
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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_text1, &mut cx);
5375 let (buffer2, diff2) = buffer_with_diff(&base_text2, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_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, ¤t_text_a, &mut cx);
6246 let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, ¤t_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, ¤t_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, ¤t_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