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