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 let main_buffer = primary_multibuffer_ref
1581 .buffer(main_buffer.remote_id())
1582 .unwrap();
1583
1584 self.editor.update(cx, |editor, cx| {
1585 editor.buffer().update(cx, |buffer, cx| {
1586 let (ids, _) = buffer.update_path_excerpts(
1587 path_key.clone(),
1588 base_text_buffer.clone(),
1589 &base_text_buffer_snapshot,
1590 new,
1591 cx,
1592 );
1593 if !ids.is_empty()
1594 && buffer
1595 .diff_for(base_text_buffer.read(cx).remote_id())
1596 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1597 {
1598 buffer.add_inverted_diff(diff, main_buffer, cx);
1599 }
1600 })
1601 });
1602
1603 let secondary_excerpt_ids: Vec<ExcerptId> = self
1604 .multibuffer
1605 .read(cx)
1606 .excerpts_for_path(&path_key)
1607 .collect();
1608
1609 debug_assert_eq!(primary_excerpt_ids.len(), secondary_excerpt_ids.len());
1610
1611 secondary_excerpt_ids
1612 .into_iter()
1613 .zip(primary_excerpt_ids)
1614 .collect()
1615 }
1616
1617 fn sync_path_excerpts(
1618 &mut self,
1619 path_key: PathKey,
1620 primary_multibuffer: &Entity<MultiBuffer>,
1621 diff: Entity<BufferDiff>,
1622 primary_display_map: &Entity<DisplayMap>,
1623 secondary_display_map: &Entity<DisplayMap>,
1624 cx: &mut App,
1625 ) {
1626 self.remove_mappings_for_path(
1627 &path_key,
1628 primary_multibuffer,
1629 primary_display_map,
1630 secondary_display_map,
1631 cx,
1632 );
1633
1634 let mappings =
1635 self.update_path_excerpts_from_primary(path_key, primary_multibuffer, diff.clone(), cx);
1636
1637 let secondary_buffer_id = diff.read(cx).base_text(cx).remote_id();
1638 let primary_buffer_id = diff.read(cx).buffer_id;
1639
1640 if let Some(companion) = primary_display_map.read(cx).companion().cloned() {
1641 companion.update(cx, |c, _| {
1642 for (lhs, rhs) in mappings {
1643 c.add_excerpt_mapping(lhs, rhs);
1644 }
1645 c.add_buffer_mapping(secondary_buffer_id, primary_buffer_id);
1646 });
1647 }
1648 }
1649
1650 fn remove_mappings_for_path(
1651 &self,
1652 path_key: &PathKey,
1653 primary_multibuffer: &Entity<MultiBuffer>,
1654 primary_display_map: &Entity<DisplayMap>,
1655 _secondary_display_map: &Entity<DisplayMap>,
1656 cx: &mut App,
1657 ) {
1658 let primary_excerpt_ids: Vec<ExcerptId> = primary_multibuffer
1659 .read(cx)
1660 .excerpts_for_path(path_key)
1661 .collect();
1662 let secondary_excerpt_ids: Vec<ExcerptId> = self
1663 .multibuffer
1664 .read(cx)
1665 .excerpts_for_path(path_key)
1666 .collect();
1667
1668 if let Some(companion) = primary_display_map.read(cx).companion().cloned() {
1669 companion.update(cx, |c, _| {
1670 c.remove_excerpt_mappings(secondary_excerpt_ids, primary_excerpt_ids);
1671 });
1672 }
1673 }
1674}
1675
1676#[cfg(test)]
1677mod tests {
1678 use buffer_diff::BufferDiff;
1679 use fs::FakeFs;
1680 use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
1681 use language::language_settings::SoftWrap;
1682 use language::{Buffer, Capability};
1683 use multi_buffer::{MultiBuffer, PathKey};
1684 use pretty_assertions::assert_eq;
1685 use project::Project;
1686 use rand::rngs::StdRng;
1687 use settings::SettingsStore;
1688 use ui::{VisualContext as _, px};
1689 use workspace::Workspace;
1690
1691 use crate::SplittableEditor;
1692 use crate::test::editor_content_with_blocks_and_width;
1693
1694 async fn init_test(
1695 cx: &mut gpui::TestAppContext,
1696 ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
1697 cx.update(|cx| {
1698 let store = SettingsStore::test(cx);
1699 cx.set_global(store);
1700 theme::init(theme::LoadThemes::JustBase, cx);
1701 crate::init(cx);
1702 });
1703 let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
1704 let (workspace, cx) =
1705 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1706 let primary_multibuffer = cx.new(|cx| {
1707 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1708 multibuffer.set_all_diff_hunks_expanded(cx);
1709 multibuffer
1710 });
1711 let editor = cx.new_window_entity(|window, cx| {
1712 let mut editor = SplittableEditor::new_unsplit(
1713 primary_multibuffer.clone(),
1714 project.clone(),
1715 workspace,
1716 window,
1717 cx,
1718 );
1719 editor.split(&Default::default(), window, cx);
1720 editor.primary_editor.update(cx, |editor, cx| {
1721 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1722 });
1723 editor
1724 .secondary
1725 .as_ref()
1726 .unwrap()
1727 .editor
1728 .update(cx, |editor, cx| {
1729 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1730 });
1731 editor
1732 });
1733 (editor, cx)
1734 }
1735
1736 fn buffer_with_diff(
1737 base_text: &str,
1738 current_text: &str,
1739 cx: &mut VisualTestContext,
1740 ) -> (Entity<Buffer>, Entity<BufferDiff>) {
1741 let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
1742 let diff = cx.new(|cx| {
1743 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
1744 });
1745 (buffer, diff)
1746 }
1747
1748 #[track_caller]
1749 fn assert_split_content(
1750 editor: &Entity<SplittableEditor>,
1751 expected_primary: String,
1752 expected_secondary: String,
1753 cx: &mut VisualTestContext,
1754 ) {
1755 assert_split_content_with_widths(
1756 editor,
1757 px(3000.0),
1758 px(3000.0),
1759 expected_primary,
1760 expected_secondary,
1761 cx,
1762 );
1763 }
1764
1765 #[track_caller]
1766 fn assert_split_content_with_widths(
1767 editor: &Entity<SplittableEditor>,
1768 primary_width: Pixels,
1769 secondary_width: Pixels,
1770 expected_primary: String,
1771 expected_secondary: String,
1772 cx: &mut VisualTestContext,
1773 ) {
1774 let (primary_editor, secondary_editor) = editor.update(cx, |editor, _cx| {
1775 let secondary = editor
1776 .secondary
1777 .as_ref()
1778 .expect("should have secondary editor");
1779 (editor.primary_editor.clone(), secondary.editor.clone())
1780 });
1781
1782 // Make sure both sides learn if the other has soft-wrapped
1783 let _ = editor_content_with_blocks_and_width(&primary_editor, primary_width, cx);
1784 cx.run_until_parked();
1785 let _ = editor_content_with_blocks_and_width(&secondary_editor, secondary_width, cx);
1786 cx.run_until_parked();
1787
1788 let primary_content =
1789 editor_content_with_blocks_and_width(&primary_editor, primary_width, cx);
1790 let secondary_content =
1791 editor_content_with_blocks_and_width(&secondary_editor, secondary_width, cx);
1792
1793 if primary_content != expected_primary || secondary_content != expected_secondary {
1794 editor.update(cx, |editor, cx| editor.debug_print(cx));
1795 }
1796
1797 assert_eq!(primary_content, expected_primary, "rhs");
1798 assert_eq!(secondary_content, expected_secondary, "lhs");
1799 }
1800
1801 #[gpui::test(iterations = 100)]
1802 async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
1803 use rand::prelude::*;
1804
1805 let (editor, cx) = init_test(cx).await;
1806 let operations = std::env::var("OPERATIONS")
1807 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
1808 .unwrap_or(10);
1809 let rng = &mut rng;
1810 for _ in 0..operations {
1811 let buffers = editor.update(cx, |editor, cx| {
1812 editor
1813 .primary_editor
1814 .read(cx)
1815 .buffer()
1816 .read(cx)
1817 .all_buffers()
1818 });
1819
1820 if buffers.is_empty() {
1821 log::info!("adding excerpts to empty multibuffer");
1822 editor.update(cx, |editor, cx| {
1823 editor.randomly_edit_excerpts(rng, 2, cx);
1824 editor.check_invariants(true, cx);
1825 });
1826 continue;
1827 }
1828
1829 let mut quiesced = false;
1830
1831 match rng.random_range(0..100) {
1832 0..=44 => {
1833 log::info!("randomly editing multibuffer");
1834 editor.update(cx, |editor, cx| {
1835 editor.primary_multibuffer.update(cx, |multibuffer, cx| {
1836 multibuffer.randomly_edit(rng, 5, cx);
1837 })
1838 })
1839 }
1840 45..=64 => {
1841 log::info!("randomly undoing/redoing in single buffer");
1842 let buffer = buffers.iter().choose(rng).unwrap();
1843 buffer.update(cx, |buffer, cx| {
1844 buffer.randomly_undo_redo(rng, cx);
1845 });
1846 }
1847 65..=79 => {
1848 log::info!("mutating excerpts");
1849 editor.update(cx, |editor, cx| {
1850 editor.randomly_edit_excerpts(rng, 2, cx);
1851 });
1852 }
1853 _ => {
1854 log::info!("quiescing");
1855 for buffer in buffers {
1856 let buffer_snapshot =
1857 buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
1858 let diff = editor.update(cx, |editor, cx| {
1859 editor
1860 .primary_multibuffer
1861 .read(cx)
1862 .diff_for(buffer.read(cx).remote_id())
1863 .unwrap()
1864 });
1865 diff.update(cx, |diff, cx| {
1866 diff.recalculate_diff_sync(&buffer_snapshot, cx);
1867 });
1868 cx.run_until_parked();
1869 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
1870 let ranges = diff_snapshot
1871 .hunks(&buffer_snapshot)
1872 .map(|hunk| hunk.range)
1873 .collect::<Vec<_>>();
1874 editor.update(cx, |editor, cx| {
1875 let path = PathKey::for_buffer(&buffer, cx);
1876 editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1877 });
1878 }
1879 quiesced = true;
1880 }
1881 }
1882
1883 editor.update(cx, |editor, cx| {
1884 editor.check_invariants(quiesced, cx);
1885 });
1886 }
1887 }
1888
1889 #[gpui::test]
1890 async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
1891 use rope::Point;
1892 use unindent::Unindent as _;
1893
1894 let (editor, mut cx) = init_test(cx).await;
1895
1896 let base_text = "
1897 aaa
1898 bbb
1899 ccc
1900 ddd
1901 eee
1902 fff
1903 "
1904 .unindent();
1905 let current_text = "
1906 aaa
1907 ddd
1908 eee
1909 fff
1910 "
1911 .unindent();
1912
1913 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
1914
1915 editor.update(cx, |editor, cx| {
1916 let path = PathKey::for_buffer(&buffer, cx);
1917 editor.set_excerpts_for_path(
1918 path,
1919 buffer.clone(),
1920 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
1921 0,
1922 diff.clone(),
1923 cx,
1924 );
1925 });
1926
1927 cx.run_until_parked();
1928
1929 assert_split_content(
1930 &editor,
1931 "
1932 § <no file>
1933 § -----
1934 aaa
1935 § spacer
1936 § spacer
1937 ddd
1938 eee
1939 fff"
1940 .unindent(),
1941 "
1942 § <no file>
1943 § -----
1944 aaa
1945 bbb
1946 ccc
1947 ddd
1948 eee
1949 fff"
1950 .unindent(),
1951 &mut cx,
1952 );
1953
1954 buffer.update(cx, |buffer, cx| {
1955 buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
1956 });
1957
1958 cx.run_until_parked();
1959
1960 assert_split_content(
1961 &editor,
1962 "
1963 § <no file>
1964 § -----
1965 aaa
1966 § spacer
1967 § spacer
1968 ddd
1969 eee
1970 FFF"
1971 .unindent(),
1972 "
1973 § <no file>
1974 § -----
1975 aaa
1976 bbb
1977 ccc
1978 ddd
1979 eee
1980 fff"
1981 .unindent(),
1982 &mut cx,
1983 );
1984
1985 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
1986 diff.update(cx, |diff, cx| {
1987 diff.recalculate_diff_sync(&buffer_snapshot, cx);
1988 });
1989
1990 cx.run_until_parked();
1991
1992 assert_split_content(
1993 &editor,
1994 "
1995 § <no file>
1996 § -----
1997 aaa
1998 § spacer
1999 § spacer
2000 ddd
2001 eee
2002 FFF"
2003 .unindent(),
2004 "
2005 § <no file>
2006 § -----
2007 aaa
2008 bbb
2009 ccc
2010 ddd
2011 eee
2012 fff"
2013 .unindent(),
2014 &mut cx,
2015 );
2016 }
2017
2018 #[gpui::test]
2019 async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2020 use rope::Point;
2021 use unindent::Unindent as _;
2022
2023 let (editor, mut cx) = init_test(cx).await;
2024
2025 let base_text1 = "
2026 aaa
2027 bbb
2028 ccc
2029 ddd
2030 eee"
2031 .unindent();
2032
2033 let base_text2 = "
2034 fff
2035 ggg
2036 hhh
2037 iii
2038 jjj"
2039 .unindent();
2040
2041 let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2042 let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2043
2044 editor.update(cx, |editor, cx| {
2045 let path1 = PathKey::for_buffer(&buffer1, cx);
2046 editor.set_excerpts_for_path(
2047 path1,
2048 buffer1.clone(),
2049 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2050 0,
2051 diff1.clone(),
2052 cx,
2053 );
2054 let path2 = PathKey::for_buffer(&buffer2, cx);
2055 editor.set_excerpts_for_path(
2056 path2,
2057 buffer2.clone(),
2058 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2059 1,
2060 diff2.clone(),
2061 cx,
2062 );
2063 });
2064
2065 cx.run_until_parked();
2066
2067 buffer1.update(cx, |buffer, cx| {
2068 buffer.edit(
2069 [
2070 (Point::new(0, 0)..Point::new(1, 0), ""),
2071 (Point::new(3, 0)..Point::new(4, 0), ""),
2072 ],
2073 None,
2074 cx,
2075 );
2076 });
2077 buffer2.update(cx, |buffer, cx| {
2078 buffer.edit(
2079 [
2080 (Point::new(0, 0)..Point::new(1, 0), ""),
2081 (Point::new(3, 0)..Point::new(4, 0), ""),
2082 ],
2083 None,
2084 cx,
2085 );
2086 });
2087
2088 cx.run_until_parked();
2089
2090 assert_split_content(
2091 &editor,
2092 "
2093 § <no file>
2094 § -----
2095 § spacer
2096 bbb
2097 ccc
2098 § spacer
2099 eee
2100 § <no file>
2101 § -----
2102 § spacer
2103 ggg
2104 hhh
2105 § spacer
2106 jjj"
2107 .unindent(),
2108 "
2109 § <no file>
2110 § -----
2111 aaa
2112 bbb
2113 ccc
2114 ddd
2115 eee
2116 § <no file>
2117 § -----
2118 fff
2119 ggg
2120 hhh
2121 iii
2122 jjj"
2123 .unindent(),
2124 &mut cx,
2125 );
2126
2127 let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2128 diff1.update(cx, |diff, cx| {
2129 diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2130 });
2131 let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2132 diff2.update(cx, |diff, cx| {
2133 diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2134 });
2135
2136 cx.run_until_parked();
2137
2138 assert_split_content(
2139 &editor,
2140 "
2141 § <no file>
2142 § -----
2143 § spacer
2144 bbb
2145 ccc
2146 § spacer
2147 eee
2148 § <no file>
2149 § -----
2150 § spacer
2151 ggg
2152 hhh
2153 § spacer
2154 jjj"
2155 .unindent(),
2156 "
2157 § <no file>
2158 § -----
2159 aaa
2160 bbb
2161 ccc
2162 ddd
2163 eee
2164 § <no file>
2165 § -----
2166 fff
2167 ggg
2168 hhh
2169 iii
2170 jjj"
2171 .unindent(),
2172 &mut cx,
2173 );
2174 }
2175
2176 #[gpui::test]
2177 async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2178 use rope::Point;
2179 use unindent::Unindent as _;
2180
2181 let (editor, mut cx) = init_test(cx).await;
2182
2183 let base_text = "
2184 aaa
2185 bbb
2186 ccc
2187 ddd
2188 "
2189 .unindent();
2190
2191 let current_text = "
2192 aaa
2193 NEW1
2194 NEW2
2195 ccc
2196 ddd
2197 "
2198 .unindent();
2199
2200 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2201
2202 editor.update(cx, |editor, cx| {
2203 let path = PathKey::for_buffer(&buffer, cx);
2204 editor.set_excerpts_for_path(
2205 path,
2206 buffer.clone(),
2207 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2208 0,
2209 diff.clone(),
2210 cx,
2211 );
2212 });
2213
2214 cx.run_until_parked();
2215
2216 assert_split_content(
2217 &editor,
2218 "
2219 § <no file>
2220 § -----
2221 aaa
2222 NEW1
2223 NEW2
2224 ccc
2225 ddd"
2226 .unindent(),
2227 "
2228 § <no file>
2229 § -----
2230 aaa
2231 bbb
2232 § spacer
2233 ccc
2234 ddd"
2235 .unindent(),
2236 &mut cx,
2237 );
2238
2239 buffer.update(cx, |buffer, cx| {
2240 buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2241 });
2242
2243 cx.run_until_parked();
2244
2245 assert_split_content(
2246 &editor,
2247 "
2248 § <no file>
2249 § -----
2250 aaa
2251 NEW1
2252 ccc
2253 ddd"
2254 .unindent(),
2255 "
2256 § <no file>
2257 § -----
2258 aaa
2259 bbb
2260 ccc
2261 ddd"
2262 .unindent(),
2263 &mut cx,
2264 );
2265
2266 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2267 diff.update(cx, |diff, cx| {
2268 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2269 });
2270
2271 cx.run_until_parked();
2272
2273 assert_split_content(
2274 &editor,
2275 "
2276 § <no file>
2277 § -----
2278 aaa
2279 NEW1
2280 ccc
2281 ddd"
2282 .unindent(),
2283 "
2284 § <no file>
2285 § -----
2286 aaa
2287 bbb
2288 ccc
2289 ddd"
2290 .unindent(),
2291 &mut cx,
2292 );
2293 }
2294
2295 #[gpui::test]
2296 async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2297 use rope::Point;
2298 use unindent::Unindent as _;
2299
2300 let (editor, mut cx) = init_test(cx).await;
2301
2302 let base_text = "
2303 aaa
2304 bbb
2305
2306
2307
2308
2309
2310 ccc
2311 ddd
2312 "
2313 .unindent();
2314 let current_text = "
2315 aaa
2316 bbb
2317
2318
2319
2320
2321
2322 CCC
2323 ddd
2324 "
2325 .unindent();
2326
2327 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2328
2329 editor.update(cx, |editor, cx| {
2330 let path = PathKey::for_buffer(&buffer, cx);
2331 editor.set_excerpts_for_path(
2332 path,
2333 buffer.clone(),
2334 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2335 0,
2336 diff.clone(),
2337 cx,
2338 );
2339 });
2340
2341 cx.run_until_parked();
2342
2343 buffer.update(cx, |buffer, cx| {
2344 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2345 });
2346
2347 cx.run_until_parked();
2348
2349 assert_split_content(
2350 &editor,
2351 "
2352 § <no file>
2353 § -----
2354 aaa
2355 bbb
2356
2357
2358
2359
2360
2361
2362 CCC
2363 ddd"
2364 .unindent(),
2365 "
2366 § <no file>
2367 § -----
2368 aaa
2369 bbb
2370 § spacer
2371
2372
2373
2374
2375
2376 ccc
2377 ddd"
2378 .unindent(),
2379 &mut cx,
2380 );
2381
2382 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2383 diff.update(cx, |diff, cx| {
2384 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2385 });
2386
2387 cx.run_until_parked();
2388
2389 assert_split_content(
2390 &editor,
2391 "
2392 § <no file>
2393 § -----
2394 aaa
2395 bbb
2396
2397
2398
2399
2400
2401
2402 CCC
2403 ddd"
2404 .unindent(),
2405 "
2406 § <no file>
2407 § -----
2408 aaa
2409 bbb
2410
2411
2412
2413
2414
2415 ccc
2416 § spacer
2417 ddd"
2418 .unindent(),
2419 &mut cx,
2420 );
2421 }
2422
2423 #[gpui::test]
2424 async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2425 use git::Restore;
2426 use rope::Point;
2427 use unindent::Unindent as _;
2428
2429 let (editor, mut cx) = init_test(cx).await;
2430
2431 let base_text = "
2432 aaa
2433 bbb
2434 ccc
2435 ddd
2436 eee
2437 "
2438 .unindent();
2439 let current_text = "
2440 aaa
2441 ddd
2442 eee
2443 "
2444 .unindent();
2445
2446 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2447
2448 editor.update(cx, |editor, cx| {
2449 let path = PathKey::for_buffer(&buffer, cx);
2450 editor.set_excerpts_for_path(
2451 path,
2452 buffer.clone(),
2453 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2454 0,
2455 diff.clone(),
2456 cx,
2457 );
2458 });
2459
2460 cx.run_until_parked();
2461
2462 assert_split_content(
2463 &editor,
2464 "
2465 § <no file>
2466 § -----
2467 aaa
2468 § spacer
2469 § spacer
2470 ddd
2471 eee"
2472 .unindent(),
2473 "
2474 § <no file>
2475 § -----
2476 aaa
2477 bbb
2478 ccc
2479 ddd
2480 eee"
2481 .unindent(),
2482 &mut cx,
2483 );
2484
2485 let primary_editor = editor.update(cx, |editor, _cx| editor.primary_editor.clone());
2486 cx.update_window_entity(&primary_editor, |editor, window, cx| {
2487 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2488 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2489 });
2490 editor.git_restore(&Restore, window, cx);
2491 });
2492
2493 cx.run_until_parked();
2494
2495 assert_split_content(
2496 &editor,
2497 "
2498 § <no file>
2499 § -----
2500 aaa
2501 bbb
2502 ccc
2503 ddd
2504 eee"
2505 .unindent(),
2506 "
2507 § <no file>
2508 § -----
2509 aaa
2510 bbb
2511 ccc
2512 ddd
2513 eee"
2514 .unindent(),
2515 &mut cx,
2516 );
2517
2518 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2519 diff.update(cx, |diff, cx| {
2520 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2521 });
2522
2523 cx.run_until_parked();
2524
2525 assert_split_content(
2526 &editor,
2527 "
2528 § <no file>
2529 § -----
2530 aaa
2531 bbb
2532 ccc
2533 ddd
2534 eee"
2535 .unindent(),
2536 "
2537 § <no file>
2538 § -----
2539 aaa
2540 bbb
2541 ccc
2542 ddd
2543 eee"
2544 .unindent(),
2545 &mut cx,
2546 );
2547 }
2548
2549 #[gpui::test]
2550 async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
2551 use rope::Point;
2552 use unindent::Unindent as _;
2553
2554 let (editor, mut cx) = init_test(cx).await;
2555
2556 let base_text = "
2557 aaa
2558 old1
2559 old2
2560 old3
2561 old4
2562 zzz
2563 "
2564 .unindent();
2565
2566 let current_text = "
2567 aaa
2568 new1
2569 new2
2570 new3
2571 new4
2572 zzz
2573 "
2574 .unindent();
2575
2576 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2577
2578 editor.update(cx, |editor, cx| {
2579 let path = PathKey::for_buffer(&buffer, cx);
2580 editor.set_excerpts_for_path(
2581 path,
2582 buffer.clone(),
2583 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2584 0,
2585 diff.clone(),
2586 cx,
2587 );
2588 });
2589
2590 cx.run_until_parked();
2591
2592 buffer.update(cx, |buffer, cx| {
2593 buffer.edit(
2594 [
2595 (Point::new(2, 0)..Point::new(3, 0), ""),
2596 (Point::new(4, 0)..Point::new(5, 0), ""),
2597 ],
2598 None,
2599 cx,
2600 );
2601 });
2602 cx.run_until_parked();
2603
2604 assert_split_content(
2605 &editor,
2606 "
2607 § <no file>
2608 § -----
2609 aaa
2610 new1
2611 new3
2612 § spacer
2613 § spacer
2614 zzz"
2615 .unindent(),
2616 "
2617 § <no file>
2618 § -----
2619 aaa
2620 old1
2621 old2
2622 old3
2623 old4
2624 zzz"
2625 .unindent(),
2626 &mut cx,
2627 );
2628
2629 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2630 diff.update(cx, |diff, cx| {
2631 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2632 });
2633
2634 cx.run_until_parked();
2635
2636 assert_split_content(
2637 &editor,
2638 "
2639 § <no file>
2640 § -----
2641 aaa
2642 new1
2643 new3
2644 § spacer
2645 § spacer
2646 zzz"
2647 .unindent(),
2648 "
2649 § <no file>
2650 § -----
2651 aaa
2652 old1
2653 old2
2654 old3
2655 old4
2656 zzz"
2657 .unindent(),
2658 &mut cx,
2659 );
2660 }
2661
2662 #[gpui::test]
2663 async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
2664 use rope::Point;
2665 use unindent::Unindent as _;
2666
2667 let (editor, mut cx) = init_test(cx).await;
2668
2669 let text = "aaaa bbbb cccc dddd eeee ffff";
2670
2671 let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
2672 let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
2673
2674 editor.update(cx, |editor, cx| {
2675 let end = Point::new(0, text.len() as u32);
2676 let path1 = PathKey::for_buffer(&buffer1, cx);
2677 editor.set_excerpts_for_path(
2678 path1,
2679 buffer1.clone(),
2680 vec![Point::new(0, 0)..end],
2681 0,
2682 diff1.clone(),
2683 cx,
2684 );
2685 let path2 = PathKey::for_buffer(&buffer2, cx);
2686 editor.set_excerpts_for_path(
2687 path2,
2688 buffer2.clone(),
2689 vec![Point::new(0, 0)..end],
2690 0,
2691 diff2.clone(),
2692 cx,
2693 );
2694 });
2695
2696 cx.run_until_parked();
2697
2698 assert_split_content_with_widths(
2699 &editor,
2700 px(200.0),
2701 px(400.0),
2702 "
2703 § <no file>
2704 § -----
2705 aaaa bbbb\x20
2706 cccc dddd\x20
2707 eeee ffff
2708 § <no file>
2709 § -----
2710 aaaa bbbb\x20
2711 cccc dddd\x20
2712 eeee ffff"
2713 .unindent(),
2714 "
2715 § <no file>
2716 § -----
2717 aaaa bbbb cccc dddd eeee ffff
2718 § spacer
2719 § spacer
2720 § <no file>
2721 § -----
2722 aaaa bbbb cccc dddd eeee ffff
2723 § spacer
2724 § spacer"
2725 .unindent(),
2726 &mut cx,
2727 );
2728 }
2729
2730 #[gpui::test]
2731 async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
2732 use rope::Point;
2733 use unindent::Unindent as _;
2734
2735 let (editor, mut cx) = init_test(cx).await;
2736
2737 let base_text = "
2738 aaaa bbbb cccc dddd eeee ffff
2739 old line one
2740 old line two
2741 "
2742 .unindent();
2743
2744 let current_text = "
2745 aaaa bbbb cccc dddd eeee ffff
2746 new line
2747 "
2748 .unindent();
2749
2750 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2751
2752 editor.update(cx, |editor, cx| {
2753 let path = PathKey::for_buffer(&buffer, cx);
2754 editor.set_excerpts_for_path(
2755 path,
2756 buffer.clone(),
2757 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2758 0,
2759 diff.clone(),
2760 cx,
2761 );
2762 });
2763
2764 cx.run_until_parked();
2765
2766 assert_split_content_with_widths(
2767 &editor,
2768 px(200.0),
2769 px(400.0),
2770 "
2771 § <no file>
2772 § -----
2773 aaaa bbbb\x20
2774 cccc dddd\x20
2775 eeee ffff
2776 new line
2777 § spacer"
2778 .unindent(),
2779 "
2780 § <no file>
2781 § -----
2782 aaaa bbbb cccc dddd eeee ffff
2783 § spacer
2784 § spacer
2785 old line one
2786 old line two"
2787 .unindent(),
2788 &mut cx,
2789 );
2790 }
2791
2792 #[gpui::test]
2793 async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
2794 use rope::Point;
2795 use unindent::Unindent as _;
2796
2797 let (editor, mut cx) = init_test(cx).await;
2798
2799 let base_text = "
2800 aaaa bbbb cccc dddd eeee ffff
2801 deleted line one
2802 deleted line two
2803 after
2804 "
2805 .unindent();
2806
2807 let current_text = "
2808 aaaa bbbb cccc dddd eeee ffff
2809 after
2810 "
2811 .unindent();
2812
2813 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2814
2815 editor.update(cx, |editor, cx| {
2816 let path = PathKey::for_buffer(&buffer, cx);
2817 editor.set_excerpts_for_path(
2818 path,
2819 buffer.clone(),
2820 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2821 0,
2822 diff.clone(),
2823 cx,
2824 );
2825 });
2826
2827 cx.run_until_parked();
2828
2829 assert_split_content_with_widths(
2830 &editor,
2831 px(400.0),
2832 px(200.0),
2833 "
2834 § <no file>
2835 § -----
2836 aaaa bbbb cccc dddd eeee ffff
2837 § spacer
2838 § spacer
2839 § spacer
2840 § spacer
2841 § spacer
2842 § spacer
2843 after"
2844 .unindent(),
2845 "
2846 § <no file>
2847 § -----
2848 aaaa bbbb\x20
2849 cccc dddd\x20
2850 eeee ffff
2851 deleted\x20
2852 line one
2853 deleted\x20
2854 line two
2855 after"
2856 .unindent(),
2857 &mut cx,
2858 );
2859 }
2860
2861 #[gpui::test]
2862 async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
2863 use rope::Point;
2864 use unindent::Unindent as _;
2865
2866 let (editor, mut cx) = init_test(cx).await;
2867
2868 let text = "
2869 aaaa bbbb cccc dddd eeee ffff
2870 short
2871 "
2872 .unindent();
2873
2874 let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
2875
2876 editor.update(cx, |editor, cx| {
2877 let path = PathKey::for_buffer(&buffer, cx);
2878 editor.set_excerpts_for_path(
2879 path,
2880 buffer.clone(),
2881 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2882 0,
2883 diff.clone(),
2884 cx,
2885 );
2886 });
2887
2888 cx.run_until_parked();
2889
2890 assert_split_content_with_widths(
2891 &editor,
2892 px(400.0),
2893 px(200.0),
2894 "
2895 § <no file>
2896 § -----
2897 aaaa bbbb cccc dddd eeee ffff
2898 § spacer
2899 § spacer
2900 short"
2901 .unindent(),
2902 "
2903 § <no file>
2904 § -----
2905 aaaa bbbb\x20
2906 cccc dddd\x20
2907 eeee ffff
2908 short"
2909 .unindent(),
2910 &mut cx,
2911 );
2912
2913 buffer.update(cx, |buffer, cx| {
2914 buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
2915 });
2916
2917 cx.run_until_parked();
2918
2919 assert_split_content_with_widths(
2920 &editor,
2921 px(400.0),
2922 px(200.0),
2923 "
2924 § <no file>
2925 § -----
2926 aaaa bbbb cccc dddd eeee ffff
2927 § spacer
2928 § spacer
2929 modified"
2930 .unindent(),
2931 "
2932 § <no file>
2933 § -----
2934 aaaa bbbb\x20
2935 cccc dddd\x20
2936 eeee ffff
2937 short"
2938 .unindent(),
2939 &mut cx,
2940 );
2941
2942 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2943 diff.update(cx, |diff, cx| {
2944 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2945 });
2946
2947 cx.run_until_parked();
2948
2949 assert_split_content_with_widths(
2950 &editor,
2951 px(400.0),
2952 px(200.0),
2953 "
2954 § <no file>
2955 § -----
2956 aaaa bbbb cccc dddd eeee ffff
2957 § spacer
2958 § spacer
2959 modified"
2960 .unindent(),
2961 "
2962 § <no file>
2963 § -----
2964 aaaa bbbb\x20
2965 cccc dddd\x20
2966 eeee ffff
2967 short"
2968 .unindent(),
2969 &mut cx,
2970 );
2971 }
2972
2973 #[gpui::test]
2974 async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
2975 use rope::Point;
2976 use unindent::Unindent as _;
2977
2978 let (editor, mut cx) = init_test(cx).await;
2979
2980 let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
2981
2982 let current_text = "
2983 aaa
2984 bbb
2985 ccc
2986 "
2987 .unindent();
2988
2989 let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2990 let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
2991
2992 editor.update(cx, |editor, cx| {
2993 let path1 = PathKey::for_buffer(&buffer1, cx);
2994 editor.set_excerpts_for_path(
2995 path1,
2996 buffer1.clone(),
2997 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2998 0,
2999 diff1.clone(),
3000 cx,
3001 );
3002
3003 let path2 = PathKey::for_buffer(&buffer2, cx);
3004 editor.set_excerpts_for_path(
3005 path2,
3006 buffer2.clone(),
3007 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3008 1,
3009 diff2.clone(),
3010 cx,
3011 );
3012 });
3013
3014 cx.run_until_parked();
3015
3016 assert_split_content(
3017 &editor,
3018 "
3019 § <no file>
3020 § -----
3021 xxx
3022 yyy
3023 § <no file>
3024 § -----
3025 aaa
3026 bbb
3027 ccc"
3028 .unindent(),
3029 "
3030 § <no file>
3031 § -----
3032 xxx
3033 yyy
3034 § <no file>
3035 § -----
3036 § spacer
3037 § spacer
3038 § spacer"
3039 .unindent(),
3040 &mut cx,
3041 );
3042
3043 buffer1.update(cx, |buffer, cx| {
3044 buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3045 });
3046
3047 cx.run_until_parked();
3048
3049 assert_split_content(
3050 &editor,
3051 "
3052 § <no file>
3053 § -----
3054 xxxz
3055 yyy
3056 § <no file>
3057 § -----
3058 aaa
3059 bbb
3060 ccc"
3061 .unindent(),
3062 "
3063 § <no file>
3064 § -----
3065 xxx
3066 yyy
3067 § <no file>
3068 § -----
3069 § spacer
3070 § spacer
3071 § spacer"
3072 .unindent(),
3073 &mut cx,
3074 );
3075 }
3076
3077 #[gpui::test]
3078 async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3079 use rope::Point;
3080 use unindent::Unindent as _;
3081
3082 let (editor, mut cx) = init_test(cx).await;
3083
3084 let base_text = "
3085 aaa
3086 bbb
3087 ccc
3088 "
3089 .unindent();
3090
3091 let current_text = "
3092 NEW1
3093 NEW2
3094 ccc
3095 "
3096 .unindent();
3097
3098 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3099
3100 editor.update(cx, |editor, cx| {
3101 let path = PathKey::for_buffer(&buffer, cx);
3102 editor.set_excerpts_for_path(
3103 path,
3104 buffer.clone(),
3105 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3106 0,
3107 diff.clone(),
3108 cx,
3109 );
3110 });
3111
3112 cx.run_until_parked();
3113
3114 assert_split_content(
3115 &editor,
3116 "
3117 § <no file>
3118 § -----
3119 NEW1
3120 NEW2
3121 ccc"
3122 .unindent(),
3123 "
3124 § <no file>
3125 § -----
3126 aaa
3127 bbb
3128 ccc"
3129 .unindent(),
3130 &mut cx,
3131 );
3132
3133 buffer.update(cx, |buffer, cx| {
3134 buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3135 });
3136
3137 cx.run_until_parked();
3138
3139 assert_split_content(
3140 &editor,
3141 "
3142 § <no file>
3143 § -----
3144 NEW1
3145 NEW
3146 ccc"
3147 .unindent(),
3148 "
3149 § <no file>
3150 § -----
3151 aaa
3152 bbb
3153 ccc"
3154 .unindent(),
3155 &mut cx,
3156 );
3157 }
3158
3159 #[gpui::test]
3160 async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3161 use rope::Point;
3162 use unindent::Unindent as _;
3163
3164 let (editor, mut cx) = init_test(cx).await;
3165
3166 let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3167
3168 let current_text = "
3169 aaaa bbbb cccc dddd eeee ffff
3170 added line
3171 "
3172 .unindent();
3173
3174 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3175
3176 editor.update(cx, |editor, cx| {
3177 let path = PathKey::for_buffer(&buffer, cx);
3178 editor.set_excerpts_for_path(
3179 path,
3180 buffer.clone(),
3181 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3182 0,
3183 diff.clone(),
3184 cx,
3185 );
3186 });
3187
3188 cx.run_until_parked();
3189
3190 assert_split_content_with_widths(
3191 &editor,
3192 px(400.0),
3193 px(200.0),
3194 "
3195 § <no file>
3196 § -----
3197 aaaa bbbb cccc dddd eeee ffff
3198 § spacer
3199 § spacer
3200 added line"
3201 .unindent(),
3202 "
3203 § <no file>
3204 § -----
3205 aaaa bbbb\x20
3206 cccc dddd\x20
3207 eeee ffff
3208 § spacer"
3209 .unindent(),
3210 &mut cx,
3211 );
3212
3213 assert_split_content_with_widths(
3214 &editor,
3215 px(200.0),
3216 px(400.0),
3217 "
3218 § <no file>
3219 § -----
3220 aaaa bbbb\x20
3221 cccc dddd\x20
3222 eeee ffff
3223 added line"
3224 .unindent(),
3225 "
3226 § <no file>
3227 § -----
3228 aaaa bbbb cccc dddd eeee ffff
3229 § spacer
3230 § spacer
3231 § spacer"
3232 .unindent(),
3233 &mut cx,
3234 );
3235 }
3236
3237 #[gpui::test]
3238 #[ignore]
3239 async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3240 use rope::Point;
3241 use unindent::Unindent as _;
3242
3243 let (editor, mut cx) = init_test(cx).await;
3244
3245 let base_text = "
3246 aaa
3247 bbb
3248 ccc
3249 ddd
3250 eee
3251 "
3252 .unindent();
3253
3254 let current_text = "
3255 aaa
3256 NEW
3257 eee
3258 "
3259 .unindent();
3260
3261 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3262
3263 editor.update(cx, |editor, cx| {
3264 let path = PathKey::for_buffer(&buffer, cx);
3265 editor.set_excerpts_for_path(
3266 path,
3267 buffer.clone(),
3268 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3269 0,
3270 diff.clone(),
3271 cx,
3272 );
3273 });
3274
3275 cx.run_until_parked();
3276
3277 assert_split_content(
3278 &editor,
3279 "
3280 § <no file>
3281 § -----
3282 aaa
3283 NEW
3284 § spacer
3285 § spacer
3286 eee"
3287 .unindent(),
3288 "
3289 § <no file>
3290 § -----
3291 aaa
3292 bbb
3293 ccc
3294 ddd
3295 eee"
3296 .unindent(),
3297 &mut cx,
3298 );
3299
3300 buffer.update(cx, |buffer, cx| {
3301 buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3302 });
3303
3304 cx.run_until_parked();
3305
3306 assert_split_content(
3307 &editor,
3308 "
3309 § <no file>
3310 § -----
3311 aaa
3312 § spacer
3313 § spacer
3314 § spacer
3315 NEWeee"
3316 .unindent(),
3317 "
3318 § <no file>
3319 § -----
3320 aaa
3321 bbb
3322 ccc
3323 ddd
3324 eee"
3325 .unindent(),
3326 &mut cx,
3327 );
3328
3329 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3330 diff.update(cx, |diff, cx| {
3331 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3332 });
3333
3334 cx.run_until_parked();
3335
3336 assert_split_content(
3337 &editor,
3338 "
3339 § <no file>
3340 § -----
3341 aaa
3342 NEWeee
3343 § spacer
3344 § spacer
3345 § spacer"
3346 .unindent(),
3347 "
3348 § <no file>
3349 § -----
3350 aaa
3351 bbb
3352 ccc
3353 ddd
3354 eee"
3355 .unindent(),
3356 &mut cx,
3357 );
3358 }
3359}