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