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