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 if let Some(this) = this.upgrade() {
549 this.update(cx, |this, cx| {
550 if this.locked_cursors {
551 this.sync_cursor_to_other_side(true, cursor_position, window, cx);
552 }
553 });
554 }
555 },
556 )));
557 });
558 secondary.editor.update(cx, |editor, _cx| {
559 editor.set_scroll_companion(Some(primary_weak));
560 let this = this.clone();
561 editor.set_on_local_selections_changed(Some(Box::new(
562 move |cursor_position, window, cx| {
563 if let Some(this) = this.upgrade() {
564 this.update(cx, |this, cx| {
565 if this.locked_cursors {
566 this.sync_cursor_to_other_side(false, cursor_position, window, cx);
567 }
568 });
569 }
570 },
571 )));
572 });
573
574 let primary_scroll_position = self
575 .primary_editor
576 .update(cx, |editor, cx| editor.scroll_position(cx));
577 secondary.editor.update(cx, |editor, cx| {
578 editor.set_scroll_position_internal(primary_scroll_position, false, false, window, cx);
579 });
580
581 // Copy soft wrap state from primary (source of truth) to secondary
582 let primary_soft_wrap_override = self.primary_editor.read(cx).soft_wrap_mode_override;
583 secondary.editor.update(cx, |editor, cx| {
584 editor.soft_wrap_mode_override = primary_soft_wrap_override;
585 cx.notify();
586 });
587
588 self.secondary = Some(secondary);
589
590 let primary_pane = self.panes.first_pane();
591 self.panes
592 .split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
593 .unwrap();
594 cx.notify();
595 }
596
597 fn activate_pane_left(
598 &mut self,
599 _: &ActivatePaneLeft,
600 window: &mut Window,
601 cx: &mut Context<Self>,
602 ) {
603 if let Some(secondary) = &mut self.secondary {
604 if !secondary.has_latest_selection {
605 secondary.editor.read(cx).focus_handle(cx).focus(window, cx);
606 secondary.editor.update(cx, |editor, cx| {
607 editor.request_autoscroll(Autoscroll::fit(), cx);
608 });
609 secondary.has_latest_selection = true;
610 cx.notify();
611 } else {
612 cx.propagate();
613 }
614 } else {
615 cx.propagate();
616 }
617 }
618
619 fn activate_pane_right(
620 &mut self,
621 _: &ActivatePaneRight,
622 window: &mut Window,
623 cx: &mut Context<Self>,
624 ) {
625 if let Some(secondary) = &mut self.secondary {
626 if secondary.has_latest_selection {
627 self.primary_editor
628 .read(cx)
629 .focus_handle(cx)
630 .focus(window, cx);
631 self.primary_editor.update(cx, |editor, cx| {
632 editor.request_autoscroll(Autoscroll::fit(), cx);
633 });
634 secondary.has_latest_selection = false;
635 cx.notify();
636 } else {
637 cx.propagate();
638 }
639 } else {
640 cx.propagate();
641 }
642 }
643
644 fn toggle_locked_cursors(
645 &mut self,
646 _: &ToggleLockedCursors,
647 _window: &mut Window,
648 cx: &mut Context<Self>,
649 ) {
650 self.locked_cursors = !self.locked_cursors;
651 cx.notify();
652 }
653
654 pub fn locked_cursors(&self) -> bool {
655 self.locked_cursors
656 }
657
658 fn sync_cursor_to_other_side(
659 &mut self,
660 from_primary: bool,
661 source_point: Point,
662 window: &mut Window,
663 cx: &mut Context<Self>,
664 ) {
665 let Some(secondary) = &self.secondary else {
666 return;
667 };
668
669 let target_editor = if from_primary {
670 &secondary.editor
671 } else {
672 &self.primary_editor
673 };
674
675 let (source_multibuffer, target_multibuffer) = if from_primary {
676 (&self.primary_multibuffer, &secondary.multibuffer)
677 } else {
678 (&secondary.multibuffer, &self.primary_multibuffer)
679 };
680
681 let source_snapshot = source_multibuffer.read(cx).snapshot(cx);
682 let target_snapshot = target_multibuffer.read(cx).snapshot(cx);
683
684 let target_point = target_editor.update(cx, |target_editor, cx| {
685 target_editor.display_map.update(cx, |display_map, cx| {
686 let display_map_id = cx.entity_id();
687 display_map.companion().unwrap().update(cx, |companion, _| {
688 companion
689 .convert_rows_from_companion(
690 display_map_id,
691 &target_snapshot,
692 &source_snapshot,
693 (Bound::Included(source_point), Bound::Included(source_point)),
694 )
695 .first()
696 .unwrap()
697 .boundaries
698 .first()
699 .unwrap()
700 .1
701 .start
702 })
703 })
704 });
705
706 target_editor.update(cx, |editor, cx| {
707 editor.set_suppress_selection_callback(true);
708 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
709 s.select_ranges([target_point..target_point]);
710 });
711 editor.set_suppress_selection_callback(false);
712 });
713 }
714
715 fn toggle_split(&mut self, _: &ToggleSplitDiff, window: &mut Window, cx: &mut Context<Self>) {
716 if self.secondary.is_some() {
717 self.unsplit(&UnsplitDiff, window, cx);
718 } else {
719 self.split(&SplitDiff, window, cx);
720 }
721 }
722
723 fn intercept_toggle_code_actions(
724 &mut self,
725 _: &ToggleCodeActions,
726 _window: &mut Window,
727 cx: &mut Context<Self>,
728 ) {
729 if self.secondary.is_some() {
730 cx.stop_propagation();
731 } else {
732 cx.propagate();
733 }
734 }
735
736 fn intercept_toggle_breakpoint(
737 &mut self,
738 _: &ToggleBreakpoint,
739 _window: &mut Window,
740 cx: &mut Context<Self>,
741 ) {
742 // Only block breakpoint actions when the left (secondary) editor has focus
743 if let Some(secondary) = &self.secondary {
744 if secondary.has_latest_selection {
745 cx.stop_propagation();
746 } else {
747 cx.propagate();
748 }
749 } else {
750 cx.propagate();
751 }
752 }
753
754 fn intercept_enable_breakpoint(
755 &mut self,
756 _: &EnableBreakpoint,
757 _window: &mut Window,
758 cx: &mut Context<Self>,
759 ) {
760 // Only block breakpoint actions when the left (secondary) editor has focus
761 if let Some(secondary) = &self.secondary {
762 if secondary.has_latest_selection {
763 cx.stop_propagation();
764 } else {
765 cx.propagate();
766 }
767 } else {
768 cx.propagate();
769 }
770 }
771
772 fn intercept_disable_breakpoint(
773 &mut self,
774 _: &DisableBreakpoint,
775 _window: &mut Window,
776 cx: &mut Context<Self>,
777 ) {
778 // Only block breakpoint actions when the left (secondary) editor has focus
779 if let Some(secondary) = &self.secondary {
780 if secondary.has_latest_selection {
781 cx.stop_propagation();
782 } else {
783 cx.propagate();
784 }
785 } else {
786 cx.propagate();
787 }
788 }
789
790 fn intercept_edit_log_breakpoint(
791 &mut self,
792 _: &EditLogBreakpoint,
793 _window: &mut Window,
794 cx: &mut Context<Self>,
795 ) {
796 // Only block breakpoint actions when the left (secondary) editor has focus
797 if let Some(secondary) = &self.secondary {
798 if secondary.has_latest_selection {
799 cx.stop_propagation();
800 } else {
801 cx.propagate();
802 }
803 } else {
804 cx.propagate();
805 }
806 }
807
808 fn intercept_inline_assist(
809 &mut self,
810 _: &InlineAssist,
811 _window: &mut Window,
812 cx: &mut Context<Self>,
813 ) {
814 if self.secondary.is_some() {
815 cx.stop_propagation();
816 } else {
817 cx.propagate();
818 }
819 }
820
821 fn toggle_soft_wrap(
822 &mut self,
823 _: &ToggleSoftWrap,
824 window: &mut Window,
825 cx: &mut Context<Self>,
826 ) {
827 if let Some(secondary) = &self.secondary {
828 cx.stop_propagation();
829
830 let is_secondary_focused = secondary.has_latest_selection;
831 let (focused_editor, other_editor) = if is_secondary_focused {
832 (&secondary.editor, &self.primary_editor)
833 } else {
834 (&self.primary_editor, &secondary.editor)
835 };
836
837 // Toggle the focused editor
838 focused_editor.update(cx, |editor, cx| {
839 editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
840 });
841
842 // Copy the soft wrap state from the focused editor to the other editor
843 let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
844 other_editor.update(cx, |editor, cx| {
845 editor.soft_wrap_mode_override = soft_wrap_override;
846 cx.notify();
847 });
848 } else {
849 cx.propagate();
850 }
851 }
852
853 fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
854 let Some(secondary) = self.secondary.take() else {
855 return;
856 };
857 self.panes.remove(&secondary.pane, cx).unwrap();
858 self.primary_editor.update(cx, |primary, cx| {
859 primary.set_on_local_selections_changed(None);
860 primary.set_scroll_companion(None);
861 primary.set_delegate_expand_excerpts(false);
862 primary.buffer().update(cx, |buffer, cx| {
863 buffer.set_show_deleted_hunks(true, cx);
864 buffer.set_use_extended_diff_range(false, cx);
865 });
866 primary.display_map.update(cx, |dm, cx| {
867 dm.set_companion(None, cx);
868 });
869 });
870 secondary.editor.update(cx, |editor, _cx| {
871 editor.set_on_local_selections_changed(None);
872 editor.set_scroll_companion(None);
873 });
874 cx.notify();
875 }
876
877 pub fn added_to_workspace(
878 &mut self,
879 workspace: &mut Workspace,
880 window: &mut Window,
881 cx: &mut Context<Self>,
882 ) {
883 self.workspace = workspace.weak_handle();
884 self.primary_editor.update(cx, |primary_editor, cx| {
885 primary_editor.added_to_workspace(workspace, window, cx);
886 });
887 if let Some(secondary) = &self.secondary {
888 secondary.editor.update(cx, |secondary_editor, cx| {
889 secondary_editor.added_to_workspace(workspace, window, cx);
890 });
891 }
892 }
893
894 pub fn set_excerpts_for_path(
895 &mut self,
896 path: PathKey,
897 buffer: Entity<Buffer>,
898 ranges: impl IntoIterator<Item = Range<Point>> + Clone,
899 context_line_count: u32,
900 diff: Entity<BufferDiff>,
901 cx: &mut Context<Self>,
902 ) -> (Vec<Range<Anchor>>, bool) {
903 let primary_display_map = self.primary_editor.read(cx).display_map.clone();
904 let secondary_display_map = self
905 .secondary
906 .as_ref()
907 .map(|s| s.editor.read(cx).display_map.clone());
908
909 let (anchors, added_a_new_excerpt) =
910 self.primary_multibuffer
911 .update(cx, |primary_multibuffer, cx| {
912 let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
913 path.clone(),
914 buffer.clone(),
915 ranges,
916 context_line_count,
917 cx,
918 );
919 if !anchors.is_empty()
920 && primary_multibuffer
921 .diff_for(buffer.read(cx).remote_id())
922 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
923 {
924 primary_multibuffer.add_diff(diff.clone(), cx);
925 }
926 (anchors, added_a_new_excerpt)
927 });
928
929 if let Some(secondary) = &mut self.secondary {
930 if let Some(secondary_display_map) = &secondary_display_map {
931 secondary.sync_path_excerpts(
932 path,
933 &self.primary_multibuffer,
934 diff,
935 &primary_display_map,
936 secondary_display_map,
937 cx,
938 );
939 }
940 }
941
942 (anchors, added_a_new_excerpt)
943 }
944
945 fn expand_excerpts(
946 &mut self,
947 excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
948 lines: u32,
949 direction: ExpandExcerptDirection,
950 cx: &mut Context<Self>,
951 ) {
952 let mut corresponding_paths = HashMap::default();
953 self.primary_multibuffer.update(cx, |multibuffer, cx| {
954 let snapshot = multibuffer.snapshot(cx);
955 if self.secondary.is_some() {
956 corresponding_paths = excerpt_ids
957 .clone()
958 .map(|excerpt_id| {
959 let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
960 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
961 let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
962 (path, diff)
963 })
964 .collect::<HashMap<_, _>>();
965 }
966 multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
967 });
968
969 if let Some(secondary) = &mut self.secondary {
970 let primary_display_map = self.primary_editor.read(cx).display_map.clone();
971 let secondary_display_map = secondary.editor.read(cx).display_map.clone();
972 for (path, diff) in corresponding_paths {
973 secondary.sync_path_excerpts(
974 path,
975 &self.primary_multibuffer,
976 diff,
977 &primary_display_map,
978 &secondary_display_map,
979 cx,
980 );
981 }
982 }
983 }
984
985 pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
986 self.primary_multibuffer.update(cx, |buffer, cx| {
987 buffer.remove_excerpts_for_path(path.clone(), cx)
988 });
989 if let Some(secondary) = &self.secondary {
990 let primary_display_map = self.primary_editor.read(cx).display_map.clone();
991 let secondary_display_map = secondary.editor.read(cx).display_map.clone();
992 secondary.remove_mappings_for_path(
993 &path,
994 &self.primary_multibuffer,
995 &primary_display_map,
996 &secondary_display_map,
997 cx,
998 );
999 secondary
1000 .multibuffer
1001 .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
1002 }
1003 }
1004}
1005
1006#[cfg(test)]
1007impl SplittableEditor {
1008 fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1009 use multi_buffer::MultiBufferRow;
1010 use text::Bias;
1011
1012 use crate::display_map::Block;
1013 use crate::display_map::DisplayRow;
1014
1015 self.debug_print(cx);
1016
1017 let secondary = self.secondary.as_ref().unwrap();
1018 let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
1019 let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
1020 assert_eq!(
1021 secondary_excerpts.len(),
1022 primary_excerpts.len(),
1023 "mismatch in excerpt count"
1024 );
1025
1026 if quiesced {
1027 let rhs_snapshot = secondary
1028 .editor
1029 .update(cx, |editor, cx| editor.display_snapshot(cx));
1030 let lhs_snapshot = self
1031 .primary_editor
1032 .update(cx, |editor, cx| editor.display_snapshot(cx));
1033
1034 let lhs_max_row = lhs_snapshot.max_point().row();
1035 let rhs_max_row = rhs_snapshot.max_point().row();
1036 assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1037
1038 let lhs_excerpt_block_rows = lhs_snapshot
1039 .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1040 .filter(|(_, block)| {
1041 matches!(
1042 block,
1043 Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1044 )
1045 })
1046 .map(|(row, _)| row)
1047 .collect::<Vec<_>>();
1048 let rhs_excerpt_block_rows = rhs_snapshot
1049 .blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
1050 .filter(|(_, block)| {
1051 matches!(
1052 block,
1053 Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1054 )
1055 })
1056 .map(|(row, _)| row)
1057 .collect::<Vec<_>>();
1058 assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1059
1060 for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1061 assert_eq!(
1062 lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1063 "mismatch in hunks"
1064 );
1065 assert_eq!(
1066 lhs_hunk.status, rhs_hunk.status,
1067 "mismatch in hunk statuses"
1068 );
1069
1070 let (lhs_point, rhs_point) =
1071 if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1072 (
1073 Point::new(lhs_hunk.row_range.end.0, 0),
1074 Point::new(rhs_hunk.row_range.end.0, 0),
1075 )
1076 } else {
1077 (
1078 Point::new(lhs_hunk.row_range.start.0, 0),
1079 Point::new(rhs_hunk.row_range.start.0, 0),
1080 )
1081 };
1082 let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1083 let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1084 assert_eq!(
1085 lhs_point.row(),
1086 rhs_point.row(),
1087 "mismatch in hunk position"
1088 );
1089 }
1090
1091 // Filtering out empty lines is a bit of a hack, to work around a case where
1092 // the base text has a trailing newline but the current text doesn't, or vice versa.
1093 // In this case, we get the additional newline on one side, but that line is not
1094 // marked as added/deleted by rowinfos.
1095 self.check_sides_match(cx, |snapshot| {
1096 snapshot
1097 .buffer_snapshot()
1098 .text()
1099 .split("\n")
1100 .zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
1101 .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
1102 .map(|(line, _)| line.to_owned())
1103 .collect::<Vec<_>>()
1104 });
1105 }
1106 }
1107
1108 #[track_caller]
1109 fn check_sides_match<T: std::fmt::Debug + PartialEq>(
1110 &self,
1111 cx: &mut App,
1112 mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
1113 ) {
1114 let secondary = self.secondary.as_ref().expect("requires split");
1115 let primary_snapshot = self.primary_editor.update(cx, |editor, cx| {
1116 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1117 });
1118 let secondary_snapshot = secondary.editor.update(cx, |editor, cx| {
1119 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1120 });
1121
1122 let primary_t = extract(&primary_snapshot);
1123 let secondary_t = extract(&secondary_snapshot);
1124
1125 if primary_t != secondary_t {
1126 self.debug_print(cx);
1127 pretty_assertions::assert_eq!(primary_t, secondary_t);
1128 }
1129 }
1130
1131 fn debug_print(&self, cx: &mut App) {
1132 use crate::DisplayRow;
1133 use crate::display_map::Block;
1134 use buffer_diff::DiffHunkStatusKind;
1135
1136 assert!(
1137 self.secondary.is_some(),
1138 "debug_print is only useful when secondary editor exists"
1139 );
1140
1141 let secondary = self.secondary.as_ref().unwrap();
1142
1143 // Get terminal width, default to 80 if unavailable
1144 let terminal_width = std::env::var("COLUMNS")
1145 .ok()
1146 .and_then(|s| s.parse::<usize>().ok())
1147 .unwrap_or(80);
1148
1149 // Each side gets half the terminal width minus the separator
1150 let separator = " │ ";
1151 let side_width = (terminal_width - separator.len()) / 2;
1152
1153 // Get display snapshots for both editors
1154 let secondary_snapshot = secondary.editor.update(cx, |editor, cx| {
1155 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1156 });
1157 let primary_snapshot = self.primary_editor.update(cx, |editor, cx| {
1158 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1159 });
1160
1161 let secondary_max_row = secondary_snapshot.max_point().row().0;
1162 let primary_max_row = primary_snapshot.max_point().row().0;
1163 let max_row = secondary_max_row.max(primary_max_row);
1164
1165 // Build a map from display row -> block type string
1166 // Each row of a multi-row block gets an entry with the same block type
1167 // For spacers, the ID is included in brackets
1168 fn build_block_map(
1169 snapshot: &crate::DisplaySnapshot,
1170 max_row: u32,
1171 ) -> std::collections::HashMap<u32, String> {
1172 let mut block_map = std::collections::HashMap::new();
1173 for (start_row, block) in
1174 snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1175 {
1176 let (block_type, height) = match block {
1177 Block::Spacer {
1178 id,
1179 height,
1180 is_below: _,
1181 } => (format!("SPACER[{}]", id.0), *height),
1182 Block::ExcerptBoundary { height, .. } => {
1183 ("EXCERPT_BOUNDARY".to_string(), *height)
1184 }
1185 Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1186 Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1187 Block::Custom(custom) => {
1188 ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1189 }
1190 };
1191 for offset in 0..height {
1192 block_map.insert(start_row.0 + offset, block_type.clone());
1193 }
1194 }
1195 block_map
1196 }
1197
1198 let secondary_blocks = build_block_map(&secondary_snapshot, secondary_max_row);
1199 let primary_blocks = build_block_map(&primary_snapshot, primary_max_row);
1200
1201 fn display_width(s: &str) -> usize {
1202 unicode_width::UnicodeWidthStr::width(s)
1203 }
1204
1205 fn truncate_line(line: &str, max_width: usize) -> String {
1206 let line_width = display_width(line);
1207 if line_width <= max_width {
1208 return line.to_string();
1209 }
1210 if max_width < 9 {
1211 let mut result = String::new();
1212 let mut width = 0;
1213 for c in line.chars() {
1214 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1215 if width + c_width > max_width {
1216 break;
1217 }
1218 result.push(c);
1219 width += c_width;
1220 }
1221 return result;
1222 }
1223 let ellipsis = "...";
1224 let target_prefix_width = 3;
1225 let target_suffix_width = 3;
1226
1227 let mut prefix = String::new();
1228 let mut prefix_width = 0;
1229 for c in line.chars() {
1230 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1231 if prefix_width + c_width > target_prefix_width {
1232 break;
1233 }
1234 prefix.push(c);
1235 prefix_width += c_width;
1236 }
1237
1238 let mut suffix_chars: Vec<char> = Vec::new();
1239 let mut suffix_width = 0;
1240 for c in line.chars().rev() {
1241 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1242 if suffix_width + c_width > target_suffix_width {
1243 break;
1244 }
1245 suffix_chars.push(c);
1246 suffix_width += c_width;
1247 }
1248 suffix_chars.reverse();
1249 let suffix: String = suffix_chars.into_iter().collect();
1250
1251 format!("{}{}{}", prefix, ellipsis, suffix)
1252 }
1253
1254 fn pad_to_width(s: &str, target_width: usize) -> String {
1255 let current_width = display_width(s);
1256 if current_width >= target_width {
1257 s.to_string()
1258 } else {
1259 format!("{}{}", s, " ".repeat(target_width - current_width))
1260 }
1261 }
1262
1263 // Helper to format a single row for one side
1264 // Format: "ln# diff bytes(cumul) text" or block info
1265 // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1266 fn format_row(
1267 row: u32,
1268 max_row: u32,
1269 snapshot: &crate::DisplaySnapshot,
1270 blocks: &std::collections::HashMap<u32, String>,
1271 row_infos: &[multi_buffer::RowInfo],
1272 cumulative_bytes: &[usize],
1273 side_width: usize,
1274 ) -> String {
1275 // Get row info if available
1276 let row_info = row_infos.get(row as usize);
1277
1278 // Line number prefix (3 chars + space)
1279 // Use buffer_row from RowInfo, which is None for block rows
1280 let line_prefix = if row > max_row {
1281 " ".to_string()
1282 } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1283 format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1284 } else {
1285 " ".to_string() // block rows have no line number
1286 };
1287 let content_width = side_width.saturating_sub(line_prefix.len());
1288
1289 if row > max_row {
1290 return format!("{}{}", line_prefix, " ".repeat(content_width));
1291 }
1292
1293 // Check if this row is a block row
1294 if let Some(block_type) = blocks.get(&row) {
1295 let block_str = format!("~~~[{}]~~~", block_type);
1296 let formatted = format!("{:^width$}", block_str, width = content_width);
1297 return format!(
1298 "{}{}",
1299 line_prefix,
1300 truncate_line(&formatted, content_width)
1301 );
1302 }
1303
1304 // Get line text
1305 let line_text = snapshot.line(DisplayRow(row));
1306 let line_bytes = line_text.len();
1307
1308 // Diff status marker
1309 let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1310 Some(status) => match status.kind {
1311 DiffHunkStatusKind::Added => "+",
1312 DiffHunkStatusKind::Deleted => "-",
1313 DiffHunkStatusKind::Modified => "~",
1314 },
1315 None => " ",
1316 };
1317
1318 // Cumulative bytes
1319 let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1320
1321 // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1322 let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1323 let text_width = content_width.saturating_sub(info_prefix.len());
1324 let truncated_text = truncate_line(&line_text, text_width);
1325
1326 let text_part = pad_to_width(&truncated_text, text_width);
1327 format!("{}{}{}", line_prefix, info_prefix, text_part)
1328 }
1329
1330 // Collect row infos for both sides
1331 let secondary_row_infos: Vec<_> = secondary_snapshot
1332 .row_infos(DisplayRow(0))
1333 .take((secondary_max_row + 1) as usize)
1334 .collect();
1335 let primary_row_infos: Vec<_> = primary_snapshot
1336 .row_infos(DisplayRow(0))
1337 .take((primary_max_row + 1) as usize)
1338 .collect();
1339
1340 // Calculate cumulative bytes for each side (only counting non-block rows)
1341 let mut secondary_cumulative = Vec::with_capacity((secondary_max_row + 1) as usize);
1342 let mut cumulative = 0usize;
1343 for row in 0..=secondary_max_row {
1344 if !secondary_blocks.contains_key(&row) {
1345 cumulative += secondary_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1346 }
1347 secondary_cumulative.push(cumulative);
1348 }
1349
1350 let mut primary_cumulative = Vec::with_capacity((primary_max_row + 1) as usize);
1351 cumulative = 0;
1352 for row in 0..=primary_max_row {
1353 if !primary_blocks.contains_key(&row) {
1354 cumulative += primary_snapshot.line(DisplayRow(row)).len() + 1;
1355 }
1356 primary_cumulative.push(cumulative);
1357 }
1358
1359 // Print header
1360 eprintln!();
1361 eprintln!("{}", "═".repeat(terminal_width));
1362 let header_left = format!("{:^width$}", "SECONDARY (LEFT)", width = side_width);
1363 let header_right = format!("{:^width$}", "PRIMARY (RIGHT)", width = side_width);
1364 eprintln!("{}{}{}", header_left, separator, header_right);
1365 eprintln!(
1366 "{:^width$}{}{:^width$}",
1367 "ln# diff len(cum) text",
1368 separator,
1369 "ln# diff len(cum) text",
1370 width = side_width
1371 );
1372 eprintln!("{}", "─".repeat(terminal_width));
1373
1374 // Print each row
1375 for row in 0..=max_row {
1376 let left = format_row(
1377 row,
1378 secondary_max_row,
1379 &secondary_snapshot,
1380 &secondary_blocks,
1381 &secondary_row_infos,
1382 &secondary_cumulative,
1383 side_width,
1384 );
1385 let right = format_row(
1386 row,
1387 primary_max_row,
1388 &primary_snapshot,
1389 &primary_blocks,
1390 &primary_row_infos,
1391 &primary_cumulative,
1392 side_width,
1393 );
1394 eprintln!("{}{}{}", left, separator, right);
1395 }
1396
1397 eprintln!("{}", "═".repeat(terminal_width));
1398 eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1399 eprintln!();
1400 }
1401
1402 fn randomly_edit_excerpts(
1403 &mut self,
1404 rng: &mut impl rand::Rng,
1405 mutation_count: usize,
1406 cx: &mut Context<Self>,
1407 ) {
1408 use collections::HashSet;
1409 use rand::prelude::*;
1410 use std::env;
1411 use util::RandomCharIter;
1412
1413 let max_buffers = env::var("MAX_BUFFERS")
1414 .map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
1415 .unwrap_or(4);
1416
1417 for _ in 0..mutation_count {
1418 let paths = self
1419 .primary_multibuffer
1420 .read(cx)
1421 .paths()
1422 .cloned()
1423 .collect::<Vec<_>>();
1424 let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
1425
1426 if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
1427 let mut excerpts = HashSet::default();
1428 for _ in 0..rng.random_range(0..excerpt_ids.len()) {
1429 excerpts.extend(excerpt_ids.choose(rng).copied());
1430 }
1431
1432 let line_count = rng.random_range(1..5);
1433
1434 log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
1435
1436 self.expand_excerpts(
1437 excerpts.iter().cloned(),
1438 line_count,
1439 ExpandExcerptDirection::UpAndDown,
1440 cx,
1441 );
1442 continue;
1443 }
1444
1445 if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
1446 let len = rng.random_range(100..500);
1447 let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
1448 let buffer = cx.new(|cx| Buffer::local(text, cx));
1449 log::info!(
1450 "Creating new buffer {} with text: {:?}",
1451 buffer.read(cx).remote_id(),
1452 buffer.read(cx).text()
1453 );
1454 let buffer_snapshot = buffer.read(cx).snapshot();
1455 let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
1456 // Create some initial diff hunks.
1457 buffer.update(cx, |buffer, cx| {
1458 buffer.randomly_edit(rng, 1, cx);
1459 });
1460 let buffer_snapshot = buffer.read(cx).text_snapshot();
1461 diff.update(cx, |diff, cx| {
1462 diff.recalculate_diff_sync(&buffer_snapshot, cx);
1463 });
1464 let path = PathKey::for_buffer(&buffer, cx);
1465 let ranges = diff.update(cx, |diff, cx| {
1466 diff.snapshot(cx)
1467 .hunks(&buffer_snapshot)
1468 .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
1469 .collect::<Vec<_>>()
1470 });
1471 self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1472 } else {
1473 log::info!("removing excerpts");
1474 let remove_count = rng.random_range(1..=paths.len());
1475 let paths_to_remove = paths
1476 .choose_multiple(rng, remove_count)
1477 .cloned()
1478 .collect::<Vec<_>>();
1479 for path in paths_to_remove {
1480 self.remove_excerpts_for_path(path.clone(), cx);
1481 }
1482 }
1483 }
1484 }
1485}
1486
1487impl EventEmitter<EditorEvent> for SplittableEditor {}
1488impl Focusable for SplittableEditor {
1489 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1490 self.primary_editor.read(cx).focus_handle(cx)
1491 }
1492}
1493
1494impl Render for SplittableEditor {
1495 fn render(
1496 &mut self,
1497 _window: &mut ui::Window,
1498 cx: &mut ui::Context<Self>,
1499 ) -> impl ui::IntoElement {
1500 let inner = if self.secondary.is_some() {
1501 let style = self.primary_editor.read(cx).create_style(cx);
1502 SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
1503 } else {
1504 self.primary_editor.clone().into_any_element()
1505 };
1506 div()
1507 .id("splittable-editor")
1508 .on_action(cx.listener(Self::split))
1509 .on_action(cx.listener(Self::unsplit))
1510 .on_action(cx.listener(Self::toggle_split))
1511 .on_action(cx.listener(Self::activate_pane_left))
1512 .on_action(cx.listener(Self::activate_pane_right))
1513 .on_action(cx.listener(Self::toggle_locked_cursors))
1514 .on_action(cx.listener(Self::intercept_toggle_code_actions))
1515 .on_action(cx.listener(Self::intercept_toggle_breakpoint))
1516 .on_action(cx.listener(Self::intercept_enable_breakpoint))
1517 .on_action(cx.listener(Self::intercept_disable_breakpoint))
1518 .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
1519 .on_action(cx.listener(Self::intercept_inline_assist))
1520 .capture_action(cx.listener(Self::toggle_soft_wrap))
1521 .size_full()
1522 .child(inner)
1523 }
1524}
1525
1526impl SecondaryEditor {
1527 fn update_path_excerpts_from_primary(
1528 &mut self,
1529 path_key: PathKey,
1530 primary_multibuffer: &Entity<MultiBuffer>,
1531 diff: Entity<BufferDiff>,
1532 cx: &mut App,
1533 ) -> Vec<(ExcerptId, ExcerptId)> {
1534 let primary_multibuffer_ref = primary_multibuffer.read(cx);
1535 let primary_excerpt_ids: Vec<ExcerptId> = primary_multibuffer_ref
1536 .excerpts_for_path(&path_key)
1537 .collect();
1538
1539 let Some(excerpt_id) = primary_multibuffer_ref.excerpts_for_path(&path_key).next() else {
1540 self.multibuffer.update(cx, |multibuffer, cx| {
1541 multibuffer.remove_excerpts_for_path(path_key, cx);
1542 });
1543 return Vec::new();
1544 };
1545
1546 let primary_multibuffer_snapshot = primary_multibuffer_ref.snapshot(cx);
1547 let main_buffer = primary_multibuffer_snapshot
1548 .buffer_for_excerpt(excerpt_id)
1549 .unwrap();
1550 let base_text_buffer = diff.read(cx).base_text_buffer();
1551 let diff_snapshot = diff.read(cx).snapshot(cx);
1552 let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
1553 let new = primary_multibuffer_ref
1554 .excerpts_for_buffer(main_buffer.remote_id(), cx)
1555 .into_iter()
1556 .map(|(_, excerpt_range)| {
1557 let point_range_to_base_text_point_range = |range: Range<Point>| {
1558 let (mut translated, _, _) = diff_snapshot.points_to_base_text_points(
1559 [Point::new(range.start.row, 0), Point::new(range.end.row, 0)],
1560 main_buffer,
1561 );
1562 let start_row = translated.next().unwrap().start.row;
1563 let end_row = translated.next().unwrap().end.row;
1564 let end_column = diff_snapshot.base_text().line_len(end_row);
1565 Point::new(start_row, 0)..Point::new(end_row, end_column)
1566 };
1567 let primary = excerpt_range.primary.to_point(main_buffer);
1568 let context = excerpt_range.context.to_point(main_buffer);
1569 ExcerptRange {
1570 primary: point_range_to_base_text_point_range(primary),
1571 context: point_range_to_base_text_point_range(context),
1572 }
1573 })
1574 .collect();
1575
1576 let main_buffer = primary_multibuffer_ref
1577 .buffer(main_buffer.remote_id())
1578 .unwrap();
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, main_buffer, 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}