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