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