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