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