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::MultiWorkspace;
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 (multi_workspace, cx) =
2109 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2110 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2111 let rhs_multibuffer = cx.new(|cx| {
2112 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2113 multibuffer.set_all_diff_hunks_expanded(cx);
2114 multibuffer
2115 });
2116 let editor = cx.new_window_entity(|window, cx| {
2117 let editor = SplittableEditor::new(
2118 style,
2119 rhs_multibuffer.clone(),
2120 project.clone(),
2121 workspace,
2122 window,
2123 cx,
2124 );
2125 editor.rhs_editor.update(cx, |editor, cx| {
2126 editor.set_soft_wrap_mode(soft_wrap, cx);
2127 });
2128 if let Some(lhs) = &editor.lhs {
2129 lhs.editor.update(cx, |editor, cx| {
2130 editor.set_soft_wrap_mode(soft_wrap, cx);
2131 });
2132 }
2133 editor
2134 });
2135 (editor, cx)
2136 }
2137
2138 fn buffer_with_diff(
2139 base_text: &str,
2140 current_text: &str,
2141 cx: &mut VisualTestContext,
2142 ) -> (Entity<Buffer>, Entity<BufferDiff>) {
2143 let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2144 let diff = cx.new(|cx| {
2145 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
2146 });
2147 (buffer, diff)
2148 }
2149
2150 #[track_caller]
2151 fn assert_split_content(
2152 editor: &Entity<SplittableEditor>,
2153 expected_rhs: String,
2154 expected_lhs: String,
2155 cx: &mut VisualTestContext,
2156 ) {
2157 assert_split_content_with_widths(
2158 editor,
2159 px(3000.0),
2160 px(3000.0),
2161 expected_rhs,
2162 expected_lhs,
2163 cx,
2164 );
2165 }
2166
2167 #[track_caller]
2168 fn assert_split_content_with_widths(
2169 editor: &Entity<SplittableEditor>,
2170 rhs_width: Pixels,
2171 lhs_width: Pixels,
2172 expected_rhs: String,
2173 expected_lhs: String,
2174 cx: &mut VisualTestContext,
2175 ) {
2176 let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2177 let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2178 (editor.rhs_editor.clone(), lhs.editor.clone())
2179 });
2180
2181 // Make sure both sides learn if the other has soft-wrapped
2182 let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2183 cx.run_until_parked();
2184 let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2185 cx.run_until_parked();
2186
2187 let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2188 let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2189
2190 if rhs_content != expected_rhs || lhs_content != expected_lhs {
2191 editor.update(cx, |editor, cx| editor.debug_print(cx));
2192 }
2193
2194 assert_eq!(rhs_content, expected_rhs, "rhs");
2195 assert_eq!(lhs_content, expected_lhs, "lhs");
2196 }
2197
2198 #[gpui::test(iterations = 100)]
2199 async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2200 use rand::prelude::*;
2201
2202 let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2203 let operations = std::env::var("OPERATIONS")
2204 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2205 .unwrap_or(10);
2206 let rng = &mut rng;
2207 for _ in 0..operations {
2208 let buffers = editor.update(cx, |editor, cx| {
2209 editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2210 });
2211
2212 if buffers.is_empty() {
2213 log::info!("adding excerpts to empty multibuffer");
2214 editor.update(cx, |editor, cx| {
2215 editor.randomly_edit_excerpts(rng, 2, cx);
2216 editor.check_invariants(true, cx);
2217 });
2218 continue;
2219 }
2220
2221 let mut quiesced = false;
2222
2223 match rng.random_range(0..100) {
2224 0..=44 => {
2225 log::info!("randomly editing multibuffer");
2226 editor.update(cx, |editor, cx| {
2227 editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2228 multibuffer.randomly_edit(rng, 5, cx);
2229 })
2230 })
2231 }
2232 45..=64 => {
2233 log::info!("randomly undoing/redoing in single buffer");
2234 let buffer = buffers.iter().choose(rng).unwrap();
2235 buffer.update(cx, |buffer, cx| {
2236 buffer.randomly_undo_redo(rng, cx);
2237 });
2238 }
2239 65..=79 => {
2240 log::info!("mutating excerpts");
2241 editor.update(cx, |editor, cx| {
2242 editor.randomly_edit_excerpts(rng, 2, cx);
2243 });
2244 }
2245 _ => {
2246 log::info!("quiescing");
2247 for buffer in buffers {
2248 let buffer_snapshot =
2249 buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2250 let diff = editor.update(cx, |editor, cx| {
2251 editor
2252 .rhs_multibuffer
2253 .read(cx)
2254 .diff_for(buffer.read(cx).remote_id())
2255 .unwrap()
2256 });
2257 diff.update(cx, |diff, cx| {
2258 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2259 });
2260 cx.run_until_parked();
2261 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2262 let ranges = diff_snapshot
2263 .hunks(&buffer_snapshot)
2264 .map(|hunk| hunk.range)
2265 .collect::<Vec<_>>();
2266 editor.update(cx, |editor, cx| {
2267 let path = PathKey::for_buffer(&buffer, cx);
2268 editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2269 });
2270 }
2271 quiesced = true;
2272 }
2273 }
2274
2275 editor.update(cx, |editor, cx| {
2276 editor.check_invariants(quiesced, cx);
2277 });
2278 }
2279 }
2280
2281 #[gpui::test]
2282 async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2283 use rope::Point;
2284 use unindent::Unindent as _;
2285
2286 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2287
2288 let base_text = "
2289 aaa
2290 bbb
2291 ccc
2292 ddd
2293 eee
2294 fff
2295 "
2296 .unindent();
2297 let current_text = "
2298 aaa
2299 ddd
2300 eee
2301 fff
2302 "
2303 .unindent();
2304
2305 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2306
2307 editor.update(cx, |editor, cx| {
2308 let path = PathKey::for_buffer(&buffer, cx);
2309 editor.set_excerpts_for_path(
2310 path,
2311 buffer.clone(),
2312 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2313 0,
2314 diff.clone(),
2315 cx,
2316 );
2317 });
2318
2319 cx.run_until_parked();
2320
2321 assert_split_content(
2322 &editor,
2323 "
2324 § <no file>
2325 § -----
2326 aaa
2327 § spacer
2328 § spacer
2329 ddd
2330 eee
2331 fff"
2332 .unindent(),
2333 "
2334 § <no file>
2335 § -----
2336 aaa
2337 bbb
2338 ccc
2339 ddd
2340 eee
2341 fff"
2342 .unindent(),
2343 &mut cx,
2344 );
2345
2346 buffer.update(cx, |buffer, cx| {
2347 buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2348 });
2349
2350 cx.run_until_parked();
2351
2352 assert_split_content(
2353 &editor,
2354 "
2355 § <no file>
2356 § -----
2357 aaa
2358 § spacer
2359 § spacer
2360 ddd
2361 eee
2362 FFF"
2363 .unindent(),
2364 "
2365 § <no file>
2366 § -----
2367 aaa
2368 bbb
2369 ccc
2370 ddd
2371 eee
2372 fff"
2373 .unindent(),
2374 &mut cx,
2375 );
2376
2377 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2378 diff.update(cx, |diff, cx| {
2379 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2380 });
2381
2382 cx.run_until_parked();
2383
2384 assert_split_content(
2385 &editor,
2386 "
2387 § <no file>
2388 § -----
2389 aaa
2390 § spacer
2391 § spacer
2392 ddd
2393 eee
2394 FFF"
2395 .unindent(),
2396 "
2397 § <no file>
2398 § -----
2399 aaa
2400 bbb
2401 ccc
2402 ddd
2403 eee
2404 fff"
2405 .unindent(),
2406 &mut cx,
2407 );
2408 }
2409
2410 #[gpui::test]
2411 async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2412 use rope::Point;
2413 use unindent::Unindent as _;
2414
2415 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2416
2417 let base_text1 = "
2418 aaa
2419 bbb
2420 ccc
2421 ddd
2422 eee"
2423 .unindent();
2424
2425 let base_text2 = "
2426 fff
2427 ggg
2428 hhh
2429 iii
2430 jjj"
2431 .unindent();
2432
2433 let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2434 let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2435
2436 editor.update(cx, |editor, cx| {
2437 let path1 = PathKey::for_buffer(&buffer1, cx);
2438 editor.set_excerpts_for_path(
2439 path1,
2440 buffer1.clone(),
2441 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2442 0,
2443 diff1.clone(),
2444 cx,
2445 );
2446 let path2 = PathKey::for_buffer(&buffer2, cx);
2447 editor.set_excerpts_for_path(
2448 path2,
2449 buffer2.clone(),
2450 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2451 1,
2452 diff2.clone(),
2453 cx,
2454 );
2455 });
2456
2457 cx.run_until_parked();
2458
2459 buffer1.update(cx, |buffer, cx| {
2460 buffer.edit(
2461 [
2462 (Point::new(0, 0)..Point::new(1, 0), ""),
2463 (Point::new(3, 0)..Point::new(4, 0), ""),
2464 ],
2465 None,
2466 cx,
2467 );
2468 });
2469 buffer2.update(cx, |buffer, cx| {
2470 buffer.edit(
2471 [
2472 (Point::new(0, 0)..Point::new(1, 0), ""),
2473 (Point::new(3, 0)..Point::new(4, 0), ""),
2474 ],
2475 None,
2476 cx,
2477 );
2478 });
2479
2480 cx.run_until_parked();
2481
2482 assert_split_content(
2483 &editor,
2484 "
2485 § <no file>
2486 § -----
2487 § spacer
2488 bbb
2489 ccc
2490 § spacer
2491 eee
2492 § <no file>
2493 § -----
2494 § spacer
2495 ggg
2496 hhh
2497 § spacer
2498 jjj"
2499 .unindent(),
2500 "
2501 § <no file>
2502 § -----
2503 aaa
2504 bbb
2505 ccc
2506 ddd
2507 eee
2508 § <no file>
2509 § -----
2510 fff
2511 ggg
2512 hhh
2513 iii
2514 jjj"
2515 .unindent(),
2516 &mut cx,
2517 );
2518
2519 let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2520 diff1.update(cx, |diff, cx| {
2521 diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2522 });
2523 let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2524 diff2.update(cx, |diff, cx| {
2525 diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2526 });
2527
2528 cx.run_until_parked();
2529
2530 assert_split_content(
2531 &editor,
2532 "
2533 § <no file>
2534 § -----
2535 § spacer
2536 bbb
2537 ccc
2538 § spacer
2539 eee
2540 § <no file>
2541 § -----
2542 § spacer
2543 ggg
2544 hhh
2545 § spacer
2546 jjj"
2547 .unindent(),
2548 "
2549 § <no file>
2550 § -----
2551 aaa
2552 bbb
2553 ccc
2554 ddd
2555 eee
2556 § <no file>
2557 § -----
2558 fff
2559 ggg
2560 hhh
2561 iii
2562 jjj"
2563 .unindent(),
2564 &mut cx,
2565 );
2566 }
2567
2568 #[gpui::test]
2569 async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2570 use rope::Point;
2571 use unindent::Unindent as _;
2572
2573 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2574
2575 let base_text = "
2576 aaa
2577 bbb
2578 ccc
2579 ddd
2580 "
2581 .unindent();
2582
2583 let current_text = "
2584 aaa
2585 NEW1
2586 NEW2
2587 ccc
2588 ddd
2589 "
2590 .unindent();
2591
2592 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2593
2594 editor.update(cx, |editor, cx| {
2595 let path = PathKey::for_buffer(&buffer, cx);
2596 editor.set_excerpts_for_path(
2597 path,
2598 buffer.clone(),
2599 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2600 0,
2601 diff.clone(),
2602 cx,
2603 );
2604 });
2605
2606 cx.run_until_parked();
2607
2608 assert_split_content(
2609 &editor,
2610 "
2611 § <no file>
2612 § -----
2613 aaa
2614 NEW1
2615 NEW2
2616 ccc
2617 ddd"
2618 .unindent(),
2619 "
2620 § <no file>
2621 § -----
2622 aaa
2623 bbb
2624 § spacer
2625 ccc
2626 ddd"
2627 .unindent(),
2628 &mut cx,
2629 );
2630
2631 buffer.update(cx, |buffer, cx| {
2632 buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2633 });
2634
2635 cx.run_until_parked();
2636
2637 assert_split_content(
2638 &editor,
2639 "
2640 § <no file>
2641 § -----
2642 aaa
2643 NEW1
2644 ccc
2645 ddd"
2646 .unindent(),
2647 "
2648 § <no file>
2649 § -----
2650 aaa
2651 bbb
2652 ccc
2653 ddd"
2654 .unindent(),
2655 &mut cx,
2656 );
2657
2658 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2659 diff.update(cx, |diff, cx| {
2660 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2661 });
2662
2663 cx.run_until_parked();
2664
2665 assert_split_content(
2666 &editor,
2667 "
2668 § <no file>
2669 § -----
2670 aaa
2671 NEW1
2672 ccc
2673 ddd"
2674 .unindent(),
2675 "
2676 § <no file>
2677 § -----
2678 aaa
2679 bbb
2680 ccc
2681 ddd"
2682 .unindent(),
2683 &mut cx,
2684 );
2685 }
2686
2687 #[gpui::test]
2688 async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2689 use rope::Point;
2690 use unindent::Unindent as _;
2691
2692 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2693
2694 let base_text = "
2695 aaa
2696 bbb
2697
2698
2699
2700
2701
2702 ccc
2703 ddd
2704 "
2705 .unindent();
2706 let current_text = "
2707 aaa
2708 bbb
2709
2710
2711
2712
2713
2714 CCC
2715 ddd
2716 "
2717 .unindent();
2718
2719 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2720
2721 editor.update(cx, |editor, cx| {
2722 let path = PathKey::for_buffer(&buffer, cx);
2723 editor.set_excerpts_for_path(
2724 path,
2725 buffer.clone(),
2726 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2727 0,
2728 diff.clone(),
2729 cx,
2730 );
2731 });
2732
2733 cx.run_until_parked();
2734
2735 buffer.update(cx, |buffer, cx| {
2736 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2737 });
2738
2739 cx.run_until_parked();
2740
2741 assert_split_content(
2742 &editor,
2743 "
2744 § <no file>
2745 § -----
2746 aaa
2747 bbb
2748
2749
2750
2751
2752
2753
2754 CCC
2755 ddd"
2756 .unindent(),
2757 "
2758 § <no file>
2759 § -----
2760 aaa
2761 bbb
2762 § spacer
2763
2764
2765
2766
2767
2768 ccc
2769 ddd"
2770 .unindent(),
2771 &mut cx,
2772 );
2773
2774 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2775 diff.update(cx, |diff, cx| {
2776 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2777 });
2778
2779 cx.run_until_parked();
2780
2781 assert_split_content(
2782 &editor,
2783 "
2784 § <no file>
2785 § -----
2786 aaa
2787 bbb
2788
2789
2790
2791
2792
2793
2794 CCC
2795 ddd"
2796 .unindent(),
2797 "
2798 § <no file>
2799 § -----
2800 aaa
2801 bbb
2802
2803
2804
2805
2806
2807 ccc
2808 § spacer
2809 ddd"
2810 .unindent(),
2811 &mut cx,
2812 );
2813 }
2814
2815 #[gpui::test]
2816 async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2817 use git::Restore;
2818 use rope::Point;
2819 use unindent::Unindent as _;
2820
2821 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2822
2823 let base_text = "
2824 aaa
2825 bbb
2826 ccc
2827 ddd
2828 eee
2829 "
2830 .unindent();
2831 let current_text = "
2832 aaa
2833 ddd
2834 eee
2835 "
2836 .unindent();
2837
2838 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2839
2840 editor.update(cx, |editor, cx| {
2841 let path = PathKey::for_buffer(&buffer, cx);
2842 editor.set_excerpts_for_path(
2843 path,
2844 buffer.clone(),
2845 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2846 0,
2847 diff.clone(),
2848 cx,
2849 );
2850 });
2851
2852 cx.run_until_parked();
2853
2854 assert_split_content(
2855 &editor,
2856 "
2857 § <no file>
2858 § -----
2859 aaa
2860 § spacer
2861 § spacer
2862 ddd
2863 eee"
2864 .unindent(),
2865 "
2866 § <no file>
2867 § -----
2868 aaa
2869 bbb
2870 ccc
2871 ddd
2872 eee"
2873 .unindent(),
2874 &mut cx,
2875 );
2876
2877 let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
2878 cx.update_window_entity(&rhs_editor, |editor, window, cx| {
2879 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2880 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2881 });
2882 editor.git_restore(&Restore, window, cx);
2883 });
2884
2885 cx.run_until_parked();
2886
2887 assert_split_content(
2888 &editor,
2889 "
2890 § <no file>
2891 § -----
2892 aaa
2893 bbb
2894 ccc
2895 ddd
2896 eee"
2897 .unindent(),
2898 "
2899 § <no file>
2900 § -----
2901 aaa
2902 bbb
2903 ccc
2904 ddd
2905 eee"
2906 .unindent(),
2907 &mut cx,
2908 );
2909
2910 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2911 diff.update(cx, |diff, cx| {
2912 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2913 });
2914
2915 cx.run_until_parked();
2916
2917 assert_split_content(
2918 &editor,
2919 "
2920 § <no file>
2921 § -----
2922 aaa
2923 bbb
2924 ccc
2925 ddd
2926 eee"
2927 .unindent(),
2928 "
2929 § <no file>
2930 § -----
2931 aaa
2932 bbb
2933 ccc
2934 ddd
2935 eee"
2936 .unindent(),
2937 &mut cx,
2938 );
2939 }
2940
2941 #[gpui::test]
2942 async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
2943 use rope::Point;
2944 use unindent::Unindent as _;
2945
2946 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2947
2948 let base_text = "
2949 aaa
2950 old1
2951 old2
2952 old3
2953 old4
2954 zzz
2955 "
2956 .unindent();
2957
2958 let current_text = "
2959 aaa
2960 new1
2961 new2
2962 new3
2963 new4
2964 zzz
2965 "
2966 .unindent();
2967
2968 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2969
2970 editor.update(cx, |editor, cx| {
2971 let path = PathKey::for_buffer(&buffer, cx);
2972 editor.set_excerpts_for_path(
2973 path,
2974 buffer.clone(),
2975 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2976 0,
2977 diff.clone(),
2978 cx,
2979 );
2980 });
2981
2982 cx.run_until_parked();
2983
2984 buffer.update(cx, |buffer, cx| {
2985 buffer.edit(
2986 [
2987 (Point::new(2, 0)..Point::new(3, 0), ""),
2988 (Point::new(4, 0)..Point::new(5, 0), ""),
2989 ],
2990 None,
2991 cx,
2992 );
2993 });
2994 cx.run_until_parked();
2995
2996 assert_split_content(
2997 &editor,
2998 "
2999 § <no file>
3000 § -----
3001 aaa
3002 new1
3003 new3
3004 § spacer
3005 § spacer
3006 zzz"
3007 .unindent(),
3008 "
3009 § <no file>
3010 § -----
3011 aaa
3012 old1
3013 old2
3014 old3
3015 old4
3016 zzz"
3017 .unindent(),
3018 &mut cx,
3019 );
3020
3021 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3022 diff.update(cx, |diff, cx| {
3023 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3024 });
3025
3026 cx.run_until_parked();
3027
3028 assert_split_content(
3029 &editor,
3030 "
3031 § <no file>
3032 § -----
3033 aaa
3034 new1
3035 new3
3036 § spacer
3037 § spacer
3038 zzz"
3039 .unindent(),
3040 "
3041 § <no file>
3042 § -----
3043 aaa
3044 old1
3045 old2
3046 old3
3047 old4
3048 zzz"
3049 .unindent(),
3050 &mut cx,
3051 );
3052 }
3053
3054 #[gpui::test]
3055 async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
3056 use rope::Point;
3057 use unindent::Unindent as _;
3058
3059 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3060
3061 let text = "aaaa bbbb cccc dddd eeee ffff";
3062
3063 let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
3064 let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
3065
3066 editor.update(cx, |editor, cx| {
3067 let end = Point::new(0, text.len() as u32);
3068 let path1 = PathKey::for_buffer(&buffer1, cx);
3069 editor.set_excerpts_for_path(
3070 path1,
3071 buffer1.clone(),
3072 vec![Point::new(0, 0)..end],
3073 0,
3074 diff1.clone(),
3075 cx,
3076 );
3077 let path2 = PathKey::for_buffer(&buffer2, cx);
3078 editor.set_excerpts_for_path(
3079 path2,
3080 buffer2.clone(),
3081 vec![Point::new(0, 0)..end],
3082 0,
3083 diff2.clone(),
3084 cx,
3085 );
3086 });
3087
3088 cx.run_until_parked();
3089
3090 assert_split_content_with_widths(
3091 &editor,
3092 px(200.0),
3093 px(400.0),
3094 "
3095 § <no file>
3096 § -----
3097 aaaa bbbb\x20
3098 cccc dddd\x20
3099 eeee ffff
3100 § <no file>
3101 § -----
3102 aaaa bbbb\x20
3103 cccc dddd\x20
3104 eeee ffff"
3105 .unindent(),
3106 "
3107 § <no file>
3108 § -----
3109 aaaa bbbb cccc dddd eeee ffff
3110 § spacer
3111 § spacer
3112 § <no file>
3113 § -----
3114 aaaa bbbb cccc dddd eeee ffff
3115 § spacer
3116 § spacer"
3117 .unindent(),
3118 &mut cx,
3119 );
3120 }
3121
3122 #[gpui::test]
3123 async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
3124 use rope::Point;
3125 use unindent::Unindent as _;
3126
3127 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3128
3129 let base_text = "
3130 aaaa bbbb cccc dddd eeee ffff
3131 old line one
3132 old line two
3133 "
3134 .unindent();
3135
3136 let current_text = "
3137 aaaa bbbb cccc dddd eeee ffff
3138 new line
3139 "
3140 .unindent();
3141
3142 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3143
3144 editor.update(cx, |editor, cx| {
3145 let path = PathKey::for_buffer(&buffer, cx);
3146 editor.set_excerpts_for_path(
3147 path,
3148 buffer.clone(),
3149 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3150 0,
3151 diff.clone(),
3152 cx,
3153 );
3154 });
3155
3156 cx.run_until_parked();
3157
3158 assert_split_content_with_widths(
3159 &editor,
3160 px(200.0),
3161 px(400.0),
3162 "
3163 § <no file>
3164 § -----
3165 aaaa bbbb\x20
3166 cccc dddd\x20
3167 eeee ffff
3168 new line
3169 § spacer"
3170 .unindent(),
3171 "
3172 § <no file>
3173 § -----
3174 aaaa bbbb cccc dddd eeee ffff
3175 § spacer
3176 § spacer
3177 old line one
3178 old line two"
3179 .unindent(),
3180 &mut cx,
3181 );
3182 }
3183
3184 #[gpui::test]
3185 async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3186 use rope::Point;
3187 use unindent::Unindent as _;
3188
3189 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3190
3191 let base_text = "
3192 aaaa bbbb cccc dddd eeee ffff
3193 deleted line one
3194 deleted line two
3195 after
3196 "
3197 .unindent();
3198
3199 let current_text = "
3200 aaaa bbbb cccc dddd eeee ffff
3201 after
3202 "
3203 .unindent();
3204
3205 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3206
3207 editor.update(cx, |editor, cx| {
3208 let path = PathKey::for_buffer(&buffer, cx);
3209 editor.set_excerpts_for_path(
3210 path,
3211 buffer.clone(),
3212 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3213 0,
3214 diff.clone(),
3215 cx,
3216 );
3217 });
3218
3219 cx.run_until_parked();
3220
3221 assert_split_content_with_widths(
3222 &editor,
3223 px(400.0),
3224 px(200.0),
3225 "
3226 § <no file>
3227 § -----
3228 aaaa bbbb cccc dddd eeee ffff
3229 § spacer
3230 § spacer
3231 § spacer
3232 § spacer
3233 § spacer
3234 § spacer
3235 after"
3236 .unindent(),
3237 "
3238 § <no file>
3239 § -----
3240 aaaa bbbb\x20
3241 cccc dddd\x20
3242 eeee ffff
3243 deleted line\x20
3244 one
3245 deleted line\x20
3246 two
3247 after"
3248 .unindent(),
3249 &mut cx,
3250 );
3251 }
3252
3253 #[gpui::test]
3254 async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3255 use rope::Point;
3256 use unindent::Unindent as _;
3257
3258 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3259
3260 let text = "
3261 aaaa bbbb cccc dddd eeee ffff
3262 short
3263 "
3264 .unindent();
3265
3266 let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3267
3268 editor.update(cx, |editor, cx| {
3269 let path = PathKey::for_buffer(&buffer, cx);
3270 editor.set_excerpts_for_path(
3271 path,
3272 buffer.clone(),
3273 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3274 0,
3275 diff.clone(),
3276 cx,
3277 );
3278 });
3279
3280 cx.run_until_parked();
3281
3282 assert_split_content_with_widths(
3283 &editor,
3284 px(400.0),
3285 px(200.0),
3286 "
3287 § <no file>
3288 § -----
3289 aaaa bbbb cccc dddd eeee ffff
3290 § spacer
3291 § spacer
3292 short"
3293 .unindent(),
3294 "
3295 § <no file>
3296 § -----
3297 aaaa bbbb\x20
3298 cccc dddd\x20
3299 eeee ffff
3300 short"
3301 .unindent(),
3302 &mut cx,
3303 );
3304
3305 buffer.update(cx, |buffer, cx| {
3306 buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3307 });
3308
3309 cx.run_until_parked();
3310
3311 assert_split_content_with_widths(
3312 &editor,
3313 px(400.0),
3314 px(200.0),
3315 "
3316 § <no file>
3317 § -----
3318 aaaa bbbb cccc dddd eeee ffff
3319 § spacer
3320 § spacer
3321 modified"
3322 .unindent(),
3323 "
3324 § <no file>
3325 § -----
3326 aaaa bbbb\x20
3327 cccc dddd\x20
3328 eeee ffff
3329 short"
3330 .unindent(),
3331 &mut cx,
3332 );
3333
3334 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3335 diff.update(cx, |diff, cx| {
3336 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3337 });
3338
3339 cx.run_until_parked();
3340
3341 assert_split_content_with_widths(
3342 &editor,
3343 px(400.0),
3344 px(200.0),
3345 "
3346 § <no file>
3347 § -----
3348 aaaa bbbb cccc dddd eeee ffff
3349 § spacer
3350 § spacer
3351 modified"
3352 .unindent(),
3353 "
3354 § <no file>
3355 § -----
3356 aaaa bbbb\x20
3357 cccc dddd\x20
3358 eeee ffff
3359 short"
3360 .unindent(),
3361 &mut cx,
3362 );
3363 }
3364
3365 #[gpui::test]
3366 async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3367 use rope::Point;
3368 use unindent::Unindent as _;
3369
3370 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3371
3372 let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3373
3374 let current_text = "
3375 aaa
3376 bbb
3377 ccc
3378 "
3379 .unindent();
3380
3381 let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3382 let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3383
3384 editor.update(cx, |editor, cx| {
3385 let path1 = PathKey::for_buffer(&buffer1, cx);
3386 editor.set_excerpts_for_path(
3387 path1,
3388 buffer1.clone(),
3389 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3390 0,
3391 diff1.clone(),
3392 cx,
3393 );
3394
3395 let path2 = PathKey::for_buffer(&buffer2, cx);
3396 editor.set_excerpts_for_path(
3397 path2,
3398 buffer2.clone(),
3399 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3400 1,
3401 diff2.clone(),
3402 cx,
3403 );
3404 });
3405
3406 cx.run_until_parked();
3407
3408 assert_split_content(
3409 &editor,
3410 "
3411 § <no file>
3412 § -----
3413 xxx
3414 yyy
3415 § <no file>
3416 § -----
3417 aaa
3418 bbb
3419 ccc"
3420 .unindent(),
3421 "
3422 § <no file>
3423 § -----
3424 xxx
3425 yyy
3426 § <no file>
3427 § -----
3428 § spacer
3429 § spacer
3430 § spacer"
3431 .unindent(),
3432 &mut cx,
3433 );
3434
3435 buffer1.update(cx, |buffer, cx| {
3436 buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3437 });
3438
3439 cx.run_until_parked();
3440
3441 assert_split_content(
3442 &editor,
3443 "
3444 § <no file>
3445 § -----
3446 xxxz
3447 yyy
3448 § <no file>
3449 § -----
3450 aaa
3451 bbb
3452 ccc"
3453 .unindent(),
3454 "
3455 § <no file>
3456 § -----
3457 xxx
3458 yyy
3459 § <no file>
3460 § -----
3461 § spacer
3462 § spacer
3463 § spacer"
3464 .unindent(),
3465 &mut cx,
3466 );
3467 }
3468
3469 #[gpui::test]
3470 async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3471 use rope::Point;
3472 use unindent::Unindent as _;
3473
3474 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3475
3476 let base_text = "
3477 aaa
3478 bbb
3479 ccc
3480 "
3481 .unindent();
3482
3483 let current_text = "
3484 NEW1
3485 NEW2
3486 ccc
3487 "
3488 .unindent();
3489
3490 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3491
3492 editor.update(cx, |editor, cx| {
3493 let path = PathKey::for_buffer(&buffer, cx);
3494 editor.set_excerpts_for_path(
3495 path,
3496 buffer.clone(),
3497 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3498 0,
3499 diff.clone(),
3500 cx,
3501 );
3502 });
3503
3504 cx.run_until_parked();
3505
3506 assert_split_content(
3507 &editor,
3508 "
3509 § <no file>
3510 § -----
3511 NEW1
3512 NEW2
3513 ccc"
3514 .unindent(),
3515 "
3516 § <no file>
3517 § -----
3518 aaa
3519 bbb
3520 ccc"
3521 .unindent(),
3522 &mut cx,
3523 );
3524
3525 buffer.update(cx, |buffer, cx| {
3526 buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3527 });
3528
3529 cx.run_until_parked();
3530
3531 assert_split_content(
3532 &editor,
3533 "
3534 § <no file>
3535 § -----
3536 NEW1
3537 NEW
3538 ccc"
3539 .unindent(),
3540 "
3541 § <no file>
3542 § -----
3543 aaa
3544 bbb
3545 ccc"
3546 .unindent(),
3547 &mut cx,
3548 );
3549 }
3550
3551 #[gpui::test]
3552 async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3553 use rope::Point;
3554 use unindent::Unindent as _;
3555
3556 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3557
3558 let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3559
3560 let current_text = "
3561 aaaa bbbb cccc dddd eeee ffff
3562 added line
3563 "
3564 .unindent();
3565
3566 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3567
3568 editor.update(cx, |editor, cx| {
3569 let path = PathKey::for_buffer(&buffer, cx);
3570 editor.set_excerpts_for_path(
3571 path,
3572 buffer.clone(),
3573 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3574 0,
3575 diff.clone(),
3576 cx,
3577 );
3578 });
3579
3580 cx.run_until_parked();
3581
3582 assert_split_content_with_widths(
3583 &editor,
3584 px(400.0),
3585 px(200.0),
3586 "
3587 § <no file>
3588 § -----
3589 aaaa bbbb cccc dddd eeee ffff
3590 § spacer
3591 § spacer
3592 added line"
3593 .unindent(),
3594 "
3595 § <no file>
3596 § -----
3597 aaaa bbbb\x20
3598 cccc dddd\x20
3599 eeee ffff
3600 § spacer"
3601 .unindent(),
3602 &mut cx,
3603 );
3604
3605 assert_split_content_with_widths(
3606 &editor,
3607 px(200.0),
3608 px(400.0),
3609 "
3610 § <no file>
3611 § -----
3612 aaaa bbbb\x20
3613 cccc dddd\x20
3614 eeee ffff
3615 added line"
3616 .unindent(),
3617 "
3618 § <no file>
3619 § -----
3620 aaaa bbbb cccc dddd eeee ffff
3621 § spacer
3622 § spacer
3623 § spacer"
3624 .unindent(),
3625 &mut cx,
3626 );
3627 }
3628
3629 #[gpui::test]
3630 #[ignore]
3631 async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3632 use rope::Point;
3633 use unindent::Unindent as _;
3634
3635 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3636
3637 let base_text = "
3638 aaa
3639 bbb
3640 ccc
3641 ddd
3642 eee
3643 "
3644 .unindent();
3645
3646 let current_text = "
3647 aaa
3648 NEW
3649 eee
3650 "
3651 .unindent();
3652
3653 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3654
3655 editor.update(cx, |editor, cx| {
3656 let path = PathKey::for_buffer(&buffer, cx);
3657 editor.set_excerpts_for_path(
3658 path,
3659 buffer.clone(),
3660 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3661 0,
3662 diff.clone(),
3663 cx,
3664 );
3665 });
3666
3667 cx.run_until_parked();
3668
3669 assert_split_content(
3670 &editor,
3671 "
3672 § <no file>
3673 § -----
3674 aaa
3675 NEW
3676 § spacer
3677 § spacer
3678 eee"
3679 .unindent(),
3680 "
3681 § <no file>
3682 § -----
3683 aaa
3684 bbb
3685 ccc
3686 ddd
3687 eee"
3688 .unindent(),
3689 &mut cx,
3690 );
3691
3692 buffer.update(cx, |buffer, cx| {
3693 buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3694 });
3695
3696 cx.run_until_parked();
3697
3698 assert_split_content(
3699 &editor,
3700 "
3701 § <no file>
3702 § -----
3703 aaa
3704 § spacer
3705 § spacer
3706 § spacer
3707 NEWeee"
3708 .unindent(),
3709 "
3710 § <no file>
3711 § -----
3712 aaa
3713 bbb
3714 ccc
3715 ddd
3716 eee"
3717 .unindent(),
3718 &mut cx,
3719 );
3720
3721 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3722 diff.update(cx, |diff, cx| {
3723 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3724 });
3725
3726 cx.run_until_parked();
3727
3728 assert_split_content(
3729 &editor,
3730 "
3731 § <no file>
3732 § -----
3733 aaa
3734 NEWeee
3735 § spacer
3736 § spacer
3737 § spacer"
3738 .unindent(),
3739 "
3740 § <no file>
3741 § -----
3742 aaa
3743 bbb
3744 ccc
3745 ddd
3746 eee"
3747 .unindent(),
3748 &mut cx,
3749 );
3750 }
3751
3752 #[gpui::test]
3753 async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3754 use rope::Point;
3755 use unindent::Unindent as _;
3756
3757 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3758
3759 let base_text = "";
3760 let current_text = "
3761 aaaa bbbb cccc dddd eeee ffff
3762 bbb
3763 ccc
3764 "
3765 .unindent();
3766
3767 let (buffer, diff) = buffer_with_diff(base_text, ¤t_text, &mut cx);
3768
3769 editor.update(cx, |editor, cx| {
3770 let path = PathKey::for_buffer(&buffer, cx);
3771 editor.set_excerpts_for_path(
3772 path,
3773 buffer.clone(),
3774 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3775 0,
3776 diff.clone(),
3777 cx,
3778 );
3779 });
3780
3781 cx.run_until_parked();
3782
3783 assert_split_content(
3784 &editor,
3785 "
3786 § <no file>
3787 § -----
3788 aaaa bbbb cccc dddd eeee ffff
3789 bbb
3790 ccc"
3791 .unindent(),
3792 "
3793 § <no file>
3794 § -----
3795 § spacer
3796 § spacer
3797 § spacer"
3798 .unindent(),
3799 &mut cx,
3800 );
3801
3802 assert_split_content_with_widths(
3803 &editor,
3804 px(200.0),
3805 px(200.0),
3806 "
3807 § <no file>
3808 § -----
3809 aaaa bbbb\x20
3810 cccc dddd\x20
3811 eeee ffff
3812 bbb
3813 ccc"
3814 .unindent(),
3815 "
3816 § <no file>
3817 § -----
3818 § spacer
3819 § spacer
3820 § spacer
3821 § spacer
3822 § spacer"
3823 .unindent(),
3824 &mut cx,
3825 );
3826 }
3827
3828 #[gpui::test]
3829 async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
3830 use rope::Point;
3831 use unindent::Unindent as _;
3832
3833 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3834
3835 let base_text = "
3836 aaa
3837 bbb
3838 ccc
3839 "
3840 .unindent();
3841
3842 let current_text = "
3843 aaa
3844 bbb
3845 xxx
3846 yyy
3847 ccc
3848 "
3849 .unindent();
3850
3851 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3852
3853 editor.update(cx, |editor, cx| {
3854 let path = PathKey::for_buffer(&buffer, cx);
3855 editor.set_excerpts_for_path(
3856 path,
3857 buffer.clone(),
3858 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3859 0,
3860 diff.clone(),
3861 cx,
3862 );
3863 });
3864
3865 cx.run_until_parked();
3866
3867 assert_split_content(
3868 &editor,
3869 "
3870 § <no file>
3871 § -----
3872 aaa
3873 bbb
3874 xxx
3875 yyy
3876 ccc"
3877 .unindent(),
3878 "
3879 § <no file>
3880 § -----
3881 aaa
3882 bbb
3883 § spacer
3884 § spacer
3885 ccc"
3886 .unindent(),
3887 &mut cx,
3888 );
3889
3890 buffer.update(cx, |buffer, cx| {
3891 buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
3892 });
3893
3894 cx.run_until_parked();
3895
3896 assert_split_content(
3897 &editor,
3898 "
3899 § <no file>
3900 § -----
3901 aaa
3902 bbb
3903 xxx
3904 yyy
3905 zzz
3906 ccc"
3907 .unindent(),
3908 "
3909 § <no file>
3910 § -----
3911 aaa
3912 bbb
3913 § spacer
3914 § spacer
3915 § spacer
3916 ccc"
3917 .unindent(),
3918 &mut cx,
3919 );
3920 }
3921
3922 #[gpui::test]
3923 async fn test_scrolling(cx: &mut gpui::TestAppContext) {
3924 use crate::test::editor_content_with_blocks_and_size;
3925 use gpui::size;
3926 use rope::Point;
3927
3928 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
3929
3930 let long_line = "x".repeat(200);
3931 let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
3932 lines[25] = long_line;
3933 let content = lines.join("\n");
3934
3935 let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
3936
3937 editor.update(cx, |editor, cx| {
3938 let path = PathKey::for_buffer(&buffer, cx);
3939 editor.set_excerpts_for_path(
3940 path,
3941 buffer.clone(),
3942 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3943 0,
3944 diff.clone(),
3945 cx,
3946 );
3947 });
3948
3949 cx.run_until_parked();
3950
3951 let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
3952 let lhs = editor.lhs.as_ref().expect("should have lhs editor");
3953 (editor.rhs_editor.clone(), lhs.editor.clone())
3954 });
3955
3956 rhs_editor.update_in(cx, |e, window, cx| {
3957 e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
3958 });
3959
3960 let rhs_pos =
3961 rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3962 let lhs_pos =
3963 lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3964 assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
3965 assert_eq!(
3966 lhs_pos.y, rhs_pos.y,
3967 "LHS should have same scroll position as RHS after set_scroll_position"
3968 );
3969
3970 let draw_size = size(px(300.), px(300.));
3971
3972 rhs_editor.update_in(cx, |e, window, cx| {
3973 e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
3974 s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
3975 });
3976 });
3977
3978 let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
3979 cx.run_until_parked();
3980 let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
3981 cx.run_until_parked();
3982
3983 let rhs_pos =
3984 rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3985 let lhs_pos =
3986 lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3987
3988 assert!(
3989 rhs_pos.y > 0.,
3990 "RHS should have scrolled vertically to show cursor at row 25"
3991 );
3992 assert!(
3993 rhs_pos.x > 0.,
3994 "RHS should have scrolled horizontally to show cursor at column 150"
3995 );
3996 assert_eq!(
3997 lhs_pos.y, rhs_pos.y,
3998 "LHS should have same vertical scroll position as RHS after autoscroll"
3999 );
4000 assert_eq!(
4001 lhs_pos.x, rhs_pos.x,
4002 "LHS should have same horizontal scroll position as RHS after autoscroll"
4003 )
4004 }
4005
4006 #[gpui::test]
4007 async fn test_edit_line_before_soft_wrapped_line_preceding_hunk(cx: &mut gpui::TestAppContext) {
4008 use rope::Point;
4009 use unindent::Unindent as _;
4010
4011 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
4012
4013 let base_text = "
4014 first line
4015 aaaa bbbb cccc dddd eeee ffff
4016 original
4017 "
4018 .unindent();
4019
4020 let current_text = "
4021 first line
4022 aaaa bbbb cccc dddd eeee ffff
4023 modified
4024 "
4025 .unindent();
4026
4027 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4028
4029 editor.update(cx, |editor, cx| {
4030 let path = PathKey::for_buffer(&buffer, cx);
4031 editor.set_excerpts_for_path(
4032 path,
4033 buffer.clone(),
4034 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4035 0,
4036 diff.clone(),
4037 cx,
4038 );
4039 });
4040
4041 cx.run_until_parked();
4042
4043 assert_split_content_with_widths(
4044 &editor,
4045 px(400.0),
4046 px(200.0),
4047 "
4048 § <no file>
4049 § -----
4050 first line
4051 aaaa bbbb cccc dddd eeee ffff
4052 § spacer
4053 § spacer
4054 modified"
4055 .unindent(),
4056 "
4057 § <no file>
4058 § -----
4059 first line
4060 aaaa bbbb\x20
4061 cccc dddd\x20
4062 eeee ffff
4063 original"
4064 .unindent(),
4065 &mut cx,
4066 );
4067
4068 buffer.update(cx, |buffer, cx| {
4069 buffer.edit(
4070 [(Point::new(0, 0)..Point::new(0, 10), "edited first")],
4071 None,
4072 cx,
4073 );
4074 });
4075
4076 cx.run_until_parked();
4077
4078 assert_split_content_with_widths(
4079 &editor,
4080 px(400.0),
4081 px(200.0),
4082 "
4083 § <no file>
4084 § -----
4085 edited first
4086 aaaa bbbb cccc dddd eeee ffff
4087 § spacer
4088 § spacer
4089 modified"
4090 .unindent(),
4091 "
4092 § <no file>
4093 § -----
4094 first line
4095 aaaa bbbb\x20
4096 cccc dddd\x20
4097 eeee ffff
4098 original"
4099 .unindent(),
4100 &mut cx,
4101 );
4102
4103 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4104 diff.update(cx, |diff, cx| {
4105 diff.recalculate_diff_sync(&buffer_snapshot, cx);
4106 });
4107
4108 cx.run_until_parked();
4109
4110 assert_split_content_with_widths(
4111 &editor,
4112 px(400.0),
4113 px(200.0),
4114 "
4115 § <no file>
4116 § -----
4117 edited first
4118 aaaa bbbb cccc dddd eeee ffff
4119 § spacer
4120 § spacer
4121 modified"
4122 .unindent(),
4123 "
4124 § <no file>
4125 § -----
4126 first line
4127 aaaa bbbb\x20
4128 cccc dddd\x20
4129 eeee ffff
4130 original"
4131 .unindent(),
4132 &mut cx,
4133 );
4134 }
4135
4136 #[gpui::test]
4137 async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
4138 use rope::Point;
4139 use unindent::Unindent as _;
4140
4141 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4142
4143 let base_text = "
4144 bbb
4145 ccc
4146 "
4147 .unindent();
4148 let current_text = "
4149 aaa
4150 bbb
4151 ccc
4152 "
4153 .unindent();
4154
4155 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4156
4157 editor.update(cx, |editor, cx| {
4158 let path = PathKey::for_buffer(&buffer, cx);
4159 editor.set_excerpts_for_path(
4160 path,
4161 buffer.clone(),
4162 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4163 0,
4164 diff.clone(),
4165 cx,
4166 );
4167 });
4168
4169 cx.run_until_parked();
4170
4171 assert_split_content(
4172 &editor,
4173 "
4174 § <no file>
4175 § -----
4176 aaa
4177 bbb
4178 ccc"
4179 .unindent(),
4180 "
4181 § <no file>
4182 § -----
4183 § spacer
4184 bbb
4185 ccc"
4186 .unindent(),
4187 &mut cx,
4188 );
4189
4190 let block_ids = editor.update(cx, |splittable_editor, cx| {
4191 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4192 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4193 let anchor = snapshot.anchor_before(Point::new(2, 0));
4194 rhs_editor.insert_blocks(
4195 [BlockProperties {
4196 placement: BlockPlacement::Above(anchor),
4197 height: Some(1),
4198 style: BlockStyle::Fixed,
4199 render: Arc::new(|_| div().into_any()),
4200 priority: 0,
4201 }],
4202 None,
4203 cx,
4204 )
4205 })
4206 });
4207
4208 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4209 let lhs_editor =
4210 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4211
4212 cx.update(|_, cx| {
4213 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4214 "custom block".to_string()
4215 });
4216 });
4217
4218 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
4219 let display_map = lhs_editor.display_map.read(cx);
4220 let companion = display_map.companion().unwrap().read(cx);
4221 let mapping = companion
4222 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4223 *mapping.borrow().get(&block_ids[0]).unwrap()
4224 });
4225
4226 cx.update(|_, cx| {
4227 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
4228 "custom block".to_string()
4229 });
4230 });
4231
4232 cx.run_until_parked();
4233
4234 assert_split_content(
4235 &editor,
4236 "
4237 § <no file>
4238 § -----
4239 aaa
4240 bbb
4241 § custom block
4242 ccc"
4243 .unindent(),
4244 "
4245 § <no file>
4246 § -----
4247 § spacer
4248 bbb
4249 § custom block
4250 ccc"
4251 .unindent(),
4252 &mut cx,
4253 );
4254
4255 editor.update(cx, |splittable_editor, cx| {
4256 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4257 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
4258 });
4259 });
4260
4261 cx.run_until_parked();
4262
4263 assert_split_content(
4264 &editor,
4265 "
4266 § <no file>
4267 § -----
4268 aaa
4269 bbb
4270 ccc"
4271 .unindent(),
4272 "
4273 § <no file>
4274 § -----
4275 § spacer
4276 bbb
4277 ccc"
4278 .unindent(),
4279 &mut cx,
4280 );
4281 }
4282
4283 #[gpui::test]
4284 async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
4285 use rope::Point;
4286 use unindent::Unindent as _;
4287
4288 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4289
4290 let base_text = "
4291 bbb
4292 ccc
4293 "
4294 .unindent();
4295 let current_text = "
4296 aaa
4297 bbb
4298 ccc
4299 "
4300 .unindent();
4301
4302 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4303
4304 editor.update(cx, |editor, cx| {
4305 let path = PathKey::for_buffer(&buffer, cx);
4306 editor.set_excerpts_for_path(
4307 path,
4308 buffer.clone(),
4309 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4310 0,
4311 diff.clone(),
4312 cx,
4313 );
4314 });
4315
4316 cx.run_until_parked();
4317
4318 assert_split_content(
4319 &editor,
4320 "
4321 § <no file>
4322 § -----
4323 aaa
4324 bbb
4325 ccc"
4326 .unindent(),
4327 "
4328 § <no file>
4329 § -----
4330 § spacer
4331 bbb
4332 ccc"
4333 .unindent(),
4334 &mut cx,
4335 );
4336
4337 let block_ids = editor.update(cx, |splittable_editor, cx| {
4338 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4339 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4340 let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4341 let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4342 rhs_editor.insert_blocks(
4343 [
4344 BlockProperties {
4345 placement: BlockPlacement::Above(anchor1),
4346 height: Some(1),
4347 style: BlockStyle::Fixed,
4348 render: Arc::new(|_| div().into_any()),
4349 priority: 0,
4350 },
4351 BlockProperties {
4352 placement: BlockPlacement::Above(anchor2),
4353 height: Some(1),
4354 style: BlockStyle::Fixed,
4355 render: Arc::new(|_| div().into_any()),
4356 priority: 0,
4357 },
4358 ],
4359 None,
4360 cx,
4361 )
4362 })
4363 });
4364
4365 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4366 let lhs_editor =
4367 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4368
4369 cx.update(|_, cx| {
4370 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4371 "custom block 1".to_string()
4372 });
4373 set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4374 "custom block 2".to_string()
4375 });
4376 });
4377
4378 let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4379 let display_map = lhs_editor.display_map.read(cx);
4380 let companion = display_map.companion().unwrap().read(cx);
4381 let mapping = companion
4382 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4383 (
4384 *mapping.borrow().get(&block_ids[0]).unwrap(),
4385 *mapping.borrow().get(&block_ids[1]).unwrap(),
4386 )
4387 });
4388
4389 cx.update(|_, cx| {
4390 set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4391 "custom block 1".to_string()
4392 });
4393 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4394 "custom block 2".to_string()
4395 });
4396 });
4397
4398 cx.run_until_parked();
4399
4400 assert_split_content(
4401 &editor,
4402 "
4403 § <no file>
4404 § -----
4405 aaa
4406 bbb
4407 § custom block 1
4408 ccc
4409 § custom block 2"
4410 .unindent(),
4411 "
4412 § <no file>
4413 § -----
4414 § spacer
4415 bbb
4416 § custom block 1
4417 ccc
4418 § custom block 2"
4419 .unindent(),
4420 &mut cx,
4421 );
4422
4423 editor.update(cx, |splittable_editor, cx| {
4424 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4425 rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4426 });
4427 });
4428
4429 cx.run_until_parked();
4430
4431 assert_split_content(
4432 &editor,
4433 "
4434 § <no file>
4435 § -----
4436 aaa
4437 bbb
4438 ccc
4439 § custom block 2"
4440 .unindent(),
4441 "
4442 § <no file>
4443 § -----
4444 § spacer
4445 bbb
4446 ccc
4447 § custom block 2"
4448 .unindent(),
4449 &mut cx,
4450 );
4451
4452 editor.update_in(cx, |splittable_editor, window, cx| {
4453 splittable_editor.unsplit(window, cx);
4454 });
4455
4456 cx.run_until_parked();
4457
4458 editor.update_in(cx, |splittable_editor, window, cx| {
4459 splittable_editor.split(window, cx);
4460 });
4461
4462 cx.run_until_parked();
4463
4464 let lhs_editor =
4465 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4466
4467 let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4468 let display_map = lhs_editor.display_map.read(cx);
4469 let companion = display_map.companion().unwrap().read(cx);
4470 let mapping = companion
4471 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4472 *mapping.borrow().get(&block_ids[1]).unwrap()
4473 });
4474
4475 cx.update(|_, cx| {
4476 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4477 "custom block 2".to_string()
4478 });
4479 });
4480
4481 cx.run_until_parked();
4482
4483 assert_split_content(
4484 &editor,
4485 "
4486 § <no file>
4487 § -----
4488 aaa
4489 bbb
4490 ccc
4491 § custom block 2"
4492 .unindent(),
4493 "
4494 § <no file>
4495 § -----
4496 § spacer
4497 bbb
4498 ccc
4499 § custom block 2"
4500 .unindent(),
4501 &mut cx,
4502 );
4503 }
4504
4505 #[gpui::test]
4506 async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
4507 use rope::Point;
4508 use unindent::Unindent as _;
4509
4510 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4511
4512 let base_text = "
4513 bbb
4514 ccc
4515 "
4516 .unindent();
4517 let current_text = "
4518 aaa
4519 bbb
4520 ccc
4521 "
4522 .unindent();
4523
4524 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4525
4526 editor.update(cx, |editor, cx| {
4527 let path = PathKey::for_buffer(&buffer, cx);
4528 editor.set_excerpts_for_path(
4529 path,
4530 buffer.clone(),
4531 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4532 0,
4533 diff.clone(),
4534 cx,
4535 );
4536 });
4537
4538 cx.run_until_parked();
4539
4540 editor.update_in(cx, |splittable_editor, window, cx| {
4541 splittable_editor.unsplit(window, cx);
4542 });
4543
4544 cx.run_until_parked();
4545
4546 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4547
4548 let block_ids = editor.update(cx, |splittable_editor, cx| {
4549 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4550 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4551 let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4552 let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4553 rhs_editor.insert_blocks(
4554 [
4555 BlockProperties {
4556 placement: BlockPlacement::Above(anchor1),
4557 height: Some(1),
4558 style: BlockStyle::Fixed,
4559 render: Arc::new(|_| div().into_any()),
4560 priority: 0,
4561 },
4562 BlockProperties {
4563 placement: BlockPlacement::Above(anchor2),
4564 height: Some(1),
4565 style: BlockStyle::Fixed,
4566 render: Arc::new(|_| div().into_any()),
4567 priority: 0,
4568 },
4569 ],
4570 None,
4571 cx,
4572 )
4573 })
4574 });
4575
4576 cx.update(|_, cx| {
4577 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4578 "custom block 1".to_string()
4579 });
4580 set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4581 "custom block 2".to_string()
4582 });
4583 });
4584
4585 cx.run_until_parked();
4586
4587 let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
4588 assert_eq!(
4589 rhs_content,
4590 "
4591 § <no file>
4592 § -----
4593 aaa
4594 bbb
4595 § custom block 1
4596 ccc
4597 § custom block 2"
4598 .unindent(),
4599 "rhs content before split"
4600 );
4601
4602 editor.update_in(cx, |splittable_editor, window, cx| {
4603 splittable_editor.split(window, cx);
4604 });
4605
4606 cx.run_until_parked();
4607
4608 let lhs_editor =
4609 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4610
4611 let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4612 let display_map = lhs_editor.display_map.read(cx);
4613 let companion = display_map.companion().unwrap().read(cx);
4614 let mapping = companion
4615 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4616 (
4617 *mapping.borrow().get(&block_ids[0]).unwrap(),
4618 *mapping.borrow().get(&block_ids[1]).unwrap(),
4619 )
4620 });
4621
4622 cx.update(|_, cx| {
4623 set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4624 "custom block 1".to_string()
4625 });
4626 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4627 "custom block 2".to_string()
4628 });
4629 });
4630
4631 cx.run_until_parked();
4632
4633 assert_split_content(
4634 &editor,
4635 "
4636 § <no file>
4637 § -----
4638 aaa
4639 bbb
4640 § custom block 1
4641 ccc
4642 § custom block 2"
4643 .unindent(),
4644 "
4645 § <no file>
4646 § -----
4647 § spacer
4648 bbb
4649 § custom block 1
4650 ccc
4651 § custom block 2"
4652 .unindent(),
4653 &mut cx,
4654 );
4655
4656 editor.update(cx, |splittable_editor, cx| {
4657 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4658 rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4659 });
4660 });
4661
4662 cx.run_until_parked();
4663
4664 assert_split_content(
4665 &editor,
4666 "
4667 § <no file>
4668 § -----
4669 aaa
4670 bbb
4671 ccc
4672 § custom block 2"
4673 .unindent(),
4674 "
4675 § <no file>
4676 § -----
4677 § spacer
4678 bbb
4679 ccc
4680 § custom block 2"
4681 .unindent(),
4682 &mut cx,
4683 );
4684
4685 editor.update_in(cx, |splittable_editor, window, cx| {
4686 splittable_editor.unsplit(window, cx);
4687 });
4688
4689 cx.run_until_parked();
4690
4691 editor.update_in(cx, |splittable_editor, window, cx| {
4692 splittable_editor.split(window, cx);
4693 });
4694
4695 cx.run_until_parked();
4696
4697 let lhs_editor =
4698 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4699
4700 let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4701 let display_map = lhs_editor.display_map.read(cx);
4702 let companion = display_map.companion().unwrap().read(cx);
4703 let mapping = companion
4704 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4705 *mapping.borrow().get(&block_ids[1]).unwrap()
4706 });
4707
4708 cx.update(|_, cx| {
4709 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4710 "custom block 2".to_string()
4711 });
4712 });
4713
4714 cx.run_until_parked();
4715
4716 assert_split_content(
4717 &editor,
4718 "
4719 § <no file>
4720 § -----
4721 aaa
4722 bbb
4723 ccc
4724 § custom block 2"
4725 .unindent(),
4726 "
4727 § <no file>
4728 § -----
4729 § spacer
4730 bbb
4731 ccc
4732 § custom block 2"
4733 .unindent(),
4734 &mut cx,
4735 );
4736
4737 let new_block_ids = editor.update(cx, |splittable_editor, cx| {
4738 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4739 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4740 let anchor = snapshot.anchor_before(Point::new(2, 0));
4741 rhs_editor.insert_blocks(
4742 [BlockProperties {
4743 placement: BlockPlacement::Above(anchor),
4744 height: Some(1),
4745 style: BlockStyle::Fixed,
4746 render: Arc::new(|_| div().into_any()),
4747 priority: 0,
4748 }],
4749 None,
4750 cx,
4751 )
4752 })
4753 });
4754
4755 cx.update(|_, cx| {
4756 set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
4757 "custom block 3".to_string()
4758 });
4759 });
4760
4761 let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4762 let display_map = lhs_editor.display_map.read(cx);
4763 let companion = display_map.companion().unwrap().read(cx);
4764 let mapping = companion
4765 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4766 *mapping.borrow().get(&new_block_ids[0]).unwrap()
4767 });
4768
4769 cx.update(|_, cx| {
4770 set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
4771 "custom block 3".to_string()
4772 });
4773 });
4774
4775 cx.run_until_parked();
4776
4777 assert_split_content(
4778 &editor,
4779 "
4780 § <no file>
4781 § -----
4782 aaa
4783 bbb
4784 § custom block 3
4785 ccc
4786 § custom block 2"
4787 .unindent(),
4788 "
4789 § <no file>
4790 § -----
4791 § spacer
4792 bbb
4793 § custom block 3
4794 ccc
4795 § custom block 2"
4796 .unindent(),
4797 &mut cx,
4798 );
4799
4800 editor.update(cx, |splittable_editor, cx| {
4801 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4802 rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
4803 });
4804 });
4805
4806 cx.run_until_parked();
4807
4808 assert_split_content(
4809 &editor,
4810 "
4811 § <no file>
4812 § -----
4813 aaa
4814 bbb
4815 ccc
4816 § custom block 2"
4817 .unindent(),
4818 "
4819 § <no file>
4820 § -----
4821 § spacer
4822 bbb
4823 ccc
4824 § custom block 2"
4825 .unindent(),
4826 &mut cx,
4827 );
4828 }
4829
4830 #[gpui::test]
4831 async fn test_buffer_folding_sync(cx: &mut gpui::TestAppContext) {
4832 use rope::Point;
4833 use unindent::Unindent as _;
4834
4835 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
4836
4837 let base_text1 = "
4838 aaa
4839 bbb
4840 ccc"
4841 .unindent();
4842 let current_text1 = "
4843 aaa
4844 bbb
4845 ccc"
4846 .unindent();
4847
4848 let base_text2 = "
4849 ddd
4850 eee
4851 fff"
4852 .unindent();
4853 let current_text2 = "
4854 ddd
4855 eee
4856 fff"
4857 .unindent();
4858
4859 let (buffer1, diff1) = buffer_with_diff(&base_text1, ¤t_text1, &mut cx);
4860 let (buffer2, diff2) = buffer_with_diff(&base_text2, ¤t_text2, &mut cx);
4861
4862 let buffer1_id = buffer1.read_with(cx, |buffer, _| buffer.remote_id());
4863 let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
4864
4865 editor.update(cx, |editor, cx| {
4866 let path1 = PathKey::for_buffer(&buffer1, cx);
4867 editor.set_excerpts_for_path(
4868 path1,
4869 buffer1.clone(),
4870 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
4871 0,
4872 diff1.clone(),
4873 cx,
4874 );
4875 let path2 = PathKey::for_buffer(&buffer2, cx);
4876 editor.set_excerpts_for_path(
4877 path2,
4878 buffer2.clone(),
4879 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
4880 1,
4881 diff2.clone(),
4882 cx,
4883 );
4884 });
4885
4886 cx.run_until_parked();
4887
4888 editor.update(cx, |editor, cx| {
4889 editor.rhs_editor.update(cx, |rhs_editor, cx| {
4890 rhs_editor.fold_buffer(buffer1_id, cx);
4891 });
4892 });
4893
4894 cx.run_until_parked();
4895
4896 let rhs_buffer1_folded = editor.read_with(cx, |editor, cx| {
4897 editor.rhs_editor.read(cx).is_buffer_folded(buffer1_id, cx)
4898 });
4899 assert!(
4900 rhs_buffer1_folded,
4901 "buffer1 should be folded in rhs before split"
4902 );
4903
4904 editor.update_in(cx, |editor, window, cx| {
4905 editor.split(window, cx);
4906 });
4907
4908 cx.run_until_parked();
4909
4910 let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
4911 (
4912 editor.rhs_editor.clone(),
4913 editor.lhs.as_ref().unwrap().editor.clone(),
4914 )
4915 });
4916
4917 let rhs_buffer1_folded =
4918 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
4919 assert!(
4920 rhs_buffer1_folded,
4921 "buffer1 should be folded in rhs after split"
4922 );
4923
4924 let base_buffer1_id = diff1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
4925 let lhs_buffer1_folded = lhs_editor.read_with(cx, |editor, cx| {
4926 editor.is_buffer_folded(base_buffer1_id, cx)
4927 });
4928 assert!(
4929 lhs_buffer1_folded,
4930 "buffer1 should be folded in lhs after split"
4931 );
4932
4933 assert_split_content(
4934 &editor,
4935 "
4936 § <no file>
4937 § -----
4938 § <no file>
4939 § -----
4940 ddd
4941 eee
4942 fff"
4943 .unindent(),
4944 "
4945 § <no file>
4946 § -----
4947 § <no file>
4948 § -----
4949 ddd
4950 eee
4951 fff"
4952 .unindent(),
4953 &mut cx,
4954 );
4955
4956 editor.update(cx, |editor, cx| {
4957 editor.rhs_editor.update(cx, |rhs_editor, cx| {
4958 rhs_editor.fold_buffer(buffer2_id, cx);
4959 });
4960 });
4961
4962 cx.run_until_parked();
4963
4964 let rhs_buffer2_folded =
4965 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer2_id, cx));
4966 assert!(rhs_buffer2_folded, "buffer2 should be folded in rhs");
4967
4968 let base_buffer2_id = diff2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
4969 let lhs_buffer2_folded = lhs_editor.read_with(cx, |editor, cx| {
4970 editor.is_buffer_folded(base_buffer2_id, cx)
4971 });
4972 assert!(lhs_buffer2_folded, "buffer2 should be folded in lhs");
4973
4974 let rhs_buffer1_still_folded =
4975 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
4976 assert!(
4977 rhs_buffer1_still_folded,
4978 "buffer1 should still be folded in rhs"
4979 );
4980
4981 let lhs_buffer1_still_folded = lhs_editor.read_with(cx, |editor, cx| {
4982 editor.is_buffer_folded(base_buffer1_id, cx)
4983 });
4984 assert!(
4985 lhs_buffer1_still_folded,
4986 "buffer1 should still be folded in lhs"
4987 );
4988
4989 assert_split_content(
4990 &editor,
4991 "
4992 § <no file>
4993 § -----
4994 § <no file>
4995 § -----"
4996 .unindent(),
4997 "
4998 § <no file>
4999 § -----
5000 § <no file>
5001 § -----"
5002 .unindent(),
5003 &mut cx,
5004 );
5005 }
5006
5007 #[gpui::test]
5008 async fn test_custom_block_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5009 use rope::Point;
5010 use unindent::Unindent as _;
5011
5012 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5013
5014 let base_text = "
5015 ddd
5016 eee
5017 "
5018 .unindent();
5019 let current_text = "
5020 aaa
5021 bbb
5022 ccc
5023 ddd
5024 eee
5025 "
5026 .unindent();
5027
5028 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5029
5030 editor.update(cx, |editor, cx| {
5031 let path = PathKey::for_buffer(&buffer, cx);
5032 editor.set_excerpts_for_path(
5033 path,
5034 buffer.clone(),
5035 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5036 0,
5037 diff.clone(),
5038 cx,
5039 );
5040 });
5041
5042 cx.run_until_parked();
5043
5044 assert_split_content(
5045 &editor,
5046 "
5047 § <no file>
5048 § -----
5049 aaa
5050 bbb
5051 ccc
5052 ddd
5053 eee"
5054 .unindent(),
5055 "
5056 § <no file>
5057 § -----
5058 § spacer
5059 § spacer
5060 § spacer
5061 ddd
5062 eee"
5063 .unindent(),
5064 &mut cx,
5065 );
5066
5067 let block_ids = editor.update(cx, |splittable_editor, cx| {
5068 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5069 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5070 let anchor = snapshot.anchor_before(Point::new(2, 0));
5071 rhs_editor.insert_blocks(
5072 [BlockProperties {
5073 placement: BlockPlacement::Above(anchor),
5074 height: Some(1),
5075 style: BlockStyle::Fixed,
5076 render: Arc::new(|_| div().into_any()),
5077 priority: 0,
5078 }],
5079 None,
5080 cx,
5081 )
5082 })
5083 });
5084
5085 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5086 let lhs_editor =
5087 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5088
5089 cx.update(|_, cx| {
5090 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5091 "custom block".to_string()
5092 });
5093 });
5094
5095 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5096 let display_map = lhs_editor.display_map.read(cx);
5097 let companion = display_map.companion().unwrap().read(cx);
5098 let mapping = companion
5099 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5100 *mapping.borrow().get(&block_ids[0]).unwrap()
5101 });
5102
5103 cx.update(|_, cx| {
5104 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5105 "custom block".to_string()
5106 });
5107 });
5108
5109 cx.run_until_parked();
5110
5111 assert_split_content(
5112 &editor,
5113 "
5114 § <no file>
5115 § -----
5116 aaa
5117 bbb
5118 § custom block
5119 ccc
5120 ddd
5121 eee"
5122 .unindent(),
5123 "
5124 § <no file>
5125 § -----
5126 § spacer
5127 § spacer
5128 § spacer
5129 § custom block
5130 ddd
5131 eee"
5132 .unindent(),
5133 &mut cx,
5134 );
5135
5136 editor.update(cx, |splittable_editor, cx| {
5137 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5138 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5139 });
5140 });
5141
5142 cx.run_until_parked();
5143
5144 assert_split_content(
5145 &editor,
5146 "
5147 § <no file>
5148 § -----
5149 aaa
5150 bbb
5151 ccc
5152 ddd
5153 eee"
5154 .unindent(),
5155 "
5156 § <no file>
5157 § -----
5158 § spacer
5159 § spacer
5160 § spacer
5161 ddd
5162 eee"
5163 .unindent(),
5164 &mut cx,
5165 );
5166 }
5167
5168 #[gpui::test]
5169 async fn test_custom_block_below_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5170 use rope::Point;
5171 use unindent::Unindent as _;
5172
5173 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5174
5175 let base_text = "
5176 ddd
5177 eee
5178 "
5179 .unindent();
5180 let current_text = "
5181 aaa
5182 bbb
5183 ccc
5184 ddd
5185 eee
5186 "
5187 .unindent();
5188
5189 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5190
5191 editor.update(cx, |editor, cx| {
5192 let path = PathKey::for_buffer(&buffer, cx);
5193 editor.set_excerpts_for_path(
5194 path,
5195 buffer.clone(),
5196 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5197 0,
5198 diff.clone(),
5199 cx,
5200 );
5201 });
5202
5203 cx.run_until_parked();
5204
5205 assert_split_content(
5206 &editor,
5207 "
5208 § <no file>
5209 § -----
5210 aaa
5211 bbb
5212 ccc
5213 ddd
5214 eee"
5215 .unindent(),
5216 "
5217 § <no file>
5218 § -----
5219 § spacer
5220 § spacer
5221 § spacer
5222 ddd
5223 eee"
5224 .unindent(),
5225 &mut cx,
5226 );
5227
5228 let block_ids = editor.update(cx, |splittable_editor, cx| {
5229 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5230 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5231 let anchor = snapshot.anchor_after(Point::new(1, 3));
5232 rhs_editor.insert_blocks(
5233 [BlockProperties {
5234 placement: BlockPlacement::Below(anchor),
5235 height: Some(1),
5236 style: BlockStyle::Fixed,
5237 render: Arc::new(|_| div().into_any()),
5238 priority: 0,
5239 }],
5240 None,
5241 cx,
5242 )
5243 })
5244 });
5245
5246 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5247 let lhs_editor =
5248 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5249
5250 cx.update(|_, cx| {
5251 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5252 "custom block".to_string()
5253 });
5254 });
5255
5256 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5257 let display_map = lhs_editor.display_map.read(cx);
5258 let companion = display_map.companion().unwrap().read(cx);
5259 let mapping = companion
5260 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5261 *mapping.borrow().get(&block_ids[0]).unwrap()
5262 });
5263
5264 cx.update(|_, cx| {
5265 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5266 "custom block".to_string()
5267 });
5268 });
5269
5270 cx.run_until_parked();
5271
5272 assert_split_content(
5273 &editor,
5274 "
5275 § <no file>
5276 § -----
5277 aaa
5278 bbb
5279 § custom block
5280 ccc
5281 ddd
5282 eee"
5283 .unindent(),
5284 "
5285 § <no file>
5286 § -----
5287 § spacer
5288 § spacer
5289 § spacer
5290 § custom block
5291 ddd
5292 eee"
5293 .unindent(),
5294 &mut cx,
5295 );
5296
5297 editor.update(cx, |splittable_editor, cx| {
5298 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5299 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5300 });
5301 });
5302
5303 cx.run_until_parked();
5304
5305 assert_split_content(
5306 &editor,
5307 "
5308 § <no file>
5309 § -----
5310 aaa
5311 bbb
5312 ccc
5313 ddd
5314 eee"
5315 .unindent(),
5316 "
5317 § <no file>
5318 § -----
5319 § spacer
5320 § spacer
5321 § spacer
5322 ddd
5323 eee"
5324 .unindent(),
5325 &mut cx,
5326 );
5327 }
5328
5329 #[gpui::test]
5330 async fn test_custom_block_resize_syncs_balancing_block(cx: &mut gpui::TestAppContext) {
5331 use rope::Point;
5332 use unindent::Unindent as _;
5333
5334 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5335
5336 let base_text = "
5337 bbb
5338 ccc
5339 "
5340 .unindent();
5341 let current_text = "
5342 aaa
5343 bbb
5344 ccc
5345 "
5346 .unindent();
5347
5348 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5349
5350 editor.update(cx, |editor, cx| {
5351 let path = PathKey::for_buffer(&buffer, cx);
5352 editor.set_excerpts_for_path(
5353 path,
5354 buffer.clone(),
5355 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5356 0,
5357 diff.clone(),
5358 cx,
5359 );
5360 });
5361
5362 cx.run_until_parked();
5363
5364 let block_ids = editor.update(cx, |splittable_editor, cx| {
5365 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5366 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5367 let anchor = snapshot.anchor_before(Point::new(2, 0));
5368 rhs_editor.insert_blocks(
5369 [BlockProperties {
5370 placement: BlockPlacement::Above(anchor),
5371 height: Some(1),
5372 style: BlockStyle::Fixed,
5373 render: Arc::new(|_| div().into_any()),
5374 priority: 0,
5375 }],
5376 None,
5377 cx,
5378 )
5379 })
5380 });
5381
5382 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5383 let lhs_editor =
5384 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5385
5386 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5387 let display_map = lhs_editor.display_map.read(cx);
5388 let companion = display_map.companion().unwrap().read(cx);
5389 let mapping = companion
5390 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5391 *mapping.borrow().get(&block_ids[0]).unwrap()
5392 });
5393
5394 cx.run_until_parked();
5395
5396 let get_block_height = |editor: &Entity<crate::Editor>,
5397 block_id: crate::CustomBlockId,
5398 cx: &mut VisualTestContext| {
5399 editor.update_in(cx, |editor, window, cx| {
5400 let snapshot = editor.snapshot(window, cx);
5401 snapshot
5402 .block_for_id(crate::BlockId::Custom(block_id))
5403 .map(|block| block.height())
5404 })
5405 };
5406
5407 assert_eq!(
5408 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5409 Some(1)
5410 );
5411 assert_eq!(
5412 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5413 Some(1)
5414 );
5415
5416 editor.update(cx, |splittable_editor, cx| {
5417 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5418 let mut heights = HashMap::default();
5419 heights.insert(block_ids[0], 3);
5420 rhs_editor.resize_blocks(heights, None, cx);
5421 });
5422 });
5423
5424 cx.run_until_parked();
5425
5426 assert_eq!(
5427 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5428 Some(3)
5429 );
5430 assert_eq!(
5431 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5432 Some(3)
5433 );
5434
5435 editor.update(cx, |splittable_editor, cx| {
5436 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5437 let mut heights = HashMap::default();
5438 heights.insert(block_ids[0], 5);
5439 rhs_editor.resize_blocks(heights, None, cx);
5440 });
5441 });
5442
5443 cx.run_until_parked();
5444
5445 assert_eq!(
5446 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5447 Some(5)
5448 );
5449 assert_eq!(
5450 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5451 Some(5)
5452 );
5453 }
5454}