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