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