1use std::ops::{Bound, Range};
2
3use buffer_diff::{BufferDiff, BufferDiffSnapshot};
4use collections::HashMap;
5use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
6use gpui::{
7 Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
8};
9use language::{Buffer, Capability};
10use multi_buffer::{
11 Anchor, BufferOffset, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer,
12 MultiBufferPoint, MultiBufferSnapshot, PathKey,
13};
14use project::Project;
15use rope::Point;
16use text::{OffsetRangeExt as _, ToPoint as _};
17use ui::{
18 App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
19 Styled as _, Window, div,
20};
21
22use crate::{
23 display_map::MultiBufferRowMapping,
24 split_editor_view::{SplitEditorState, SplitEditorView},
25};
26use workspace::{
27 ActivatePaneLeft, ActivatePaneRight, Item, ItemHandle, Pane, PaneGroup, SplitDirection,
28 Workspace,
29};
30
31use crate::{
32 Autoscroll, DisplayMap, Editor, EditorEvent, ToggleCodeActions, ToggleSoftWrap,
33 actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
34 display_map::Companion,
35};
36use zed_actions::assistant::InlineAssist;
37
38pub(crate) fn convert_lhs_rows_to_rhs(
39 lhs_excerpt_to_rhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
40 rhs_snapshot: &MultiBufferSnapshot,
41 lhs_snapshot: &MultiBufferSnapshot,
42 lhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
43) -> Vec<MultiBufferRowMapping> {
44 convert_rows(
45 lhs_excerpt_to_rhs_excerpt,
46 lhs_snapshot,
47 rhs_snapshot,
48 lhs_bounds,
49 |diff, points, buffer| {
50 let (points, first_group, prev_boundary) =
51 diff.base_text_points_to_points(points, buffer);
52 (points.collect(), first_group, prev_boundary)
53 },
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<MultiBufferRowMapping> {
63 convert_rows(
64 rhs_excerpt_to_lhs_excerpt,
65 rhs_snapshot,
66 lhs_snapshot,
67 rhs_bounds,
68 |diff, points, buffer| {
69 let (points, first_group, prev_boundary) =
70 diff.points_to_base_text_points(points, buffer);
71 (points.collect(), first_group, prev_boundary)
72 },
73 )
74}
75
76fn convert_rows<F>(
77 excerpt_map: &HashMap<ExcerptId, ExcerptId>,
78 source_snapshot: &MultiBufferSnapshot,
79 target_snapshot: &MultiBufferSnapshot,
80 source_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
81 translate_fn: F,
82) -> Vec<MultiBufferRowMapping>
83where
84 F: Fn(
85 &BufferDiffSnapshot,
86 Vec<Point>,
87 &text::BufferSnapshot,
88 ) -> (
89 Vec<Range<Point>>,
90 Option<Range<Point>>,
91 Option<(Point, Range<Point>)>,
92 ),
93{
94 let mut result = Vec::new();
95
96 for (buffer, buffer_offset_range, source_excerpt_id) in
97 source_snapshot.range_to_buffer_ranges(source_bounds)
98 {
99 if let Some(translation) = convert_excerpt_rows(
100 excerpt_map,
101 source_snapshot,
102 target_snapshot,
103 source_excerpt_id,
104 buffer,
105 buffer_offset_range,
106 &translate_fn,
107 ) {
108 result.push(translation);
109 }
110 }
111
112 result
113}
114
115fn convert_excerpt_rows<F>(
116 excerpt_map: &HashMap<ExcerptId, ExcerptId>,
117 source_snapshot: &MultiBufferSnapshot,
118 target_snapshot: &MultiBufferSnapshot,
119 source_excerpt_id: ExcerptId,
120 source_buffer: &text::BufferSnapshot,
121 source_buffer_range: Range<BufferOffset>,
122 translate_fn: F,
123) -> Option<MultiBufferRowMapping>
124where
125 F: Fn(
126 &BufferDiffSnapshot,
127 Vec<Point>,
128 &text::BufferSnapshot,
129 ) -> (
130 Vec<Range<Point>>,
131 Option<Range<Point>>,
132 Option<(Point, Range<Point>)>,
133 ),
134{
135 let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied()?;
136 let target_buffer = target_snapshot.buffer_for_excerpt(target_excerpt_id)?;
137
138 let diff = source_snapshot.diff_for_buffer_id(source_buffer.remote_id())?;
139 let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() {
140 &target_buffer
141 } else {
142 source_buffer
143 };
144
145 let local_start = source_buffer.offset_to_point(source_buffer_range.start.0);
146 let local_end = source_buffer.offset_to_point(source_buffer_range.end.0);
147
148 let mut input_points: Vec<Point> = (local_start.row..=local_end.row)
149 .map(|row| Point::new(row, 0))
150 .collect();
151 if local_end.column > 0 {
152 input_points.push(local_end);
153 }
154
155 let (translated_ranges, first_group, prev_boundary) =
156 translate_fn(&diff, input_points.clone(), rhs_buffer);
157
158 let source_multibuffer_range = source_snapshot.range_for_excerpt(source_excerpt_id)?;
159 let source_excerpt_start_in_multibuffer = source_multibuffer_range.start;
160 let source_context_range = source_snapshot.context_range_for_excerpt(source_excerpt_id)?;
161 let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer);
162 let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer);
163 let target_multibuffer_range = target_snapshot.range_for_excerpt(target_excerpt_id)?;
164 let target_excerpt_start_in_multibuffer = target_multibuffer_range.start;
165 let target_context_range = target_snapshot.context_range_for_excerpt(target_excerpt_id)?;
166 let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer);
167 let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer);
168
169 let boundaries: Vec<_> = input_points
170 .into_iter()
171 .zip(translated_ranges)
172 .map(|(source_buffer_point, target_range)| {
173 let source_multibuffer_point = source_excerpt_start_in_multibuffer
174 + (source_buffer_point - source_excerpt_start_in_buffer.min(source_buffer_point));
175
176 let clamped_target_start = target_range
177 .start
178 .max(target_excerpt_start_in_buffer)
179 .min(target_excerpt_end_in_buffer);
180 let clamped_target_end = target_range
181 .end
182 .max(target_excerpt_start_in_buffer)
183 .min(target_excerpt_end_in_buffer);
184
185 let target_multibuffer_start = target_excerpt_start_in_multibuffer
186 + (clamped_target_start - target_excerpt_start_in_buffer);
187
188 let target_multibuffer_end = target_excerpt_start_in_multibuffer
189 + (clamped_target_end - target_excerpt_start_in_buffer);
190
191 (
192 source_multibuffer_point,
193 target_multibuffer_start..target_multibuffer_end,
194 )
195 })
196 .collect();
197 let first_group = first_group.map(|first_group| {
198 let start = source_excerpt_start_in_multibuffer
199 + (first_group.start - source_excerpt_start_in_buffer.min(first_group.start));
200 let end = source_excerpt_start_in_multibuffer
201 + (first_group.end - source_excerpt_start_in_buffer.min(first_group.end));
202 start..end
203 });
204
205 let prev_boundary = prev_boundary.map(|(source_buffer_point, target_range)| {
206 let source_multibuffer_point = source_excerpt_start_in_multibuffer
207 + (source_buffer_point - source_excerpt_start_in_buffer.min(source_buffer_point));
208
209 let clamped_target_start = target_range
210 .start
211 .max(target_excerpt_start_in_buffer)
212 .min(target_excerpt_end_in_buffer);
213 let clamped_target_end = target_range
214 .end
215 .max(target_excerpt_start_in_buffer)
216 .min(target_excerpt_end_in_buffer);
217
218 let target_multibuffer_start = target_excerpt_start_in_multibuffer
219 + (clamped_target_start - target_excerpt_start_in_buffer);
220 let target_multibuffer_end = target_excerpt_start_in_multibuffer
221 + (clamped_target_end - target_excerpt_start_in_buffer);
222
223 (
224 source_multibuffer_point,
225 target_multibuffer_start..target_multibuffer_end,
226 )
227 });
228
229 Some(MultiBufferRowMapping {
230 boundaries,
231 first_group,
232 prev_boundary,
233 source_excerpt_end: source_excerpt_start_in_multibuffer
234 + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer),
235 target_excerpt_end: target_excerpt_start_in_multibuffer
236 + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer),
237 })
238}
239
240struct SplitDiffFeatureFlag;
241
242impl FeatureFlag for SplitDiffFeatureFlag {
243 const NAME: &'static str = "split-diff";
244
245 fn enabled_for_staff() -> bool {
246 true
247 }
248}
249
250#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
251#[action(namespace = editor)]
252struct SplitDiff;
253
254#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
255#[action(namespace = editor)]
256struct UnsplitDiff;
257
258#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
259#[action(namespace = editor)]
260pub struct ToggleSplitDiff;
261
262#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
263#[action(namespace = editor)]
264struct JumpToCorrespondingRow;
265
266/// When locked cursors mode is enabled, cursor movements in one editor will
267/// update the cursor position in the other editor to the corresponding row.
268#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
269#[action(namespace = editor)]
270pub struct ToggleLockedCursors;
271
272pub struct SplittableEditor {
273 primary_multibuffer: Entity<MultiBuffer>,
274 primary_editor: Entity<Editor>,
275 secondary: Option<SecondaryEditor>,
276 panes: PaneGroup,
277 workspace: WeakEntity<Workspace>,
278 split_state: Entity<SplitEditorState>,
279 locked_cursors: bool,
280 _subscriptions: Vec<Subscription>,
281}
282
283struct SecondaryEditor {
284 multibuffer: Entity<MultiBuffer>,
285 editor: Entity<Editor>,
286 pane: Entity<Pane>,
287 has_latest_selection: bool,
288 _subscriptions: Vec<Subscription>,
289}
290
291impl SplittableEditor {
292 pub fn primary_editor(&self) -> &Entity<Editor> {
293 &self.primary_editor
294 }
295
296 pub fn secondary_editor(&self) -> Option<&Entity<Editor>> {
297 self.secondary.as_ref().map(|s| &s.editor)
298 }
299
300 pub fn is_split(&self) -> bool {
301 self.secondary.is_some()
302 }
303
304 pub fn last_selected_editor(&self) -> &Entity<Editor> {
305 if let Some(secondary) = &self.secondary
306 && secondary.has_latest_selection
307 {
308 &secondary.editor
309 } else {
310 &self.primary_editor
311 }
312 }
313
314 pub fn new_unsplit(
315 primary_multibuffer: Entity<MultiBuffer>,
316 project: Entity<Project>,
317 workspace: Entity<Workspace>,
318 window: &mut Window,
319 cx: &mut Context<Self>,
320 ) -> Self {
321 let primary_editor = cx.new(|cx| {
322 let mut editor = Editor::for_multibuffer(
323 primary_multibuffer.clone(),
324 Some(project.clone()),
325 window,
326 cx,
327 );
328 editor.set_expand_all_diff_hunks(cx);
329 editor
330 });
331 let pane = cx.new(|cx| {
332 let mut pane = Pane::new(
333 workspace.downgrade(),
334 project,
335 Default::default(),
336 None,
337 NoAction.boxed_clone(),
338 true,
339 window,
340 cx,
341 );
342 pane.set_should_display_tab_bar(|_, _| false);
343 pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
344 pane
345 });
346 let panes = PaneGroup::new(pane);
347 // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
348 let subscriptions = vec![cx.subscribe(
349 &primary_editor,
350 |this, _, event: &EditorEvent, cx| match event {
351 EditorEvent::ExpandExcerptsRequested {
352 excerpt_ids,
353 lines,
354 direction,
355 } => {
356 this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
357 }
358 EditorEvent::SelectionsChanged { .. } => {
359 if let Some(secondary) = &mut this.secondary {
360 secondary.has_latest_selection = false;
361 }
362 cx.emit(event.clone());
363 }
364 _ => cx.emit(event.clone()),
365 },
366 )];
367
368 window.defer(cx, {
369 let workspace = workspace.downgrade();
370 let primary_editor = primary_editor.downgrade();
371 move |window, cx| {
372 workspace
373 .update(cx, |workspace, cx| {
374 primary_editor.update(cx, |editor, cx| {
375 editor.added_to_workspace(workspace, window, cx);
376 })
377 })
378 .ok();
379 }
380 });
381 let split_state = cx.new(|cx| SplitEditorState::new(cx));
382 Self {
383 primary_editor,
384 primary_multibuffer,
385 secondary: None,
386 panes,
387 workspace: workspace.downgrade(),
388 split_state,
389 locked_cursors: false,
390 _subscriptions: subscriptions,
391 }
392 }
393
394 fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
395 if !cx.has_flag::<SplitDiffFeatureFlag>() {
396 return;
397 }
398 if self.secondary.is_some() {
399 return;
400 }
401 let Some(workspace) = self.workspace.upgrade() else {
402 return;
403 };
404 let project = workspace.read(cx).project().clone();
405
406 let secondary_multibuffer = cx.new(|cx| {
407 let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
408 multibuffer.set_all_diff_hunks_expanded(cx);
409 multibuffer
410 });
411 let secondary_editor = cx.new(|cx| {
412 let mut editor = Editor::for_multibuffer(
413 secondary_multibuffer.clone(),
414 Some(project.clone()),
415 window,
416 cx,
417 );
418 editor.number_deleted_lines = true;
419 editor.set_delegate_expand_excerpts(true);
420 editor
421 });
422 let secondary_pane = cx.new(|cx| {
423 let mut pane = Pane::new(
424 workspace.downgrade(),
425 workspace.read(cx).project().clone(),
426 Default::default(),
427 None,
428 NoAction.boxed_clone(),
429 true,
430 window,
431 cx,
432 );
433 pane.set_should_display_tab_bar(|_, _| false);
434 pane.add_item(
435 ItemHandle::boxed_clone(&secondary_editor),
436 false,
437 false,
438 None,
439 window,
440 cx,
441 );
442 pane
443 });
444
445 let subscriptions = vec![cx.subscribe(
446 &secondary_editor,
447 |this, _, event: &EditorEvent, cx| match event {
448 EditorEvent::ExpandExcerptsRequested {
449 excerpt_ids,
450 lines,
451 direction,
452 } => {
453 if this.secondary.is_some() {
454 let primary_display_map = this.primary_editor.read(cx).display_map.read(cx);
455 let primary_ids: Vec<_> = excerpt_ids
456 .iter()
457 .filter_map(|id| {
458 primary_display_map.companion_excerpt_to_my_excerpt(*id, cx)
459 })
460 .collect();
461 this.expand_excerpts(primary_ids.into_iter(), *lines, *direction, cx);
462 }
463 }
464 EditorEvent::SelectionsChanged { .. } => {
465 if let Some(secondary) = &mut this.secondary {
466 secondary.has_latest_selection = true;
467 }
468 cx.emit(event.clone());
469 }
470 _ => cx.emit(event.clone()),
471 },
472 )];
473 let mut secondary = SecondaryEditor {
474 editor: secondary_editor,
475 multibuffer: secondary_multibuffer,
476 pane: secondary_pane.clone(),
477 has_latest_selection: false,
478 _subscriptions: subscriptions,
479 };
480 let primary_display_map = self.primary_editor.read(cx).display_map.clone();
481 let secondary_display_map = secondary.editor.read(cx).display_map.clone();
482 let rhs_display_map_id = primary_display_map.entity_id();
483
484 self.primary_editor.update(cx, |editor, cx| {
485 editor.set_delegate_expand_excerpts(true);
486 editor.buffer().update(cx, |primary_multibuffer, cx| {
487 primary_multibuffer.set_show_deleted_hunks(false, cx);
488 primary_multibuffer.set_use_extended_diff_range(true, cx);
489 })
490 });
491
492 let path_diffs: Vec<_> = {
493 let primary_multibuffer = self.primary_multibuffer.read(cx);
494 primary_multibuffer
495 .paths()
496 .filter_map(|path| {
497 let excerpt_id = primary_multibuffer.excerpts_for_path(path).next()?;
498 let snapshot = primary_multibuffer.snapshot(cx);
499 let buffer = snapshot.buffer_for_excerpt(excerpt_id)?;
500 let diff = primary_multibuffer.diff_for(buffer.remote_id())?;
501 Some((path.clone(), diff))
502 })
503 .collect()
504 };
505
506 let mut companion = Companion::new(
507 rhs_display_map_id,
508 convert_rhs_rows_to_lhs,
509 convert_lhs_rows_to_rhs,
510 );
511
512 for (path, diff) in path_diffs {
513 for (lhs, rhs) in secondary.update_path_excerpts_from_primary(
514 path,
515 &self.primary_multibuffer,
516 diff.clone(),
517 cx,
518 ) {
519 companion.add_excerpt_mapping(lhs, rhs);
520 }
521 companion.add_buffer_mapping(
522 diff.read(cx).base_text(cx).remote_id(),
523 diff.read(cx).buffer_id,
524 );
525 }
526
527 let companion = cx.new(|_| companion);
528
529 primary_display_map.update(cx, |dm, cx| {
530 dm.set_companion(
531 Some((secondary_display_map.downgrade(), companion.clone())),
532 cx,
533 );
534 });
535 secondary_display_map.update(cx, |dm, cx| {
536 dm.set_companion(Some((primary_display_map.downgrade(), companion)), cx);
537 });
538
539 let primary_weak = self.primary_editor.downgrade();
540 let secondary_weak = secondary.editor.downgrade();
541
542 let this = cx.entity().downgrade();
543 self.primary_editor.update(cx, |editor, _cx| {
544 editor.set_scroll_companion(Some(secondary_weak));
545 let this = this.clone();
546 editor.set_on_local_selections_changed(Some(Box::new(
547 move |cursor_position, window, cx| {
548 let this = this.clone();
549 window.defer(cx, move |window, cx| {
550 this.update(cx, |this, cx| {
551 if this.locked_cursors {
552 this.sync_cursor_to_other_side(true, cursor_position, window, cx);
553 }
554 })
555 .ok();
556 })
557 },
558 )));
559 });
560 secondary.editor.update(cx, |editor, _cx| {
561 editor.set_scroll_companion(Some(primary_weak));
562 let this = this.clone();
563 editor.set_on_local_selections_changed(Some(Box::new(
564 move |cursor_position, window, cx| {
565 let this = this.clone();
566 window.defer(cx, move |window, cx| {
567 this.update(cx, |this, cx| {
568 if this.locked_cursors {
569 this.sync_cursor_to_other_side(false, cursor_position, window, cx);
570 }
571 })
572 .ok();
573 })
574 },
575 )));
576 });
577
578 let primary_scroll_position = self
579 .primary_editor
580 .update(cx, |editor, cx| editor.scroll_position(cx));
581 secondary.editor.update(cx, |editor, cx| {
582 editor.set_scroll_position_internal(primary_scroll_position, false, false, window, cx);
583 });
584
585 // Copy soft wrap state from primary (source of truth) to secondary
586 let primary_soft_wrap_override = self.primary_editor.read(cx).soft_wrap_mode_override;
587 secondary.editor.update(cx, |editor, cx| {
588 editor.soft_wrap_mode_override = primary_soft_wrap_override;
589 cx.notify();
590 });
591
592 self.secondary = Some(secondary);
593
594 let primary_pane = self.panes.first_pane();
595 self.panes
596 .split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
597 .unwrap();
598 cx.notify();
599 }
600
601 fn activate_pane_left(
602 &mut self,
603 _: &ActivatePaneLeft,
604 window: &mut Window,
605 cx: &mut Context<Self>,
606 ) {
607 if let Some(secondary) = &mut self.secondary {
608 if !secondary.has_latest_selection {
609 secondary.editor.read(cx).focus_handle(cx).focus(window, cx);
610 secondary.editor.update(cx, |editor, cx| {
611 editor.request_autoscroll(Autoscroll::fit(), cx);
612 });
613 secondary.has_latest_selection = true;
614 cx.notify();
615 } else {
616 cx.propagate();
617 }
618 } else {
619 cx.propagate();
620 }
621 }
622
623 fn activate_pane_right(
624 &mut self,
625 _: &ActivatePaneRight,
626 window: &mut Window,
627 cx: &mut Context<Self>,
628 ) {
629 if let Some(secondary) = &mut self.secondary {
630 if secondary.has_latest_selection {
631 self.primary_editor
632 .read(cx)
633 .focus_handle(cx)
634 .focus(window, cx);
635 self.primary_editor.update(cx, |editor, cx| {
636 editor.request_autoscroll(Autoscroll::fit(), cx);
637 });
638 secondary.has_latest_selection = false;
639 cx.notify();
640 } else {
641 cx.propagate();
642 }
643 } else {
644 cx.propagate();
645 }
646 }
647
648 fn toggle_locked_cursors(
649 &mut self,
650 _: &ToggleLockedCursors,
651 _window: &mut Window,
652 cx: &mut Context<Self>,
653 ) {
654 self.locked_cursors = !self.locked_cursors;
655 cx.notify();
656 }
657
658 pub fn locked_cursors(&self) -> bool {
659 self.locked_cursors
660 }
661
662 fn sync_cursor_to_other_side(
663 &mut self,
664 from_primary: bool,
665 source_point: Point,
666 window: &mut Window,
667 cx: &mut Context<Self>,
668 ) {
669 let Some(secondary) = &self.secondary else {
670 return;
671 };
672
673 let target_editor = if from_primary {
674 &secondary.editor
675 } else {
676 &self.primary_editor
677 };
678
679 let (source_multibuffer, target_multibuffer) = if from_primary {
680 (&self.primary_multibuffer, &secondary.multibuffer)
681 } else {
682 (&secondary.multibuffer, &self.primary_multibuffer)
683 };
684
685 let source_snapshot = source_multibuffer.read(cx).snapshot(cx);
686 let target_snapshot = target_multibuffer.read(cx).snapshot(cx);
687
688 let target_point = target_editor.update(cx, |target_editor, cx| {
689 target_editor.display_map.update(cx, |display_map, cx| {
690 let display_map_id = cx.entity_id();
691 display_map.companion().unwrap().update(cx, |companion, _| {
692 companion
693 .convert_rows_from_companion(
694 display_map_id,
695 &target_snapshot,
696 &source_snapshot,
697 (Bound::Included(source_point), Bound::Included(source_point)),
698 )
699 .first()
700 .unwrap()
701 .boundaries
702 .first()
703 .unwrap()
704 .1
705 .start
706 })
707 })
708 });
709
710 target_editor.update(cx, |editor, cx| {
711 editor.set_suppress_selection_callback(true);
712 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
713 s.select_ranges([target_point..target_point]);
714 });
715 editor.set_suppress_selection_callback(false);
716 });
717 }
718
719 fn toggle_split(&mut self, _: &ToggleSplitDiff, window: &mut Window, cx: &mut Context<Self>) {
720 if self.secondary.is_some() {
721 self.unsplit(&UnsplitDiff, window, cx);
722 } else {
723 self.split(&SplitDiff, window, cx);
724 }
725 }
726
727 fn intercept_toggle_code_actions(
728 &mut self,
729 _: &ToggleCodeActions,
730 _window: &mut Window,
731 cx: &mut Context<Self>,
732 ) {
733 if self.secondary.is_some() {
734 cx.stop_propagation();
735 } else {
736 cx.propagate();
737 }
738 }
739
740 fn intercept_toggle_breakpoint(
741 &mut self,
742 _: &ToggleBreakpoint,
743 _window: &mut Window,
744 cx: &mut Context<Self>,
745 ) {
746 // Only block breakpoint actions when the left (secondary) editor has focus
747 if let Some(secondary) = &self.secondary {
748 if secondary.has_latest_selection {
749 cx.stop_propagation();
750 } else {
751 cx.propagate();
752 }
753 } else {
754 cx.propagate();
755 }
756 }
757
758 fn intercept_enable_breakpoint(
759 &mut self,
760 _: &EnableBreakpoint,
761 _window: &mut Window,
762 cx: &mut Context<Self>,
763 ) {
764 // Only block breakpoint actions when the left (secondary) editor has focus
765 if let Some(secondary) = &self.secondary {
766 if secondary.has_latest_selection {
767 cx.stop_propagation();
768 } else {
769 cx.propagate();
770 }
771 } else {
772 cx.propagate();
773 }
774 }
775
776 fn intercept_disable_breakpoint(
777 &mut self,
778 _: &DisableBreakpoint,
779 _window: &mut Window,
780 cx: &mut Context<Self>,
781 ) {
782 // Only block breakpoint actions when the left (secondary) editor has focus
783 if let Some(secondary) = &self.secondary {
784 if secondary.has_latest_selection {
785 cx.stop_propagation();
786 } else {
787 cx.propagate();
788 }
789 } else {
790 cx.propagate();
791 }
792 }
793
794 fn intercept_edit_log_breakpoint(
795 &mut self,
796 _: &EditLogBreakpoint,
797 _window: &mut Window,
798 cx: &mut Context<Self>,
799 ) {
800 // Only block breakpoint actions when the left (secondary) editor has focus
801 if let Some(secondary) = &self.secondary {
802 if secondary.has_latest_selection {
803 cx.stop_propagation();
804 } else {
805 cx.propagate();
806 }
807 } else {
808 cx.propagate();
809 }
810 }
811
812 fn intercept_inline_assist(
813 &mut self,
814 _: &InlineAssist,
815 _window: &mut Window,
816 cx: &mut Context<Self>,
817 ) {
818 if self.secondary.is_some() {
819 cx.stop_propagation();
820 } else {
821 cx.propagate();
822 }
823 }
824
825 fn toggle_soft_wrap(
826 &mut self,
827 _: &ToggleSoftWrap,
828 window: &mut Window,
829 cx: &mut Context<Self>,
830 ) {
831 if let Some(secondary) = &self.secondary {
832 cx.stop_propagation();
833
834 let is_secondary_focused = secondary.has_latest_selection;
835 let (focused_editor, other_editor) = if is_secondary_focused {
836 (&secondary.editor, &self.primary_editor)
837 } else {
838 (&self.primary_editor, &secondary.editor)
839 };
840
841 // Toggle the focused editor
842 focused_editor.update(cx, |editor, cx| {
843 editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
844 });
845
846 // Copy the soft wrap state from the focused editor to the other editor
847 let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
848 other_editor.update(cx, |editor, cx| {
849 editor.soft_wrap_mode_override = soft_wrap_override;
850 cx.notify();
851 });
852 } else {
853 cx.propagate();
854 }
855 }
856
857 fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
858 let Some(secondary) = self.secondary.take() else {
859 return;
860 };
861 self.panes.remove(&secondary.pane, cx).unwrap();
862 self.primary_editor.update(cx, |primary, cx| {
863 primary.set_on_local_selections_changed(None);
864 primary.set_scroll_companion(None);
865 primary.set_delegate_expand_excerpts(false);
866 primary.buffer().update(cx, |buffer, cx| {
867 buffer.set_show_deleted_hunks(true, cx);
868 buffer.set_use_extended_diff_range(false, cx);
869 });
870 primary.display_map.update(cx, |dm, cx| {
871 dm.set_companion(None, cx);
872 });
873 });
874 secondary.editor.update(cx, |editor, _cx| {
875 editor.set_on_local_selections_changed(None);
876 editor.set_scroll_companion(None);
877 });
878 cx.notify();
879 }
880
881 pub fn added_to_workspace(
882 &mut self,
883 workspace: &mut Workspace,
884 window: &mut Window,
885 cx: &mut Context<Self>,
886 ) {
887 self.workspace = workspace.weak_handle();
888 self.primary_editor.update(cx, |primary_editor, cx| {
889 primary_editor.added_to_workspace(workspace, window, cx);
890 });
891 if let Some(secondary) = &self.secondary {
892 secondary.editor.update(cx, |secondary_editor, cx| {
893 secondary_editor.added_to_workspace(workspace, window, cx);
894 });
895 }
896 }
897
898 pub fn set_excerpts_for_path(
899 &mut self,
900 path: PathKey,
901 buffer: Entity<Buffer>,
902 ranges: impl IntoIterator<Item = Range<Point>> + Clone,
903 context_line_count: u32,
904 diff: Entity<BufferDiff>,
905 cx: &mut Context<Self>,
906 ) -> (Vec<Range<Anchor>>, bool) {
907 let primary_display_map = self.primary_editor.read(cx).display_map.clone();
908 let secondary_display_map = self
909 .secondary
910 .as_ref()
911 .map(|s| s.editor.read(cx).display_map.clone());
912
913 let (anchors, added_a_new_excerpt) =
914 self.primary_multibuffer
915 .update(cx, |primary_multibuffer, cx| {
916 let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
917 path.clone(),
918 buffer.clone(),
919 ranges,
920 context_line_count,
921 cx,
922 );
923 if !anchors.is_empty()
924 && primary_multibuffer
925 .diff_for(buffer.read(cx).remote_id())
926 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
927 {
928 primary_multibuffer.add_diff(diff.clone(), cx);
929 }
930 (anchors, added_a_new_excerpt)
931 });
932
933 if let Some(secondary) = &mut self.secondary {
934 if let Some(secondary_display_map) = &secondary_display_map {
935 secondary.sync_path_excerpts(
936 path,
937 &self.primary_multibuffer,
938 diff,
939 &primary_display_map,
940 secondary_display_map,
941 cx,
942 );
943 }
944 }
945
946 (anchors, added_a_new_excerpt)
947 }
948
949 fn expand_excerpts(
950 &mut self,
951 excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
952 lines: u32,
953 direction: ExpandExcerptDirection,
954 cx: &mut Context<Self>,
955 ) {
956 let mut corresponding_paths = HashMap::default();
957 self.primary_multibuffer.update(cx, |multibuffer, cx| {
958 let snapshot = multibuffer.snapshot(cx);
959 if self.secondary.is_some() {
960 corresponding_paths = excerpt_ids
961 .clone()
962 .map(|excerpt_id| {
963 let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
964 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
965 let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
966 (path, diff)
967 })
968 .collect::<HashMap<_, _>>();
969 }
970 multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
971 });
972
973 if let Some(secondary) = &mut self.secondary {
974 let primary_display_map = self.primary_editor.read(cx).display_map.clone();
975 let secondary_display_map = secondary.editor.read(cx).display_map.clone();
976 for (path, diff) in corresponding_paths {
977 secondary.sync_path_excerpts(
978 path,
979 &self.primary_multibuffer,
980 diff,
981 &primary_display_map,
982 &secondary_display_map,
983 cx,
984 );
985 }
986 }
987 }
988
989 pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
990 self.primary_multibuffer.update(cx, |buffer, cx| {
991 buffer.remove_excerpts_for_path(path.clone(), cx)
992 });
993 if let Some(secondary) = &self.secondary {
994 let primary_display_map = self.primary_editor.read(cx).display_map.clone();
995 let secondary_display_map = secondary.editor.read(cx).display_map.clone();
996 secondary.remove_mappings_for_path(
997 &path,
998 &self.primary_multibuffer,
999 &primary_display_map,
1000 &secondary_display_map,
1001 cx,
1002 );
1003 secondary
1004 .multibuffer
1005 .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
1006 }
1007 }
1008}
1009
1010#[cfg(test)]
1011impl SplittableEditor {
1012 fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1013 use multi_buffer::MultiBufferRow;
1014 use text::Bias;
1015
1016 use crate::display_map::Block;
1017 use crate::display_map::DisplayRow;
1018
1019 self.debug_print(cx);
1020
1021 let secondary = self.secondary.as_ref().unwrap();
1022 let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
1023 let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
1024 assert_eq!(
1025 secondary_excerpts.len(),
1026 primary_excerpts.len(),
1027 "mismatch in excerpt count"
1028 );
1029
1030 if quiesced {
1031 let rhs_snapshot = secondary
1032 .editor
1033 .update(cx, |editor, cx| editor.display_snapshot(cx));
1034 let lhs_snapshot = self
1035 .primary_editor
1036 .update(cx, |editor, cx| editor.display_snapshot(cx));
1037
1038 let lhs_max_row = lhs_snapshot.max_point().row();
1039 let rhs_max_row = rhs_snapshot.max_point().row();
1040 assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1041
1042 let lhs_excerpt_block_rows = lhs_snapshot
1043 .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1044 .filter(|(_, block)| {
1045 matches!(
1046 block,
1047 Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1048 )
1049 })
1050 .map(|(row, _)| row)
1051 .collect::<Vec<_>>();
1052 let rhs_excerpt_block_rows = rhs_snapshot
1053 .blocks_in_range(DisplayRow(0)..rhs_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 assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1063
1064 for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1065 assert_eq!(
1066 lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1067 "mismatch in hunks"
1068 );
1069 assert_eq!(
1070 lhs_hunk.status, rhs_hunk.status,
1071 "mismatch in hunk statuses"
1072 );
1073
1074 let (lhs_point, rhs_point) =
1075 if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1076 (
1077 Point::new(lhs_hunk.row_range.end.0, 0),
1078 Point::new(rhs_hunk.row_range.end.0, 0),
1079 )
1080 } else {
1081 (
1082 Point::new(lhs_hunk.row_range.start.0, 0),
1083 Point::new(rhs_hunk.row_range.start.0, 0),
1084 )
1085 };
1086 let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1087 let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1088 assert_eq!(
1089 lhs_point.row(),
1090 rhs_point.row(),
1091 "mismatch in hunk position"
1092 );
1093 }
1094
1095 // Filtering out empty lines is a bit of a hack, to work around a case where
1096 // the base text has a trailing newline but the current text doesn't, or vice versa.
1097 // In this case, we get the additional newline on one side, but that line is not
1098 // marked as added/deleted by rowinfos.
1099 self.check_sides_match(cx, |snapshot| {
1100 snapshot
1101 .buffer_snapshot()
1102 .text()
1103 .split("\n")
1104 .zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
1105 .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
1106 .map(|(line, _)| line.to_owned())
1107 .collect::<Vec<_>>()
1108 });
1109 }
1110 }
1111
1112 #[track_caller]
1113 fn check_sides_match<T: std::fmt::Debug + PartialEq>(
1114 &self,
1115 cx: &mut App,
1116 mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
1117 ) {
1118 let secondary = self.secondary.as_ref().expect("requires split");
1119 let primary_snapshot = self.primary_editor.update(cx, |editor, cx| {
1120 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1121 });
1122 let secondary_snapshot = secondary.editor.update(cx, |editor, cx| {
1123 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1124 });
1125
1126 let primary_t = extract(&primary_snapshot);
1127 let secondary_t = extract(&secondary_snapshot);
1128
1129 if primary_t != secondary_t {
1130 self.debug_print(cx);
1131 pretty_assertions::assert_eq!(primary_t, secondary_t);
1132 }
1133 }
1134
1135 fn debug_print(&self, cx: &mut App) {
1136 use crate::DisplayRow;
1137 use crate::display_map::Block;
1138 use buffer_diff::DiffHunkStatusKind;
1139
1140 assert!(
1141 self.secondary.is_some(),
1142 "debug_print is only useful when secondary editor exists"
1143 );
1144
1145 let secondary = self.secondary.as_ref().unwrap();
1146
1147 // Get terminal width, default to 80 if unavailable
1148 let terminal_width = std::env::var("COLUMNS")
1149 .ok()
1150 .and_then(|s| s.parse::<usize>().ok())
1151 .unwrap_or(80);
1152
1153 // Each side gets half the terminal width minus the separator
1154 let separator = " │ ";
1155 let side_width = (terminal_width - separator.len()) / 2;
1156
1157 // Get display snapshots for both editors
1158 let secondary_snapshot = secondary.editor.update(cx, |editor, cx| {
1159 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1160 });
1161 let primary_snapshot = self.primary_editor.update(cx, |editor, cx| {
1162 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1163 });
1164
1165 let secondary_max_row = secondary_snapshot.max_point().row().0;
1166 let primary_max_row = primary_snapshot.max_point().row().0;
1167 let max_row = secondary_max_row.max(primary_max_row);
1168
1169 // Build a map from display row -> block type string
1170 // Each row of a multi-row block gets an entry with the same block type
1171 // For spacers, the ID is included in brackets
1172 fn build_block_map(
1173 snapshot: &crate::DisplaySnapshot,
1174 max_row: u32,
1175 ) -> std::collections::HashMap<u32, String> {
1176 let mut block_map = std::collections::HashMap::new();
1177 for (start_row, block) in
1178 snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1179 {
1180 let (block_type, height) = match block {
1181 Block::Spacer {
1182 id,
1183 height,
1184 is_below: _,
1185 } => (format!("SPACER[{}]", id.0), *height),
1186 Block::ExcerptBoundary { height, .. } => {
1187 ("EXCERPT_BOUNDARY".to_string(), *height)
1188 }
1189 Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1190 Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1191 Block::Custom(custom) => {
1192 ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1193 }
1194 };
1195 for offset in 0..height {
1196 block_map.insert(start_row.0 + offset, block_type.clone());
1197 }
1198 }
1199 block_map
1200 }
1201
1202 let secondary_blocks = build_block_map(&secondary_snapshot, secondary_max_row);
1203 let primary_blocks = build_block_map(&primary_snapshot, primary_max_row);
1204
1205 fn display_width(s: &str) -> usize {
1206 unicode_width::UnicodeWidthStr::width(s)
1207 }
1208
1209 fn truncate_line(line: &str, max_width: usize) -> String {
1210 let line_width = display_width(line);
1211 if line_width <= max_width {
1212 return line.to_string();
1213 }
1214 if max_width < 9 {
1215 let mut result = String::new();
1216 let mut width = 0;
1217 for c in line.chars() {
1218 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1219 if width + c_width > max_width {
1220 break;
1221 }
1222 result.push(c);
1223 width += c_width;
1224 }
1225 return result;
1226 }
1227 let ellipsis = "...";
1228 let target_prefix_width = 3;
1229 let target_suffix_width = 3;
1230
1231 let mut prefix = String::new();
1232 let mut prefix_width = 0;
1233 for c in line.chars() {
1234 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1235 if prefix_width + c_width > target_prefix_width {
1236 break;
1237 }
1238 prefix.push(c);
1239 prefix_width += c_width;
1240 }
1241
1242 let mut suffix_chars: Vec<char> = Vec::new();
1243 let mut suffix_width = 0;
1244 for c in line.chars().rev() {
1245 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1246 if suffix_width + c_width > target_suffix_width {
1247 break;
1248 }
1249 suffix_chars.push(c);
1250 suffix_width += c_width;
1251 }
1252 suffix_chars.reverse();
1253 let suffix: String = suffix_chars.into_iter().collect();
1254
1255 format!("{}{}{}", prefix, ellipsis, suffix)
1256 }
1257
1258 fn pad_to_width(s: &str, target_width: usize) -> String {
1259 let current_width = display_width(s);
1260 if current_width >= target_width {
1261 s.to_string()
1262 } else {
1263 format!("{}{}", s, " ".repeat(target_width - current_width))
1264 }
1265 }
1266
1267 // Helper to format a single row for one side
1268 // Format: "ln# diff bytes(cumul) text" or block info
1269 // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1270 fn format_row(
1271 row: u32,
1272 max_row: u32,
1273 snapshot: &crate::DisplaySnapshot,
1274 blocks: &std::collections::HashMap<u32, String>,
1275 row_infos: &[multi_buffer::RowInfo],
1276 cumulative_bytes: &[usize],
1277 side_width: usize,
1278 ) -> String {
1279 // Get row info if available
1280 let row_info = row_infos.get(row as usize);
1281
1282 // Line number prefix (3 chars + space)
1283 // Use buffer_row from RowInfo, which is None for block rows
1284 let line_prefix = if row > max_row {
1285 " ".to_string()
1286 } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1287 format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1288 } else {
1289 " ".to_string() // block rows have no line number
1290 };
1291 let content_width = side_width.saturating_sub(line_prefix.len());
1292
1293 if row > max_row {
1294 return format!("{}{}", line_prefix, " ".repeat(content_width));
1295 }
1296
1297 // Check if this row is a block row
1298 if let Some(block_type) = blocks.get(&row) {
1299 let block_str = format!("~~~[{}]~~~", block_type);
1300 let formatted = format!("{:^width$}", block_str, width = content_width);
1301 return format!(
1302 "{}{}",
1303 line_prefix,
1304 truncate_line(&formatted, content_width)
1305 );
1306 }
1307
1308 // Get line text
1309 let line_text = snapshot.line(DisplayRow(row));
1310 let line_bytes = line_text.len();
1311
1312 // Diff status marker
1313 let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1314 Some(status) => match status.kind {
1315 DiffHunkStatusKind::Added => "+",
1316 DiffHunkStatusKind::Deleted => "-",
1317 DiffHunkStatusKind::Modified => "~",
1318 },
1319 None => " ",
1320 };
1321
1322 // Cumulative bytes
1323 let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1324
1325 // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1326 let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1327 let text_width = content_width.saturating_sub(info_prefix.len());
1328 let truncated_text = truncate_line(&line_text, text_width);
1329
1330 let text_part = pad_to_width(&truncated_text, text_width);
1331 format!("{}{}{}", line_prefix, info_prefix, text_part)
1332 }
1333
1334 // Collect row infos for both sides
1335 let secondary_row_infos: Vec<_> = secondary_snapshot
1336 .row_infos(DisplayRow(0))
1337 .take((secondary_max_row + 1) as usize)
1338 .collect();
1339 let primary_row_infos: Vec<_> = primary_snapshot
1340 .row_infos(DisplayRow(0))
1341 .take((primary_max_row + 1) as usize)
1342 .collect();
1343
1344 // Calculate cumulative bytes for each side (only counting non-block rows)
1345 let mut secondary_cumulative = Vec::with_capacity((secondary_max_row + 1) as usize);
1346 let mut cumulative = 0usize;
1347 for row in 0..=secondary_max_row {
1348 if !secondary_blocks.contains_key(&row) {
1349 cumulative += secondary_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1350 }
1351 secondary_cumulative.push(cumulative);
1352 }
1353
1354 let mut primary_cumulative = Vec::with_capacity((primary_max_row + 1) as usize);
1355 cumulative = 0;
1356 for row in 0..=primary_max_row {
1357 if !primary_blocks.contains_key(&row) {
1358 cumulative += primary_snapshot.line(DisplayRow(row)).len() + 1;
1359 }
1360 primary_cumulative.push(cumulative);
1361 }
1362
1363 // Print header
1364 eprintln!();
1365 eprintln!("{}", "═".repeat(terminal_width));
1366 let header_left = format!("{:^width$}", "SECONDARY (LEFT)", width = side_width);
1367 let header_right = format!("{:^width$}", "PRIMARY (RIGHT)", width = side_width);
1368 eprintln!("{}{}{}", header_left, separator, header_right);
1369 eprintln!(
1370 "{:^width$}{}{:^width$}",
1371 "ln# diff len(cum) text",
1372 separator,
1373 "ln# diff len(cum) text",
1374 width = side_width
1375 );
1376 eprintln!("{}", "─".repeat(terminal_width));
1377
1378 // Print each row
1379 for row in 0..=max_row {
1380 let left = format_row(
1381 row,
1382 secondary_max_row,
1383 &secondary_snapshot,
1384 &secondary_blocks,
1385 &secondary_row_infos,
1386 &secondary_cumulative,
1387 side_width,
1388 );
1389 let right = format_row(
1390 row,
1391 primary_max_row,
1392 &primary_snapshot,
1393 &primary_blocks,
1394 &primary_row_infos,
1395 &primary_cumulative,
1396 side_width,
1397 );
1398 eprintln!("{}{}{}", left, separator, right);
1399 }
1400
1401 eprintln!("{}", "═".repeat(terminal_width));
1402 eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1403 eprintln!();
1404 }
1405
1406 fn randomly_edit_excerpts(
1407 &mut self,
1408 rng: &mut impl rand::Rng,
1409 mutation_count: usize,
1410 cx: &mut Context<Self>,
1411 ) {
1412 use collections::HashSet;
1413 use rand::prelude::*;
1414 use std::env;
1415 use util::RandomCharIter;
1416
1417 let max_buffers = env::var("MAX_BUFFERS")
1418 .map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
1419 .unwrap_or(4);
1420
1421 for _ in 0..mutation_count {
1422 let paths = self
1423 .primary_multibuffer
1424 .read(cx)
1425 .paths()
1426 .cloned()
1427 .collect::<Vec<_>>();
1428 let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
1429
1430 if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
1431 let mut excerpts = HashSet::default();
1432 for _ in 0..rng.random_range(0..excerpt_ids.len()) {
1433 excerpts.extend(excerpt_ids.choose(rng).copied());
1434 }
1435
1436 let line_count = rng.random_range(1..5);
1437
1438 log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
1439
1440 self.expand_excerpts(
1441 excerpts.iter().cloned(),
1442 line_count,
1443 ExpandExcerptDirection::UpAndDown,
1444 cx,
1445 );
1446 continue;
1447 }
1448
1449 if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
1450 let len = rng.random_range(100..500);
1451 let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
1452 let buffer = cx.new(|cx| Buffer::local(text, cx));
1453 log::info!(
1454 "Creating new buffer {} with text: {:?}",
1455 buffer.read(cx).remote_id(),
1456 buffer.read(cx).text()
1457 );
1458 let buffer_snapshot = buffer.read(cx).snapshot();
1459 let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
1460 // Create some initial diff hunks.
1461 buffer.update(cx, |buffer, cx| {
1462 buffer.randomly_edit(rng, 1, cx);
1463 });
1464 let buffer_snapshot = buffer.read(cx).text_snapshot();
1465 diff.update(cx, |diff, cx| {
1466 diff.recalculate_diff_sync(&buffer_snapshot, cx);
1467 });
1468 let path = PathKey::for_buffer(&buffer, cx);
1469 let ranges = diff.update(cx, |diff, cx| {
1470 diff.snapshot(cx)
1471 .hunks(&buffer_snapshot)
1472 .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
1473 .collect::<Vec<_>>()
1474 });
1475 self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1476 } else {
1477 log::info!("removing excerpts");
1478 let remove_count = rng.random_range(1..=paths.len());
1479 let paths_to_remove = paths
1480 .choose_multiple(rng, remove_count)
1481 .cloned()
1482 .collect::<Vec<_>>();
1483 for path in paths_to_remove {
1484 self.remove_excerpts_for_path(path.clone(), cx);
1485 }
1486 }
1487 }
1488 }
1489}
1490
1491impl EventEmitter<EditorEvent> for SplittableEditor {}
1492impl Focusable for SplittableEditor {
1493 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1494 self.primary_editor.read(cx).focus_handle(cx)
1495 }
1496}
1497
1498impl Render for SplittableEditor {
1499 fn render(
1500 &mut self,
1501 _window: &mut ui::Window,
1502 cx: &mut ui::Context<Self>,
1503 ) -> impl ui::IntoElement {
1504 let inner = if self.secondary.is_some() {
1505 let style = self.primary_editor.read(cx).create_style(cx);
1506 SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
1507 } else {
1508 self.primary_editor.clone().into_any_element()
1509 };
1510 div()
1511 .id("splittable-editor")
1512 .on_action(cx.listener(Self::split))
1513 .on_action(cx.listener(Self::unsplit))
1514 .on_action(cx.listener(Self::toggle_split))
1515 .on_action(cx.listener(Self::activate_pane_left))
1516 .on_action(cx.listener(Self::activate_pane_right))
1517 .on_action(cx.listener(Self::toggle_locked_cursors))
1518 .on_action(cx.listener(Self::intercept_toggle_code_actions))
1519 .on_action(cx.listener(Self::intercept_toggle_breakpoint))
1520 .on_action(cx.listener(Self::intercept_enable_breakpoint))
1521 .on_action(cx.listener(Self::intercept_disable_breakpoint))
1522 .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
1523 .on_action(cx.listener(Self::intercept_inline_assist))
1524 .capture_action(cx.listener(Self::toggle_soft_wrap))
1525 .size_full()
1526 .child(inner)
1527 }
1528}
1529
1530impl SecondaryEditor {
1531 fn update_path_excerpts_from_primary(
1532 &mut self,
1533 path_key: PathKey,
1534 primary_multibuffer: &Entity<MultiBuffer>,
1535 diff: Entity<BufferDiff>,
1536 cx: &mut App,
1537 ) -> Vec<(ExcerptId, ExcerptId)> {
1538 let primary_multibuffer_ref = primary_multibuffer.read(cx);
1539 let primary_excerpt_ids: Vec<ExcerptId> = primary_multibuffer_ref
1540 .excerpts_for_path(&path_key)
1541 .collect();
1542
1543 let Some(excerpt_id) = primary_multibuffer_ref.excerpts_for_path(&path_key).next() else {
1544 self.multibuffer.update(cx, |multibuffer, cx| {
1545 multibuffer.remove_excerpts_for_path(path_key, cx);
1546 });
1547 return Vec::new();
1548 };
1549
1550 let primary_multibuffer_snapshot = primary_multibuffer_ref.snapshot(cx);
1551 let main_buffer = primary_multibuffer_snapshot
1552 .buffer_for_excerpt(excerpt_id)
1553 .unwrap();
1554 let base_text_buffer = diff.read(cx).base_text_buffer();
1555 let diff_snapshot = diff.read(cx).snapshot(cx);
1556 let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
1557 let new = primary_multibuffer_ref
1558 .excerpts_for_buffer(main_buffer.remote_id(), cx)
1559 .into_iter()
1560 .map(|(_, excerpt_range)| {
1561 let point_range_to_base_text_point_range = |range: Range<Point>| {
1562 let (mut translated, _, _) = diff_snapshot.points_to_base_text_points(
1563 [Point::new(range.start.row, 0), Point::new(range.end.row, 0)],
1564 main_buffer,
1565 );
1566 let start_row = translated.next().unwrap().start.row;
1567 let end_row = translated.next().unwrap().end.row;
1568 let end_column = diff_snapshot.base_text().line_len(end_row);
1569 Point::new(start_row, 0)..Point::new(end_row, end_column)
1570 };
1571 let primary = excerpt_range.primary.to_point(main_buffer);
1572 let context = excerpt_range.context.to_point(main_buffer);
1573 ExcerptRange {
1574 primary: point_range_to_base_text_point_range(primary),
1575 context: point_range_to_base_text_point_range(context),
1576 }
1577 })
1578 .collect();
1579
1580 self.editor.update(cx, |editor, cx| {
1581 editor.buffer().update(cx, |buffer, cx| {
1582 let (ids, _) = buffer.update_path_excerpts(
1583 path_key.clone(),
1584 base_text_buffer.clone(),
1585 &base_text_buffer_snapshot,
1586 new,
1587 cx,
1588 );
1589 if !ids.is_empty()
1590 && buffer
1591 .diff_for(base_text_buffer.read(cx).remote_id())
1592 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1593 {
1594 buffer.add_inverted_diff(diff, cx);
1595 }
1596 })
1597 });
1598
1599 let secondary_excerpt_ids: Vec<ExcerptId> = self
1600 .multibuffer
1601 .read(cx)
1602 .excerpts_for_path(&path_key)
1603 .collect();
1604
1605 debug_assert_eq!(primary_excerpt_ids.len(), secondary_excerpt_ids.len());
1606
1607 secondary_excerpt_ids
1608 .into_iter()
1609 .zip(primary_excerpt_ids)
1610 .collect()
1611 }
1612
1613 fn sync_path_excerpts(
1614 &mut self,
1615 path_key: PathKey,
1616 primary_multibuffer: &Entity<MultiBuffer>,
1617 diff: Entity<BufferDiff>,
1618 primary_display_map: &Entity<DisplayMap>,
1619 secondary_display_map: &Entity<DisplayMap>,
1620 cx: &mut App,
1621 ) {
1622 self.remove_mappings_for_path(
1623 &path_key,
1624 primary_multibuffer,
1625 primary_display_map,
1626 secondary_display_map,
1627 cx,
1628 );
1629
1630 let mappings =
1631 self.update_path_excerpts_from_primary(path_key, primary_multibuffer, diff.clone(), cx);
1632
1633 let secondary_buffer_id = diff.read(cx).base_text(cx).remote_id();
1634 let primary_buffer_id = diff.read(cx).buffer_id;
1635
1636 if let Some(companion) = primary_display_map.read(cx).companion().cloned() {
1637 companion.update(cx, |c, _| {
1638 for (lhs, rhs) in mappings {
1639 c.add_excerpt_mapping(lhs, rhs);
1640 }
1641 c.add_buffer_mapping(secondary_buffer_id, primary_buffer_id);
1642 });
1643 }
1644 }
1645
1646 fn remove_mappings_for_path(
1647 &self,
1648 path_key: &PathKey,
1649 primary_multibuffer: &Entity<MultiBuffer>,
1650 primary_display_map: &Entity<DisplayMap>,
1651 _secondary_display_map: &Entity<DisplayMap>,
1652 cx: &mut App,
1653 ) {
1654 let primary_excerpt_ids: Vec<ExcerptId> = primary_multibuffer
1655 .read(cx)
1656 .excerpts_for_path(path_key)
1657 .collect();
1658 let secondary_excerpt_ids: Vec<ExcerptId> = self
1659 .multibuffer
1660 .read(cx)
1661 .excerpts_for_path(path_key)
1662 .collect();
1663
1664 if let Some(companion) = primary_display_map.read(cx).companion().cloned() {
1665 companion.update(cx, |c, _| {
1666 c.remove_excerpt_mappings(secondary_excerpt_ids, primary_excerpt_ids);
1667 });
1668 }
1669 }
1670}
1671
1672#[cfg(test)]
1673mod tests {
1674 use buffer_diff::BufferDiff;
1675 use fs::FakeFs;
1676 use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
1677 use language::language_settings::SoftWrap;
1678 use language::{Buffer, Capability};
1679 use multi_buffer::{MultiBuffer, PathKey};
1680 use pretty_assertions::assert_eq;
1681 use project::Project;
1682 use rand::rngs::StdRng;
1683 use settings::SettingsStore;
1684 use ui::{VisualContext as _, px};
1685 use workspace::Workspace;
1686
1687 use crate::SplittableEditor;
1688 use crate::test::editor_content_with_blocks_and_width;
1689
1690 async fn init_test(
1691 cx: &mut gpui::TestAppContext,
1692 ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
1693 cx.update(|cx| {
1694 let store = SettingsStore::test(cx);
1695 cx.set_global(store);
1696 theme::init(theme::LoadThemes::JustBase, cx);
1697 crate::init(cx);
1698 });
1699 let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
1700 let (workspace, cx) =
1701 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1702 let primary_multibuffer = cx.new(|cx| {
1703 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1704 multibuffer.set_all_diff_hunks_expanded(cx);
1705 multibuffer
1706 });
1707 let editor = cx.new_window_entity(|window, cx| {
1708 let mut editor = SplittableEditor::new_unsplit(
1709 primary_multibuffer.clone(),
1710 project.clone(),
1711 workspace,
1712 window,
1713 cx,
1714 );
1715 editor.split(&Default::default(), window, cx);
1716 editor.primary_editor.update(cx, |editor, cx| {
1717 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1718 });
1719 editor
1720 .secondary
1721 .as_ref()
1722 .unwrap()
1723 .editor
1724 .update(cx, |editor, cx| {
1725 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1726 });
1727 editor
1728 });
1729 (editor, cx)
1730 }
1731
1732 fn buffer_with_diff(
1733 base_text: &str,
1734 current_text: &str,
1735 cx: &mut VisualTestContext,
1736 ) -> (Entity<Buffer>, Entity<BufferDiff>) {
1737 let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
1738 let diff = cx.new(|cx| {
1739 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
1740 });
1741 (buffer, diff)
1742 }
1743
1744 #[track_caller]
1745 fn assert_split_content(
1746 editor: &Entity<SplittableEditor>,
1747 expected_primary: String,
1748 expected_secondary: String,
1749 cx: &mut VisualTestContext,
1750 ) {
1751 assert_split_content_with_widths(
1752 editor,
1753 px(3000.0),
1754 px(3000.0),
1755 expected_primary,
1756 expected_secondary,
1757 cx,
1758 );
1759 }
1760
1761 #[track_caller]
1762 fn assert_split_content_with_widths(
1763 editor: &Entity<SplittableEditor>,
1764 primary_width: Pixels,
1765 secondary_width: Pixels,
1766 expected_primary: String,
1767 expected_secondary: String,
1768 cx: &mut VisualTestContext,
1769 ) {
1770 let (primary_editor, secondary_editor) = editor.update(cx, |editor, _cx| {
1771 let secondary = editor
1772 .secondary
1773 .as_ref()
1774 .expect("should have secondary editor");
1775 (editor.primary_editor.clone(), secondary.editor.clone())
1776 });
1777
1778 // Make sure both sides learn if the other has soft-wrapped
1779 let _ = editor_content_with_blocks_and_width(&primary_editor, primary_width, cx);
1780 cx.run_until_parked();
1781 let _ = editor_content_with_blocks_and_width(&secondary_editor, secondary_width, cx);
1782 cx.run_until_parked();
1783
1784 let primary_content =
1785 editor_content_with_blocks_and_width(&primary_editor, primary_width, cx);
1786 let secondary_content =
1787 editor_content_with_blocks_and_width(&secondary_editor, secondary_width, cx);
1788
1789 if primary_content != expected_primary || secondary_content != expected_secondary {
1790 editor.update(cx, |editor, cx| editor.debug_print(cx));
1791 }
1792
1793 assert_eq!(primary_content, expected_primary, "rhs");
1794 assert_eq!(secondary_content, expected_secondary, "lhs");
1795 }
1796
1797 #[gpui::test(iterations = 100)]
1798 async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
1799 use rand::prelude::*;
1800
1801 let (editor, cx) = init_test(cx).await;
1802 let operations = std::env::var("OPERATIONS")
1803 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1804 .unwrap_or(10);
1805 let rng = &mut rng;
1806 for _ in 0..operations {
1807 let buffers = editor.update(cx, |editor, cx| {
1808 editor
1809 .primary_editor
1810 .read(cx)
1811 .buffer()
1812 .read(cx)
1813 .all_buffers()
1814 });
1815
1816 if buffers.is_empty() {
1817 log::info!("adding excerpts to empty multibuffer");
1818 editor.update(cx, |editor, cx| {
1819 editor.randomly_edit_excerpts(rng, 2, cx);
1820 editor.check_invariants(true, cx);
1821 });
1822 continue;
1823 }
1824
1825 let mut quiesced = false;
1826
1827 match rng.random_range(0..100) {
1828 0..=44 => {
1829 log::info!("randomly editing multibuffer");
1830 editor.update(cx, |editor, cx| {
1831 editor.primary_multibuffer.update(cx, |multibuffer, cx| {
1832 multibuffer.randomly_edit(rng, 5, cx);
1833 })
1834 })
1835 }
1836 45..=64 => {
1837 log::info!("randomly undoing/redoing in single buffer");
1838 let buffer = buffers.iter().choose(rng).unwrap();
1839 buffer.update(cx, |buffer, cx| {
1840 buffer.randomly_undo_redo(rng, cx);
1841 });
1842 }
1843 65..=79 => {
1844 log::info!("mutating excerpts");
1845 editor.update(cx, |editor, cx| {
1846 editor.randomly_edit_excerpts(rng, 2, cx);
1847 });
1848 }
1849 _ => {
1850 log::info!("quiescing");
1851 for buffer in buffers {
1852 let buffer_snapshot =
1853 buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
1854 let diff = editor.update(cx, |editor, cx| {
1855 editor
1856 .primary_multibuffer
1857 .read(cx)
1858 .diff_for(buffer.read(cx).remote_id())
1859 .unwrap()
1860 });
1861 diff.update(cx, |diff, cx| {
1862 diff.recalculate_diff_sync(&buffer_snapshot, cx);
1863 });
1864 cx.run_until_parked();
1865 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
1866 let ranges = diff_snapshot
1867 .hunks(&buffer_snapshot)
1868 .map(|hunk| hunk.range)
1869 .collect::<Vec<_>>();
1870 editor.update(cx, |editor, cx| {
1871 let path = PathKey::for_buffer(&buffer, cx);
1872 editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1873 });
1874 }
1875 quiesced = true;
1876 }
1877 }
1878
1879 editor.update(cx, |editor, cx| {
1880 editor.check_invariants(quiesced, cx);
1881 });
1882 }
1883 }
1884
1885 #[gpui::test]
1886 async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
1887 use rope::Point;
1888 use unindent::Unindent as _;
1889
1890 let (editor, mut cx) = init_test(cx).await;
1891
1892 let base_text = "
1893 aaa
1894 bbb
1895 ccc
1896 ddd
1897 eee
1898 fff
1899 "
1900 .unindent();
1901 let current_text = "
1902 aaa
1903 ddd
1904 eee
1905 fff
1906 "
1907 .unindent();
1908
1909 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
1910
1911 editor.update(cx, |editor, cx| {
1912 let path = PathKey::for_buffer(&buffer, cx);
1913 editor.set_excerpts_for_path(
1914 path,
1915 buffer.clone(),
1916 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
1917 0,
1918 diff.clone(),
1919 cx,
1920 );
1921 });
1922
1923 cx.run_until_parked();
1924
1925 assert_split_content(
1926 &editor,
1927 "
1928 § <no file>
1929 § -----
1930 aaa
1931 § spacer
1932 § spacer
1933 ddd
1934 eee
1935 fff"
1936 .unindent(),
1937 "
1938 § <no file>
1939 § -----
1940 aaa
1941 bbb
1942 ccc
1943 ddd
1944 eee
1945 fff"
1946 .unindent(),
1947 &mut cx,
1948 );
1949
1950 buffer.update(cx, |buffer, cx| {
1951 buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
1952 });
1953
1954 cx.run_until_parked();
1955
1956 assert_split_content(
1957 &editor,
1958 "
1959 § <no file>
1960 § -----
1961 aaa
1962 § spacer
1963 § spacer
1964 ddd
1965 eee
1966 FFF"
1967 .unindent(),
1968 "
1969 § <no file>
1970 § -----
1971 aaa
1972 bbb
1973 ccc
1974 ddd
1975 eee
1976 fff"
1977 .unindent(),
1978 &mut cx,
1979 );
1980
1981 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
1982 diff.update(cx, |diff, cx| {
1983 diff.recalculate_diff_sync(&buffer_snapshot, cx);
1984 });
1985
1986 cx.run_until_parked();
1987
1988 assert_split_content(
1989 &editor,
1990 "
1991 § <no file>
1992 § -----
1993 aaa
1994 § spacer
1995 § spacer
1996 ddd
1997 eee
1998 FFF"
1999 .unindent(),
2000 "
2001 § <no file>
2002 § -----
2003 aaa
2004 bbb
2005 ccc
2006 ddd
2007 eee
2008 fff"
2009 .unindent(),
2010 &mut cx,
2011 );
2012 }
2013
2014 #[gpui::test]
2015 async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2016 use rope::Point;
2017 use unindent::Unindent as _;
2018
2019 let (editor, mut cx) = init_test(cx).await;
2020
2021 let base_text1 = "
2022 aaa
2023 bbb
2024 ccc
2025 ddd
2026 eee"
2027 .unindent();
2028
2029 let base_text2 = "
2030 fff
2031 ggg
2032 hhh
2033 iii
2034 jjj"
2035 .unindent();
2036
2037 let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2038 let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2039
2040 editor.update(cx, |editor, cx| {
2041 let path1 = PathKey::for_buffer(&buffer1, cx);
2042 editor.set_excerpts_for_path(
2043 path1,
2044 buffer1.clone(),
2045 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2046 0,
2047 diff1.clone(),
2048 cx,
2049 );
2050 let path2 = PathKey::for_buffer(&buffer2, cx);
2051 editor.set_excerpts_for_path(
2052 path2,
2053 buffer2.clone(),
2054 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2055 1,
2056 diff2.clone(),
2057 cx,
2058 );
2059 });
2060
2061 cx.run_until_parked();
2062
2063 buffer1.update(cx, |buffer, cx| {
2064 buffer.edit(
2065 [
2066 (Point::new(0, 0)..Point::new(1, 0), ""),
2067 (Point::new(3, 0)..Point::new(4, 0), ""),
2068 ],
2069 None,
2070 cx,
2071 );
2072 });
2073 buffer2.update(cx, |buffer, cx| {
2074 buffer.edit(
2075 [
2076 (Point::new(0, 0)..Point::new(1, 0), ""),
2077 (Point::new(3, 0)..Point::new(4, 0), ""),
2078 ],
2079 None,
2080 cx,
2081 );
2082 });
2083
2084 cx.run_until_parked();
2085
2086 assert_split_content(
2087 &editor,
2088 "
2089 § <no file>
2090 § -----
2091 § spacer
2092 bbb
2093 ccc
2094 § spacer
2095 eee
2096 § <no file>
2097 § -----
2098 § spacer
2099 ggg
2100 hhh
2101 § spacer
2102 jjj"
2103 .unindent(),
2104 "
2105 § <no file>
2106 § -----
2107 aaa
2108 bbb
2109 ccc
2110 ddd
2111 eee
2112 § <no file>
2113 § -----
2114 fff
2115 ggg
2116 hhh
2117 iii
2118 jjj"
2119 .unindent(),
2120 &mut cx,
2121 );
2122
2123 let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2124 diff1.update(cx, |diff, cx| {
2125 diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2126 });
2127 let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2128 diff2.update(cx, |diff, cx| {
2129 diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2130 });
2131
2132 cx.run_until_parked();
2133
2134 assert_split_content(
2135 &editor,
2136 "
2137 § <no file>
2138 § -----
2139 § spacer
2140 bbb
2141 ccc
2142 § spacer
2143 eee
2144 § <no file>
2145 § -----
2146 § spacer
2147 ggg
2148 hhh
2149 § spacer
2150 jjj"
2151 .unindent(),
2152 "
2153 § <no file>
2154 § -----
2155 aaa
2156 bbb
2157 ccc
2158 ddd
2159 eee
2160 § <no file>
2161 § -----
2162 fff
2163 ggg
2164 hhh
2165 iii
2166 jjj"
2167 .unindent(),
2168 &mut cx,
2169 );
2170 }
2171
2172 #[gpui::test]
2173 async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2174 use rope::Point;
2175 use unindent::Unindent as _;
2176
2177 let (editor, mut cx) = init_test(cx).await;
2178
2179 let base_text = "
2180 aaa
2181 bbb
2182 ccc
2183 ddd
2184 "
2185 .unindent();
2186
2187 let current_text = "
2188 aaa
2189 NEW1
2190 NEW2
2191 ccc
2192 ddd
2193 "
2194 .unindent();
2195
2196 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2197
2198 editor.update(cx, |editor, cx| {
2199 let path = PathKey::for_buffer(&buffer, cx);
2200 editor.set_excerpts_for_path(
2201 path,
2202 buffer.clone(),
2203 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2204 0,
2205 diff.clone(),
2206 cx,
2207 );
2208 });
2209
2210 cx.run_until_parked();
2211
2212 assert_split_content(
2213 &editor,
2214 "
2215 § <no file>
2216 § -----
2217 aaa
2218 NEW1
2219 NEW2
2220 ccc
2221 ddd"
2222 .unindent(),
2223 "
2224 § <no file>
2225 § -----
2226 aaa
2227 bbb
2228 § spacer
2229 ccc
2230 ddd"
2231 .unindent(),
2232 &mut cx,
2233 );
2234
2235 buffer.update(cx, |buffer, cx| {
2236 buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2237 });
2238
2239 cx.run_until_parked();
2240
2241 assert_split_content(
2242 &editor,
2243 "
2244 § <no file>
2245 § -----
2246 aaa
2247 NEW1
2248 ccc
2249 ddd"
2250 .unindent(),
2251 "
2252 § <no file>
2253 § -----
2254 aaa
2255 bbb
2256 ccc
2257 ddd"
2258 .unindent(),
2259 &mut cx,
2260 );
2261
2262 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2263 diff.update(cx, |diff, cx| {
2264 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2265 });
2266
2267 cx.run_until_parked();
2268
2269 assert_split_content(
2270 &editor,
2271 "
2272 § <no file>
2273 § -----
2274 aaa
2275 NEW1
2276 ccc
2277 ddd"
2278 .unindent(),
2279 "
2280 § <no file>
2281 § -----
2282 aaa
2283 bbb
2284 ccc
2285 ddd"
2286 .unindent(),
2287 &mut cx,
2288 );
2289 }
2290
2291 #[gpui::test]
2292 async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2293 use rope::Point;
2294 use unindent::Unindent as _;
2295
2296 let (editor, mut cx) = init_test(cx).await;
2297
2298 let base_text = "
2299 aaa
2300 bbb
2301
2302
2303
2304
2305
2306 ccc
2307 ddd
2308 "
2309 .unindent();
2310 let current_text = "
2311 aaa
2312 bbb
2313
2314
2315
2316
2317
2318 CCC
2319 ddd
2320 "
2321 .unindent();
2322
2323 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2324
2325 editor.update(cx, |editor, cx| {
2326 let path = PathKey::for_buffer(&buffer, cx);
2327 editor.set_excerpts_for_path(
2328 path,
2329 buffer.clone(),
2330 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2331 0,
2332 diff.clone(),
2333 cx,
2334 );
2335 });
2336
2337 cx.run_until_parked();
2338
2339 buffer.update(cx, |buffer, cx| {
2340 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2341 });
2342
2343 cx.run_until_parked();
2344
2345 assert_split_content(
2346 &editor,
2347 "
2348 § <no file>
2349 § -----
2350 aaa
2351 bbb
2352
2353
2354
2355
2356
2357
2358 CCC
2359 ddd"
2360 .unindent(),
2361 "
2362 § <no file>
2363 § -----
2364 aaa
2365 bbb
2366 § spacer
2367
2368
2369
2370
2371
2372 ccc
2373 ddd"
2374 .unindent(),
2375 &mut cx,
2376 );
2377
2378 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2379 diff.update(cx, |diff, cx| {
2380 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2381 });
2382
2383 cx.run_until_parked();
2384
2385 assert_split_content(
2386 &editor,
2387 "
2388 § <no file>
2389 § -----
2390 aaa
2391 bbb
2392
2393
2394
2395
2396
2397
2398 CCC
2399 ddd"
2400 .unindent(),
2401 "
2402 § <no file>
2403 § -----
2404 aaa
2405 bbb
2406
2407
2408
2409
2410
2411 ccc
2412 § spacer
2413 ddd"
2414 .unindent(),
2415 &mut cx,
2416 );
2417 }
2418
2419 #[gpui::test]
2420 async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2421 use git::Restore;
2422 use rope::Point;
2423 use unindent::Unindent as _;
2424
2425 let (editor, mut cx) = init_test(cx).await;
2426
2427 let base_text = "
2428 aaa
2429 bbb
2430 ccc
2431 ddd
2432 eee
2433 "
2434 .unindent();
2435 let current_text = "
2436 aaa
2437 ddd
2438 eee
2439 "
2440 .unindent();
2441
2442 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2443
2444 editor.update(cx, |editor, cx| {
2445 let path = PathKey::for_buffer(&buffer, cx);
2446 editor.set_excerpts_for_path(
2447 path,
2448 buffer.clone(),
2449 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2450 0,
2451 diff.clone(),
2452 cx,
2453 );
2454 });
2455
2456 cx.run_until_parked();
2457
2458 assert_split_content(
2459 &editor,
2460 "
2461 § <no file>
2462 § -----
2463 aaa
2464 § spacer
2465 § spacer
2466 ddd
2467 eee"
2468 .unindent(),
2469 "
2470 § <no file>
2471 § -----
2472 aaa
2473 bbb
2474 ccc
2475 ddd
2476 eee"
2477 .unindent(),
2478 &mut cx,
2479 );
2480
2481 let primary_editor = editor.update(cx, |editor, _cx| editor.primary_editor.clone());
2482 cx.update_window_entity(&primary_editor, |editor, window, cx| {
2483 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2484 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2485 });
2486 editor.git_restore(&Restore, window, cx);
2487 });
2488
2489 cx.run_until_parked();
2490
2491 assert_split_content(
2492 &editor,
2493 "
2494 § <no file>
2495 § -----
2496 aaa
2497 bbb
2498 ccc
2499 ddd
2500 eee"
2501 .unindent(),
2502 "
2503 § <no file>
2504 § -----
2505 aaa
2506 bbb
2507 ccc
2508 ddd
2509 eee"
2510 .unindent(),
2511 &mut cx,
2512 );
2513
2514 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2515 diff.update(cx, |diff, cx| {
2516 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2517 });
2518
2519 cx.run_until_parked();
2520
2521 assert_split_content(
2522 &editor,
2523 "
2524 § <no file>
2525 § -----
2526 aaa
2527 bbb
2528 ccc
2529 ddd
2530 eee"
2531 .unindent(),
2532 "
2533 § <no file>
2534 § -----
2535 aaa
2536 bbb
2537 ccc
2538 ddd
2539 eee"
2540 .unindent(),
2541 &mut cx,
2542 );
2543 }
2544
2545 #[gpui::test]
2546 async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
2547 use rope::Point;
2548 use unindent::Unindent as _;
2549
2550 let (editor, mut cx) = init_test(cx).await;
2551
2552 let base_text = "
2553 aaa
2554 old1
2555 old2
2556 old3
2557 old4
2558 zzz
2559 "
2560 .unindent();
2561
2562 let current_text = "
2563 aaa
2564 new1
2565 new2
2566 new3
2567 new4
2568 zzz
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(
2590 [
2591 (Point::new(2, 0)..Point::new(3, 0), ""),
2592 (Point::new(4, 0)..Point::new(5, 0), ""),
2593 ],
2594 None,
2595 cx,
2596 );
2597 });
2598 cx.run_until_parked();
2599
2600 assert_split_content(
2601 &editor,
2602 "
2603 § <no file>
2604 § -----
2605 aaa
2606 new1
2607 new3
2608 § spacer
2609 § spacer
2610 zzz"
2611 .unindent(),
2612 "
2613 § <no file>
2614 § -----
2615 aaa
2616 old1
2617 old2
2618 old3
2619 old4
2620 zzz"
2621 .unindent(),
2622 &mut cx,
2623 );
2624
2625 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2626 diff.update(cx, |diff, cx| {
2627 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2628 });
2629
2630 cx.run_until_parked();
2631
2632 assert_split_content(
2633 &editor,
2634 "
2635 § <no file>
2636 § -----
2637 aaa
2638 new1
2639 new3
2640 § spacer
2641 § spacer
2642 zzz"
2643 .unindent(),
2644 "
2645 § <no file>
2646 § -----
2647 aaa
2648 old1
2649 old2
2650 old3
2651 old4
2652 zzz"
2653 .unindent(),
2654 &mut cx,
2655 );
2656 }
2657
2658 #[gpui::test]
2659 async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
2660 use rope::Point;
2661 use unindent::Unindent as _;
2662
2663 let (editor, mut cx) = init_test(cx).await;
2664
2665 let text = "aaaa bbbb cccc dddd eeee ffff";
2666
2667 let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
2668 let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
2669
2670 editor.update(cx, |editor, cx| {
2671 let end = Point::new(0, text.len() as u32);
2672 let path1 = PathKey::for_buffer(&buffer1, cx);
2673 editor.set_excerpts_for_path(
2674 path1,
2675 buffer1.clone(),
2676 vec![Point::new(0, 0)..end],
2677 0,
2678 diff1.clone(),
2679 cx,
2680 );
2681 let path2 = PathKey::for_buffer(&buffer2, cx);
2682 editor.set_excerpts_for_path(
2683 path2,
2684 buffer2.clone(),
2685 vec![Point::new(0, 0)..end],
2686 0,
2687 diff2.clone(),
2688 cx,
2689 );
2690 });
2691
2692 cx.run_until_parked();
2693
2694 assert_split_content_with_widths(
2695 &editor,
2696 px(200.0),
2697 px(400.0),
2698 "
2699 § <no file>
2700 § -----
2701 aaaa bbbb\x20
2702 cccc dddd\x20
2703 eeee ffff
2704 § <no file>
2705 § -----
2706 aaaa bbbb\x20
2707 cccc dddd\x20
2708 eeee ffff"
2709 .unindent(),
2710 "
2711 § <no file>
2712 § -----
2713 aaaa bbbb cccc dddd eeee ffff
2714 § spacer
2715 § spacer
2716 § <no file>
2717 § -----
2718 aaaa bbbb cccc dddd eeee ffff
2719 § spacer
2720 § spacer"
2721 .unindent(),
2722 &mut cx,
2723 );
2724 }
2725
2726 #[gpui::test]
2727 async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
2728 use rope::Point;
2729 use unindent::Unindent as _;
2730
2731 let (editor, mut cx) = init_test(cx).await;
2732
2733 let base_text = "
2734 aaaa bbbb cccc dddd eeee ffff
2735 old line one
2736 old line two
2737 "
2738 .unindent();
2739
2740 let current_text = "
2741 aaaa bbbb cccc dddd eeee ffff
2742 new line
2743 "
2744 .unindent();
2745
2746 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2747
2748 editor.update(cx, |editor, cx| {
2749 let path = PathKey::for_buffer(&buffer, cx);
2750 editor.set_excerpts_for_path(
2751 path,
2752 buffer.clone(),
2753 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2754 0,
2755 diff.clone(),
2756 cx,
2757 );
2758 });
2759
2760 cx.run_until_parked();
2761
2762 assert_split_content_with_widths(
2763 &editor,
2764 px(200.0),
2765 px(400.0),
2766 "
2767 § <no file>
2768 § -----
2769 aaaa bbbb\x20
2770 cccc dddd\x20
2771 eeee ffff
2772 new line
2773 § spacer"
2774 .unindent(),
2775 "
2776 § <no file>
2777 § -----
2778 aaaa bbbb cccc dddd eeee ffff
2779 § spacer
2780 § spacer
2781 old line one
2782 old line two"
2783 .unindent(),
2784 &mut cx,
2785 );
2786 }
2787
2788 #[gpui::test]
2789 async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
2790 use rope::Point;
2791 use unindent::Unindent as _;
2792
2793 let (editor, mut cx) = init_test(cx).await;
2794
2795 let base_text = "
2796 aaaa bbbb cccc dddd eeee ffff
2797 deleted line one
2798 deleted line two
2799 after
2800 "
2801 .unindent();
2802
2803 let current_text = "
2804 aaaa bbbb cccc dddd eeee ffff
2805 after
2806 "
2807 .unindent();
2808
2809 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2810
2811 editor.update(cx, |editor, cx| {
2812 let path = PathKey::for_buffer(&buffer, cx);
2813 editor.set_excerpts_for_path(
2814 path,
2815 buffer.clone(),
2816 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2817 0,
2818 diff.clone(),
2819 cx,
2820 );
2821 });
2822
2823 cx.run_until_parked();
2824
2825 assert_split_content_with_widths(
2826 &editor,
2827 px(400.0),
2828 px(200.0),
2829 "
2830 § <no file>
2831 § -----
2832 aaaa bbbb cccc dddd eeee ffff
2833 § spacer
2834 § spacer
2835 § spacer
2836 § spacer
2837 § spacer
2838 § spacer
2839 after"
2840 .unindent(),
2841 "
2842 § <no file>
2843 § -----
2844 aaaa bbbb\x20
2845 cccc dddd\x20
2846 eeee ffff
2847 deleted\x20
2848 line one
2849 deleted\x20
2850 line two
2851 after"
2852 .unindent(),
2853 &mut cx,
2854 );
2855 }
2856
2857 #[gpui::test]
2858 async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
2859 use rope::Point;
2860 use unindent::Unindent as _;
2861
2862 let (editor, mut cx) = init_test(cx).await;
2863
2864 let text = "
2865 aaaa bbbb cccc dddd eeee ffff
2866 short
2867 "
2868 .unindent();
2869
2870 let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
2871
2872 editor.update(cx, |editor, cx| {
2873 let path = PathKey::for_buffer(&buffer, cx);
2874 editor.set_excerpts_for_path(
2875 path,
2876 buffer.clone(),
2877 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2878 0,
2879 diff.clone(),
2880 cx,
2881 );
2882 });
2883
2884 cx.run_until_parked();
2885
2886 assert_split_content_with_widths(
2887 &editor,
2888 px(400.0),
2889 px(200.0),
2890 "
2891 § <no file>
2892 § -----
2893 aaaa bbbb cccc dddd eeee ffff
2894 § spacer
2895 § spacer
2896 short"
2897 .unindent(),
2898 "
2899 § <no file>
2900 § -----
2901 aaaa bbbb\x20
2902 cccc dddd\x20
2903 eeee ffff
2904 short"
2905 .unindent(),
2906 &mut cx,
2907 );
2908
2909 buffer.update(cx, |buffer, cx| {
2910 buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
2911 });
2912
2913 cx.run_until_parked();
2914
2915 assert_split_content_with_widths(
2916 &editor,
2917 px(400.0),
2918 px(200.0),
2919 "
2920 § <no file>
2921 § -----
2922 aaaa bbbb cccc dddd eeee ffff
2923 § spacer
2924 § spacer
2925 modified"
2926 .unindent(),
2927 "
2928 § <no file>
2929 § -----
2930 aaaa bbbb\x20
2931 cccc dddd\x20
2932 eeee ffff
2933 short"
2934 .unindent(),
2935 &mut cx,
2936 );
2937
2938 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2939 diff.update(cx, |diff, cx| {
2940 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2941 });
2942
2943 cx.run_until_parked();
2944
2945 assert_split_content_with_widths(
2946 &editor,
2947 px(400.0),
2948 px(200.0),
2949 "
2950 § <no file>
2951 § -----
2952 aaaa bbbb cccc dddd eeee ffff
2953 § spacer
2954 § spacer
2955 modified"
2956 .unindent(),
2957 "
2958 § <no file>
2959 § -----
2960 aaaa bbbb\x20
2961 cccc dddd\x20
2962 eeee ffff
2963 short"
2964 .unindent(),
2965 &mut cx,
2966 );
2967 }
2968
2969 #[gpui::test]
2970 async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
2971 use rope::Point;
2972 use unindent::Unindent as _;
2973
2974 let (editor, mut cx) = init_test(cx).await;
2975
2976 let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
2977
2978 let current_text = "
2979 aaa
2980 bbb
2981 ccc
2982 "
2983 .unindent();
2984
2985 let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2986 let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
2987
2988 editor.update(cx, |editor, cx| {
2989 let path1 = PathKey::for_buffer(&buffer1, cx);
2990 editor.set_excerpts_for_path(
2991 path1,
2992 buffer1.clone(),
2993 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2994 0,
2995 diff1.clone(),
2996 cx,
2997 );
2998
2999 let path2 = PathKey::for_buffer(&buffer2, cx);
3000 editor.set_excerpts_for_path(
3001 path2,
3002 buffer2.clone(),
3003 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3004 1,
3005 diff2.clone(),
3006 cx,
3007 );
3008 });
3009
3010 cx.run_until_parked();
3011
3012 assert_split_content(
3013 &editor,
3014 "
3015 § <no file>
3016 § -----
3017 xxx
3018 yyy
3019 § <no file>
3020 § -----
3021 aaa
3022 bbb
3023 ccc"
3024 .unindent(),
3025 "
3026 § <no file>
3027 § -----
3028 xxx
3029 yyy
3030 § <no file>
3031 § -----
3032 § spacer
3033 § spacer
3034 § spacer"
3035 .unindent(),
3036 &mut cx,
3037 );
3038
3039 buffer1.update(cx, |buffer, cx| {
3040 buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3041 });
3042
3043 cx.run_until_parked();
3044
3045 assert_split_content(
3046 &editor,
3047 "
3048 § <no file>
3049 § -----
3050 xxxz
3051 yyy
3052 § <no file>
3053 § -----
3054 aaa
3055 bbb
3056 ccc"
3057 .unindent(),
3058 "
3059 § <no file>
3060 § -----
3061 xxx
3062 yyy
3063 § <no file>
3064 § -----
3065 § spacer
3066 § spacer
3067 § spacer"
3068 .unindent(),
3069 &mut cx,
3070 );
3071 }
3072
3073 #[gpui::test]
3074 async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3075 use rope::Point;
3076 use unindent::Unindent as _;
3077
3078 let (editor, mut cx) = init_test(cx).await;
3079
3080 let base_text = "
3081 aaa
3082 bbb
3083 ccc
3084 "
3085 .unindent();
3086
3087 let current_text = "
3088 NEW1
3089 NEW2
3090 ccc
3091 "
3092 .unindent();
3093
3094 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3095
3096 editor.update(cx, |editor, cx| {
3097 let path = PathKey::for_buffer(&buffer, cx);
3098 editor.set_excerpts_for_path(
3099 path,
3100 buffer.clone(),
3101 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3102 0,
3103 diff.clone(),
3104 cx,
3105 );
3106 });
3107
3108 cx.run_until_parked();
3109
3110 assert_split_content(
3111 &editor,
3112 "
3113 § <no file>
3114 § -----
3115 NEW1
3116 NEW2
3117 ccc"
3118 .unindent(),
3119 "
3120 § <no file>
3121 § -----
3122 aaa
3123 bbb
3124 ccc"
3125 .unindent(),
3126 &mut cx,
3127 );
3128
3129 buffer.update(cx, |buffer, cx| {
3130 buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3131 });
3132
3133 cx.run_until_parked();
3134
3135 assert_split_content(
3136 &editor,
3137 "
3138 § <no file>
3139 § -----
3140 NEW1
3141 NEW
3142 ccc"
3143 .unindent(),
3144 "
3145 § <no file>
3146 § -----
3147 aaa
3148 bbb
3149 ccc"
3150 .unindent(),
3151 &mut cx,
3152 );
3153 }
3154
3155 #[gpui::test]
3156 async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3157 use rope::Point;
3158 use unindent::Unindent as _;
3159
3160 let (editor, mut cx) = init_test(cx).await;
3161
3162 let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3163
3164 let current_text = "
3165 aaaa bbbb cccc dddd eeee ffff
3166 added line
3167 "
3168 .unindent();
3169
3170 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3171
3172 editor.update(cx, |editor, cx| {
3173 let path = PathKey::for_buffer(&buffer, cx);
3174 editor.set_excerpts_for_path(
3175 path,
3176 buffer.clone(),
3177 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3178 0,
3179 diff.clone(),
3180 cx,
3181 );
3182 });
3183
3184 cx.run_until_parked();
3185
3186 assert_split_content_with_widths(
3187 &editor,
3188 px(400.0),
3189 px(200.0),
3190 "
3191 § <no file>
3192 § -----
3193 aaaa bbbb cccc dddd eeee ffff
3194 § spacer
3195 § spacer
3196 added line"
3197 .unindent(),
3198 "
3199 § <no file>
3200 § -----
3201 aaaa bbbb\x20
3202 cccc dddd\x20
3203 eeee ffff
3204 § spacer"
3205 .unindent(),
3206 &mut cx,
3207 );
3208
3209 assert_split_content_with_widths(
3210 &editor,
3211 px(200.0),
3212 px(400.0),
3213 "
3214 § <no file>
3215 § -----
3216 aaaa bbbb\x20
3217 cccc dddd\x20
3218 eeee ffff
3219 added line"
3220 .unindent(),
3221 "
3222 § <no file>
3223 § -----
3224 aaaa bbbb cccc dddd eeee ffff
3225 § spacer
3226 § spacer
3227 § spacer"
3228 .unindent(),
3229 &mut cx,
3230 );
3231 }
3232
3233 #[gpui::test]
3234 #[ignore]
3235 async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3236 use rope::Point;
3237 use unindent::Unindent as _;
3238
3239 let (editor, mut cx) = init_test(cx).await;
3240
3241 let base_text = "
3242 aaa
3243 bbb
3244 ccc
3245 ddd
3246 eee
3247 "
3248 .unindent();
3249
3250 let current_text = "
3251 aaa
3252 NEW
3253 eee
3254 "
3255 .unindent();
3256
3257 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3258
3259 editor.update(cx, |editor, cx| {
3260 let path = PathKey::for_buffer(&buffer, cx);
3261 editor.set_excerpts_for_path(
3262 path,
3263 buffer.clone(),
3264 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3265 0,
3266 diff.clone(),
3267 cx,
3268 );
3269 });
3270
3271 cx.run_until_parked();
3272
3273 assert_split_content(
3274 &editor,
3275 "
3276 § <no file>
3277 § -----
3278 aaa
3279 NEW
3280 § spacer
3281 § spacer
3282 eee"
3283 .unindent(),
3284 "
3285 § <no file>
3286 § -----
3287 aaa
3288 bbb
3289 ccc
3290 ddd
3291 eee"
3292 .unindent(),
3293 &mut cx,
3294 );
3295
3296 buffer.update(cx, |buffer, cx| {
3297 buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3298 });
3299
3300 cx.run_until_parked();
3301
3302 assert_split_content(
3303 &editor,
3304 "
3305 § <no file>
3306 § -----
3307 aaa
3308 § spacer
3309 § spacer
3310 § spacer
3311 NEWeee"
3312 .unindent(),
3313 "
3314 § <no file>
3315 § -----
3316 aaa
3317 bbb
3318 ccc
3319 ddd
3320 eee"
3321 .unindent(),
3322 &mut cx,
3323 );
3324
3325 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3326 diff.update(cx, |diff, cx| {
3327 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3328 });
3329
3330 cx.run_until_parked();
3331
3332 assert_split_content(
3333 &editor,
3334 "
3335 § <no file>
3336 § -----
3337 aaa
3338 NEWeee
3339 § spacer
3340 § spacer
3341 § spacer"
3342 .unindent(),
3343 "
3344 § <no file>
3345 § -----
3346 aaa
3347 bbb
3348 ccc
3349 ddd
3350 eee"
3351 .unindent(),
3352 &mut cx,
3353 );
3354 }
3355}