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