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