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