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