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