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