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