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 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 self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
617 lhs.multibuffer.update(cx, |lhs_multibuffer, lhs_cx| {
618 for (lhs, rhs) in LhsEditor::update_path_excerpts_from_rhs(
619 path.clone(),
620 rhs_multibuffer,
621 lhs_multibuffer,
622 diff.clone(),
623 lhs_cx,
624 ) {
625 companion.add_excerpt_mapping(lhs, rhs);
626 }
627 companion.add_buffer_mapping(
628 diff.read(lhs_cx).base_text(lhs_cx).remote_id(),
629 diff.read(lhs_cx).buffer_id,
630 );
631 })
632 });
633 }
634
635 let companion = cx.new(|_| companion);
636
637 rhs_display_map.update(cx, |dm, cx| {
638 dm.set_companion(Some((lhs_display_map, companion.clone())), cx);
639 });
640
641 let shared_scroll_anchor = self
642 .rhs_editor
643 .read(cx)
644 .scroll_manager
645 .scroll_anchor_entity();
646 lhs.editor.update(cx, |editor, _cx| {
647 editor
648 .scroll_manager
649 .set_shared_scroll_anchor(shared_scroll_anchor);
650 });
651
652 let this = cx.entity().downgrade();
653 self.rhs_editor.update(cx, |editor, _cx| {
654 let this = this.clone();
655 editor.set_on_local_selections_changed(Some(Box::new(
656 move |cursor_position, window, cx| {
657 let this = this.clone();
658 window.defer(cx, move |window, cx| {
659 this.update(cx, |this, cx| {
660 this.sync_cursor_to_other_side(true, cursor_position, window, cx);
661 })
662 .ok();
663 })
664 },
665 )));
666 });
667 lhs.editor.update(cx, |editor, _cx| {
668 let this = this.clone();
669 editor.set_on_local_selections_changed(Some(Box::new(
670 move |cursor_position, window, cx| {
671 let this = this.clone();
672 window.defer(cx, move |window, cx| {
673 this.update(cx, |this, cx| {
674 this.sync_cursor_to_other_side(false, cursor_position, window, cx);
675 })
676 .ok();
677 })
678 },
679 )));
680 });
681
682 // Copy soft wrap state from rhs (source of truth) to lhs
683 let rhs_soft_wrap_override = self.rhs_editor.read(cx).soft_wrap_mode_override;
684 lhs.editor.update(cx, |editor, cx| {
685 editor.soft_wrap_mode_override = rhs_soft_wrap_override;
686 cx.notify();
687 });
688
689 self.lhs = Some(lhs);
690
691 cx.notify();
692 }
693
694 fn activate_pane_left(
695 &mut self,
696 _: &ActivatePaneLeft,
697 window: &mut Window,
698 cx: &mut Context<Self>,
699 ) {
700 if let Some(lhs) = &self.lhs {
701 if !lhs.was_last_focused {
702 lhs.editor.read(cx).focus_handle(cx).focus(window, cx);
703 lhs.editor.update(cx, |editor, cx| {
704 editor.request_autoscroll(Autoscroll::fit(), cx);
705 });
706 } else {
707 cx.propagate();
708 }
709 } else {
710 cx.propagate();
711 }
712 }
713
714 fn activate_pane_right(
715 &mut self,
716 _: &ActivatePaneRight,
717 window: &mut Window,
718 cx: &mut Context<Self>,
719 ) {
720 if let Some(lhs) = &self.lhs {
721 if lhs.was_last_focused {
722 self.rhs_editor.read(cx).focus_handle(cx).focus(window, cx);
723 self.rhs_editor.update(cx, |editor, cx| {
724 editor.request_autoscroll(Autoscroll::fit(), cx);
725 });
726 } else {
727 cx.propagate();
728 }
729 } else {
730 cx.propagate();
731 }
732 }
733
734 fn sync_cursor_to_other_side(
735 &mut self,
736 from_rhs: bool,
737 source_point: Point,
738 window: &mut Window,
739 cx: &mut Context<Self>,
740 ) {
741 let Some(lhs) = &self.lhs else {
742 return;
743 };
744
745 let (source_editor, target_editor) = if from_rhs {
746 (&self.rhs_editor, &lhs.editor)
747 } else {
748 (&lhs.editor, &self.rhs_editor)
749 };
750
751 let source_snapshot = source_editor.update(cx, |editor, cx| editor.snapshot(window, cx));
752 let target_snapshot = target_editor.update(cx, |editor, cx| editor.snapshot(window, cx));
753
754 let display_point = source_snapshot
755 .display_snapshot
756 .point_to_display_point(source_point, Bias::Right);
757 let display_point = target_snapshot.clip_point(display_point, Bias::Right);
758 let target_point = target_snapshot.display_point_to_point(display_point, Bias::Right);
759
760 target_editor.update(cx, |editor, cx| {
761 editor.set_suppress_selection_callback(true);
762 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
763 s.select_ranges([target_point..target_point]);
764 });
765 editor.set_suppress_selection_callback(false);
766 });
767 }
768
769 fn toggle_split(&mut self, _: &ToggleDiffView, window: &mut Window, cx: &mut Context<Self>) {
770 if self.lhs.is_some() {
771 self.unsplit(window, cx);
772 } else {
773 self.split(window, cx);
774 }
775 }
776
777 fn intercept_toggle_breakpoint(
778 &mut self,
779 _: &ToggleBreakpoint,
780 _window: &mut Window,
781 cx: &mut Context<Self>,
782 ) {
783 // Only block breakpoint actions when the left (lhs) editor has focus
784 if let Some(lhs) = &self.lhs {
785 if lhs.was_last_focused {
786 cx.stop_propagation();
787 } else {
788 cx.propagate();
789 }
790 } else {
791 cx.propagate();
792 }
793 }
794
795 fn intercept_enable_breakpoint(
796 &mut self,
797 _: &EnableBreakpoint,
798 _window: &mut Window,
799 cx: &mut Context<Self>,
800 ) {
801 // Only block breakpoint actions when the left (lhs) editor has focus
802 if let Some(lhs) = &self.lhs {
803 if lhs.was_last_focused {
804 cx.stop_propagation();
805 } else {
806 cx.propagate();
807 }
808 } else {
809 cx.propagate();
810 }
811 }
812
813 fn intercept_disable_breakpoint(
814 &mut self,
815 _: &DisableBreakpoint,
816 _window: &mut Window,
817 cx: &mut Context<Self>,
818 ) {
819 // Only block breakpoint actions when the left (lhs) editor has focus
820 if let Some(lhs) = &self.lhs {
821 if lhs.was_last_focused {
822 cx.stop_propagation();
823 } else {
824 cx.propagate();
825 }
826 } else {
827 cx.propagate();
828 }
829 }
830
831 fn intercept_edit_log_breakpoint(
832 &mut self,
833 _: &EditLogBreakpoint,
834 _window: &mut Window,
835 cx: &mut Context<Self>,
836 ) {
837 // Only block breakpoint actions when the left (lhs) editor has focus
838 if let Some(lhs) = &self.lhs {
839 if lhs.was_last_focused {
840 cx.stop_propagation();
841 } else {
842 cx.propagate();
843 }
844 } else {
845 cx.propagate();
846 }
847 }
848
849 fn intercept_inline_assist(
850 &mut self,
851 _: &InlineAssist,
852 _window: &mut Window,
853 cx: &mut Context<Self>,
854 ) {
855 if self.lhs.is_some() {
856 cx.stop_propagation();
857 } else {
858 cx.propagate();
859 }
860 }
861
862 fn toggle_soft_wrap(
863 &mut self,
864 _: &ToggleSoftWrap,
865 window: &mut Window,
866 cx: &mut Context<Self>,
867 ) {
868 if let Some(lhs) = &self.lhs {
869 cx.stop_propagation();
870
871 let is_lhs_focused = lhs.was_last_focused;
872 let (focused_editor, other_editor) = if is_lhs_focused {
873 (&lhs.editor, &self.rhs_editor)
874 } else {
875 (&self.rhs_editor, &lhs.editor)
876 };
877
878 // Toggle the focused editor
879 focused_editor.update(cx, |editor, cx| {
880 editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
881 });
882
883 // Copy the soft wrap state from the focused editor to the other editor
884 let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
885 other_editor.update(cx, |editor, cx| {
886 editor.soft_wrap_mode_override = soft_wrap_override;
887 cx.notify();
888 });
889 } else {
890 cx.propagate();
891 }
892 }
893
894 fn unsplit(&mut self, _: &mut Window, cx: &mut Context<Self>) {
895 let Some(lhs) = self.lhs.take() else {
896 return;
897 };
898 self.rhs_editor.update(cx, |rhs, cx| {
899 let rhs_snapshot = rhs.display_map.update(cx, |dm, cx| dm.snapshot(cx));
900 let native_anchor = rhs.scroll_manager.native_anchor(&rhs_snapshot, cx);
901 let rhs_display_map_id = rhs_snapshot.display_map_id;
902 rhs.scroll_manager
903 .scroll_anchor_entity()
904 .update(cx, |shared, _| {
905 shared.scroll_anchor = native_anchor;
906 shared.display_map_id = Some(rhs_display_map_id);
907 });
908
909 rhs.set_on_local_selections_changed(None);
910 rhs.set_delegate_expand_excerpts(false);
911 rhs.buffer().update(cx, |buffer, cx| {
912 buffer.set_show_deleted_hunks(true, cx);
913 buffer.set_use_extended_diff_range(false, cx);
914 });
915 rhs.display_map.update(cx, |dm, cx| {
916 dm.set_companion(None, cx);
917 });
918 });
919 lhs.editor.update(cx, |editor, _cx| {
920 editor.set_on_local_selections_changed(None);
921 });
922 cx.notify();
923 }
924
925 pub fn set_excerpts_for_path(
926 &mut self,
927 path: PathKey,
928 buffer: Entity<Buffer>,
929 ranges: impl IntoIterator<Item = Range<Point>> + Clone,
930 context_line_count: u32,
931 diff: Entity<BufferDiff>,
932 cx: &mut Context<Self>,
933 ) -> (Vec<Range<Anchor>>, bool) {
934 let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
935 let lhs = &mut self.lhs;
936 let (anchors, added_a_new_excerpt) =
937 self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
938 let (anchors, added_a_new_excerpt) = rhs_multibuffer.set_excerpts_for_path(
939 path.clone(),
940 buffer.clone(),
941 ranges,
942 context_line_count,
943 cx,
944 );
945 if !anchors.is_empty()
946 && rhs_multibuffer
947 .diff_for(buffer.read(cx).remote_id())
948 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
949 {
950 rhs_multibuffer.add_diff(diff.clone(), cx);
951 }
952
953 if let Some(lhs) = lhs {
954 lhs.multibuffer.update(cx, |lhs_multibuffer, lhs_cx| {
955 LhsEditor::sync_path_excerpts(
956 path,
957 rhs_multibuffer,
958 lhs_multibuffer,
959 diff,
960 &rhs_display_map,
961 lhs_cx,
962 )
963 })
964 }
965
966 (anchors, added_a_new_excerpt)
967 });
968
969 (anchors, added_a_new_excerpt)
970 }
971
972 fn expand_excerpts(
973 &mut self,
974 excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
975 lines: u32,
976 direction: ExpandExcerptDirection,
977 cx: &mut Context<Self>,
978 ) {
979 let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
980
981 let lhs = &mut self.lhs;
982 let lhs_multibuffer = lhs.as_ref().map(|l| l.multibuffer.clone());
983 self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
984 if let Some(lhs_multibuffer) = lhs_multibuffer {
985 let snapshot = rhs_multibuffer.snapshot(cx);
986 let affected_paths: HashMap<_, _> = excerpt_ids
987 .clone()
988 .map(|excerpt_id| {
989 let path = rhs_multibuffer.path_for_excerpt(excerpt_id).unwrap();
990 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
991 let diff = rhs_multibuffer.diff_for(buffer.remote_id()).unwrap();
992 (path, diff)
993 })
994 .collect();
995 rhs_multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
996 for (path, diff) in affected_paths {
997 lhs_multibuffer.update(cx, |lhs_multibuffer, lhs_cx| {
998 LhsEditor::sync_path_excerpts(
999 path,
1000 rhs_multibuffer,
1001 lhs_multibuffer,
1002 diff,
1003 &rhs_display_map,
1004 lhs_cx,
1005 );
1006 });
1007 }
1008 } else {
1009 rhs_multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
1010 };
1011 });
1012 }
1013
1014 pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
1015 let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
1016
1017 if let Some(lhs) = &self.lhs {
1018 self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1019 lhs.multibuffer.update(cx, |lhs_multibuffer, cx| {
1020 LhsEditor::remove_mappings_for_path(
1021 &path,
1022 rhs_multibuffer,
1023 lhs_multibuffer,
1024 &rhs_display_map,
1025 cx,
1026 );
1027 });
1028 rhs_multibuffer.remove_excerpts_for_path(path.clone(), cx);
1029 });
1030 lhs.multibuffer.update(cx, |lhs_multibuffer, cx| {
1031 lhs_multibuffer.remove_excerpts_for_path(path, cx);
1032 });
1033 } else {
1034 self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1035 rhs_multibuffer.remove_excerpts_for_path(path.clone(), cx);
1036 });
1037 }
1038 }
1039}
1040
1041#[cfg(test)]
1042impl SplittableEditor {
1043 fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1044 use multi_buffer::MultiBufferRow;
1045 use text::Bias;
1046
1047 use crate::display_map::Block;
1048 use crate::display_map::DisplayRow;
1049
1050 self.debug_print(cx);
1051
1052 let lhs = self.lhs.as_ref().unwrap();
1053 let rhs_excerpts = self.rhs_multibuffer.read(cx).excerpt_ids();
1054 let lhs_excerpts = lhs.multibuffer.read(cx).excerpt_ids();
1055 assert_eq!(
1056 lhs_excerpts.len(),
1057 rhs_excerpts.len(),
1058 "mismatch in excerpt count"
1059 );
1060
1061 if quiesced {
1062 let rhs_snapshot = lhs
1063 .editor
1064 .update(cx, |editor, cx| editor.display_snapshot(cx));
1065 let lhs_snapshot = self
1066 .rhs_editor
1067 .update(cx, |editor, cx| editor.display_snapshot(cx));
1068
1069 let lhs_max_row = lhs_snapshot.max_point().row();
1070 let rhs_max_row = rhs_snapshot.max_point().row();
1071 assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1072
1073 let lhs_excerpt_block_rows = lhs_snapshot
1074 .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1075 .filter(|(_, block)| {
1076 matches!(
1077 block,
1078 Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1079 )
1080 })
1081 .map(|(row, _)| row)
1082 .collect::<Vec<_>>();
1083 let rhs_excerpt_block_rows = rhs_snapshot
1084 .blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
1085 .filter(|(_, block)| {
1086 matches!(
1087 block,
1088 Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1089 )
1090 })
1091 .map(|(row, _)| row)
1092 .collect::<Vec<_>>();
1093 assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1094
1095 for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1096 assert_eq!(
1097 lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1098 "mismatch in hunks"
1099 );
1100 assert_eq!(
1101 lhs_hunk.status, rhs_hunk.status,
1102 "mismatch in hunk statuses"
1103 );
1104
1105 let (lhs_point, rhs_point) =
1106 if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1107 (
1108 Point::new(lhs_hunk.row_range.end.0, 0),
1109 Point::new(rhs_hunk.row_range.end.0, 0),
1110 )
1111 } else {
1112 (
1113 Point::new(lhs_hunk.row_range.start.0, 0),
1114 Point::new(rhs_hunk.row_range.start.0, 0),
1115 )
1116 };
1117 let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1118 let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1119 assert_eq!(
1120 lhs_point.row(),
1121 rhs_point.row(),
1122 "mismatch in hunk position"
1123 );
1124 }
1125
1126 // Filtering out empty lines is a bit of a hack, to work around a case where
1127 // the base text has a trailing newline but the current text doesn't, or vice versa.
1128 // In this case, we get the additional newline on one side, but that line is not
1129 // marked as added/deleted by rowinfos.
1130 self.check_sides_match(cx, |snapshot| {
1131 snapshot
1132 .buffer_snapshot()
1133 .text()
1134 .split("\n")
1135 .zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
1136 .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
1137 .map(|(line, _)| line.to_owned())
1138 .collect::<Vec<_>>()
1139 });
1140 }
1141 }
1142
1143 #[track_caller]
1144 fn check_sides_match<T: std::fmt::Debug + PartialEq>(
1145 &self,
1146 cx: &mut App,
1147 mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
1148 ) {
1149 let lhs = self.lhs.as_ref().expect("requires split");
1150 let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1151 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1152 });
1153 let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1154 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1155 });
1156
1157 let rhs_t = extract(&rhs_snapshot);
1158 let lhs_t = extract(&lhs_snapshot);
1159
1160 if rhs_t != lhs_t {
1161 self.debug_print(cx);
1162 pretty_assertions::assert_eq!(rhs_t, lhs_t);
1163 }
1164 }
1165
1166 fn debug_print(&self, cx: &mut App) {
1167 use crate::DisplayRow;
1168 use crate::display_map::Block;
1169 use buffer_diff::DiffHunkStatusKind;
1170
1171 assert!(
1172 self.lhs.is_some(),
1173 "debug_print is only useful when lhs editor exists"
1174 );
1175
1176 let lhs = self.lhs.as_ref().unwrap();
1177
1178 // Get terminal width, default to 80 if unavailable
1179 let terminal_width = std::env::var("COLUMNS")
1180 .ok()
1181 .and_then(|s| s.parse::<usize>().ok())
1182 .unwrap_or(80);
1183
1184 // Each side gets half the terminal width minus the separator
1185 let separator = " │ ";
1186 let side_width = (terminal_width - separator.len()) / 2;
1187
1188 // Get display snapshots for both editors
1189 let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1190 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1191 });
1192 let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1193 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1194 });
1195
1196 let lhs_max_row = lhs_snapshot.max_point().row().0;
1197 let rhs_max_row = rhs_snapshot.max_point().row().0;
1198 let max_row = lhs_max_row.max(rhs_max_row);
1199
1200 // Build a map from display row -> block type string
1201 // Each row of a multi-row block gets an entry with the same block type
1202 // For spacers, the ID is included in brackets
1203 fn build_block_map(
1204 snapshot: &crate::DisplaySnapshot,
1205 max_row: u32,
1206 ) -> std::collections::HashMap<u32, String> {
1207 let mut block_map = std::collections::HashMap::new();
1208 for (start_row, block) in
1209 snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1210 {
1211 let (block_type, height) = match block {
1212 Block::Spacer {
1213 id,
1214 height,
1215 is_below: _,
1216 } => (format!("SPACER[{}]", id.0), *height),
1217 Block::ExcerptBoundary { height, .. } => {
1218 ("EXCERPT_BOUNDARY".to_string(), *height)
1219 }
1220 Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1221 Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1222 Block::Custom(custom) => {
1223 ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1224 }
1225 };
1226 for offset in 0..height {
1227 block_map.insert(start_row.0 + offset, block_type.clone());
1228 }
1229 }
1230 block_map
1231 }
1232
1233 let lhs_blocks = build_block_map(&lhs_snapshot, lhs_max_row);
1234 let rhs_blocks = build_block_map(&rhs_snapshot, rhs_max_row);
1235
1236 fn display_width(s: &str) -> usize {
1237 unicode_width::UnicodeWidthStr::width(s)
1238 }
1239
1240 fn truncate_line(line: &str, max_width: usize) -> String {
1241 let line_width = display_width(line);
1242 if line_width <= max_width {
1243 return line.to_string();
1244 }
1245 if max_width < 9 {
1246 let mut result = String::new();
1247 let mut width = 0;
1248 for c in line.chars() {
1249 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1250 if width + c_width > max_width {
1251 break;
1252 }
1253 result.push(c);
1254 width += c_width;
1255 }
1256 return result;
1257 }
1258 let ellipsis = "...";
1259 let target_prefix_width = 3;
1260 let target_suffix_width = 3;
1261
1262 let mut prefix = String::new();
1263 let mut prefix_width = 0;
1264 for c in line.chars() {
1265 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1266 if prefix_width + c_width > target_prefix_width {
1267 break;
1268 }
1269 prefix.push(c);
1270 prefix_width += c_width;
1271 }
1272
1273 let mut suffix_chars: Vec<char> = Vec::new();
1274 let mut suffix_width = 0;
1275 for c in line.chars().rev() {
1276 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1277 if suffix_width + c_width > target_suffix_width {
1278 break;
1279 }
1280 suffix_chars.push(c);
1281 suffix_width += c_width;
1282 }
1283 suffix_chars.reverse();
1284 let suffix: String = suffix_chars.into_iter().collect();
1285
1286 format!("{}{}{}", prefix, ellipsis, suffix)
1287 }
1288
1289 fn pad_to_width(s: &str, target_width: usize) -> String {
1290 let current_width = display_width(s);
1291 if current_width >= target_width {
1292 s.to_string()
1293 } else {
1294 format!("{}{}", s, " ".repeat(target_width - current_width))
1295 }
1296 }
1297
1298 // Helper to format a single row for one side
1299 // Format: "ln# diff bytes(cumul) text" or block info
1300 // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1301 fn format_row(
1302 row: u32,
1303 max_row: u32,
1304 snapshot: &crate::DisplaySnapshot,
1305 blocks: &std::collections::HashMap<u32, String>,
1306 row_infos: &[multi_buffer::RowInfo],
1307 cumulative_bytes: &[usize],
1308 side_width: usize,
1309 ) -> String {
1310 // Get row info if available
1311 let row_info = row_infos.get(row as usize);
1312
1313 // Line number prefix (3 chars + space)
1314 // Use buffer_row from RowInfo, which is None for block rows
1315 let line_prefix = if row > max_row {
1316 " ".to_string()
1317 } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1318 format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1319 } else {
1320 " ".to_string() // block rows have no line number
1321 };
1322 let content_width = side_width.saturating_sub(line_prefix.len());
1323
1324 if row > max_row {
1325 return format!("{}{}", line_prefix, " ".repeat(content_width));
1326 }
1327
1328 // Check if this row is a block row
1329 if let Some(block_type) = blocks.get(&row) {
1330 let block_str = format!("~~~[{}]~~~", block_type);
1331 let formatted = format!("{:^width$}", block_str, width = content_width);
1332 return format!(
1333 "{}{}",
1334 line_prefix,
1335 truncate_line(&formatted, content_width)
1336 );
1337 }
1338
1339 // Get line text
1340 let line_text = snapshot.line(DisplayRow(row));
1341 let line_bytes = line_text.len();
1342
1343 // Diff status marker
1344 let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1345 Some(status) => match status.kind {
1346 DiffHunkStatusKind::Added => "+",
1347 DiffHunkStatusKind::Deleted => "-",
1348 DiffHunkStatusKind::Modified => "~",
1349 },
1350 None => " ",
1351 };
1352
1353 // Cumulative bytes
1354 let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1355
1356 // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1357 let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1358 let text_width = content_width.saturating_sub(info_prefix.len());
1359 let truncated_text = truncate_line(&line_text, text_width);
1360
1361 let text_part = pad_to_width(&truncated_text, text_width);
1362 format!("{}{}{}", line_prefix, info_prefix, text_part)
1363 }
1364
1365 // Collect row infos for both sides
1366 let lhs_row_infos: Vec<_> = lhs_snapshot
1367 .row_infos(DisplayRow(0))
1368 .take((lhs_max_row + 1) as usize)
1369 .collect();
1370 let rhs_row_infos: Vec<_> = rhs_snapshot
1371 .row_infos(DisplayRow(0))
1372 .take((rhs_max_row + 1) as usize)
1373 .collect();
1374
1375 // Calculate cumulative bytes for each side (only counting non-block rows)
1376 let mut lhs_cumulative = Vec::with_capacity((lhs_max_row + 1) as usize);
1377 let mut cumulative = 0usize;
1378 for row in 0..=lhs_max_row {
1379 if !lhs_blocks.contains_key(&row) {
1380 cumulative += lhs_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1381 }
1382 lhs_cumulative.push(cumulative);
1383 }
1384
1385 let mut rhs_cumulative = Vec::with_capacity((rhs_max_row + 1) as usize);
1386 cumulative = 0;
1387 for row in 0..=rhs_max_row {
1388 if !rhs_blocks.contains_key(&row) {
1389 cumulative += rhs_snapshot.line(DisplayRow(row)).len() + 1;
1390 }
1391 rhs_cumulative.push(cumulative);
1392 }
1393
1394 // Print header
1395 eprintln!();
1396 eprintln!("{}", "═".repeat(terminal_width));
1397 let header_left = format!("{:^width$}", "(LHS)", width = side_width);
1398 let header_right = format!("{:^width$}", "(RHS)", width = side_width);
1399 eprintln!("{}{}{}", header_left, separator, header_right);
1400 eprintln!(
1401 "{:^width$}{}{:^width$}",
1402 "ln# diff len(cum) text",
1403 separator,
1404 "ln# diff len(cum) text",
1405 width = side_width
1406 );
1407 eprintln!("{}", "─".repeat(terminal_width));
1408
1409 // Print each row
1410 for row in 0..=max_row {
1411 let left = format_row(
1412 row,
1413 lhs_max_row,
1414 &lhs_snapshot,
1415 &lhs_blocks,
1416 &lhs_row_infos,
1417 &lhs_cumulative,
1418 side_width,
1419 );
1420 let right = format_row(
1421 row,
1422 rhs_max_row,
1423 &rhs_snapshot,
1424 &rhs_blocks,
1425 &rhs_row_infos,
1426 &rhs_cumulative,
1427 side_width,
1428 );
1429 eprintln!("{}{}{}", left, separator, right);
1430 }
1431
1432 eprintln!("{}", "═".repeat(terminal_width));
1433 eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1434 eprintln!();
1435 }
1436
1437 fn randomly_edit_excerpts(
1438 &mut self,
1439 rng: &mut impl rand::Rng,
1440 mutation_count: usize,
1441 cx: &mut Context<Self>,
1442 ) {
1443 use collections::HashSet;
1444 use rand::prelude::*;
1445 use std::env;
1446 use util::RandomCharIter;
1447
1448 let max_buffers = env::var("MAX_BUFFERS")
1449 .map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
1450 .unwrap_or(4);
1451
1452 for _ in 0..mutation_count {
1453 let paths = self
1454 .rhs_multibuffer
1455 .read(cx)
1456 .paths()
1457 .cloned()
1458 .collect::<Vec<_>>();
1459 let excerpt_ids = self.rhs_multibuffer.read(cx).excerpt_ids();
1460
1461 if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
1462 let mut excerpts = HashSet::default();
1463 for _ in 0..rng.random_range(0..excerpt_ids.len()) {
1464 excerpts.extend(excerpt_ids.choose(rng).copied());
1465 }
1466
1467 let line_count = rng.random_range(1..5);
1468
1469 log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
1470
1471 self.expand_excerpts(
1472 excerpts.iter().cloned(),
1473 line_count,
1474 ExpandExcerptDirection::UpAndDown,
1475 cx,
1476 );
1477 continue;
1478 }
1479
1480 if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
1481 let len = rng.random_range(100..500);
1482 let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
1483 let buffer = cx.new(|cx| Buffer::local(text, cx));
1484 log::info!(
1485 "Creating new buffer {} with text: {:?}",
1486 buffer.read(cx).remote_id(),
1487 buffer.read(cx).text()
1488 );
1489 let buffer_snapshot = buffer.read(cx).snapshot();
1490 let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
1491 // Create some initial diff hunks.
1492 buffer.update(cx, |buffer, cx| {
1493 buffer.randomly_edit(rng, 1, cx);
1494 });
1495 let buffer_snapshot = buffer.read(cx).text_snapshot();
1496 diff.update(cx, |diff, cx| {
1497 diff.recalculate_diff_sync(&buffer_snapshot, cx);
1498 });
1499 let path = PathKey::for_buffer(&buffer, cx);
1500 let ranges = diff.update(cx, |diff, cx| {
1501 diff.snapshot(cx)
1502 .hunks(&buffer_snapshot)
1503 .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
1504 .collect::<Vec<_>>()
1505 });
1506 self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1507 } else {
1508 log::info!("removing excerpts");
1509 let remove_count = rng.random_range(1..=paths.len());
1510 let paths_to_remove = paths
1511 .choose_multiple(rng, remove_count)
1512 .cloned()
1513 .collect::<Vec<_>>();
1514 for path in paths_to_remove {
1515 self.remove_excerpts_for_path(path.clone(), cx);
1516 }
1517 }
1518 }
1519 }
1520}
1521
1522impl Item for SplittableEditor {
1523 type Event = EditorEvent;
1524
1525 fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1526 self.rhs_editor.read(cx).tab_content_text(detail, cx)
1527 }
1528
1529 fn tab_tooltip_text(&self, cx: &App) -> Option<ui::SharedString> {
1530 self.rhs_editor.read(cx).tab_tooltip_text(cx)
1531 }
1532
1533 fn tab_icon(&self, window: &Window, cx: &App) -> Option<ui::Icon> {
1534 self.rhs_editor.read(cx).tab_icon(window, cx)
1535 }
1536
1537 fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> gpui::AnyElement {
1538 self.rhs_editor.read(cx).tab_content(params, window, cx)
1539 }
1540
1541 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
1542 Editor::to_item_events(event, f)
1543 }
1544
1545 fn for_each_project_item(
1546 &self,
1547 cx: &App,
1548 f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1549 ) {
1550 self.rhs_editor.read(cx).for_each_project_item(cx, f)
1551 }
1552
1553 fn buffer_kind(&self, cx: &App) -> ItemBufferKind {
1554 self.rhs_editor.read(cx).buffer_kind(cx)
1555 }
1556
1557 fn is_dirty(&self, cx: &App) -> bool {
1558 self.rhs_editor.read(cx).is_dirty(cx)
1559 }
1560
1561 fn has_conflict(&self, cx: &App) -> bool {
1562 self.rhs_editor.read(cx).has_conflict(cx)
1563 }
1564
1565 fn has_deleted_file(&self, cx: &App) -> bool {
1566 self.rhs_editor.read(cx).has_deleted_file(cx)
1567 }
1568
1569 fn capability(&self, cx: &App) -> language::Capability {
1570 self.rhs_editor.read(cx).capability(cx)
1571 }
1572
1573 fn can_save(&self, cx: &App) -> bool {
1574 self.rhs_editor.read(cx).can_save(cx)
1575 }
1576
1577 fn can_save_as(&self, cx: &App) -> bool {
1578 self.rhs_editor.read(cx).can_save_as(cx)
1579 }
1580
1581 fn save(
1582 &mut self,
1583 options: SaveOptions,
1584 project: Entity<Project>,
1585 window: &mut Window,
1586 cx: &mut Context<Self>,
1587 ) -> gpui::Task<anyhow::Result<()>> {
1588 self.rhs_editor
1589 .update(cx, |editor, cx| editor.save(options, project, window, cx))
1590 }
1591
1592 fn save_as(
1593 &mut self,
1594 project: Entity<Project>,
1595 path: project::ProjectPath,
1596 window: &mut Window,
1597 cx: &mut Context<Self>,
1598 ) -> gpui::Task<anyhow::Result<()>> {
1599 self.rhs_editor
1600 .update(cx, |editor, cx| editor.save_as(project, path, window, cx))
1601 }
1602
1603 fn reload(
1604 &mut self,
1605 project: Entity<Project>,
1606 window: &mut Window,
1607 cx: &mut Context<Self>,
1608 ) -> gpui::Task<anyhow::Result<()>> {
1609 self.rhs_editor
1610 .update(cx, |editor, cx| editor.reload(project, window, cx))
1611 }
1612
1613 fn navigate(
1614 &mut self,
1615 data: Arc<dyn std::any::Any + Send>,
1616 window: &mut Window,
1617 cx: &mut Context<Self>,
1618 ) -> bool {
1619 self.last_selected_editor()
1620 .update(cx, |editor, cx| editor.navigate(data, window, cx))
1621 }
1622
1623 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1624 self.last_selected_editor().update(cx, |editor, cx| {
1625 editor.deactivated(window, cx);
1626 });
1627 }
1628
1629 fn added_to_workspace(
1630 &mut self,
1631 workspace: &mut Workspace,
1632 window: &mut Window,
1633 cx: &mut Context<Self>,
1634 ) {
1635 self.workspace = workspace.weak_handle();
1636 self.rhs_editor.update(cx, |rhs_editor, cx| {
1637 rhs_editor.added_to_workspace(workspace, window, cx);
1638 });
1639 if let Some(lhs) = &self.lhs {
1640 lhs.editor.update(cx, |lhs_editor, cx| {
1641 lhs_editor.added_to_workspace(workspace, window, cx);
1642 });
1643 }
1644 }
1645
1646 fn as_searchable(
1647 &self,
1648 handle: &Entity<Self>,
1649 _: &App,
1650 ) -> Option<Box<dyn SearchableItemHandle>> {
1651 Some(Box::new(handle.clone()))
1652 }
1653
1654 fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1655 self.rhs_editor.read(cx).breadcrumb_location(cx)
1656 }
1657
1658 fn breadcrumbs(&self, cx: &App) -> Option<Vec<BreadcrumbText>> {
1659 self.rhs_editor.read(cx).breadcrumbs(cx)
1660 }
1661
1662 fn pixel_position_of_cursor(&self, cx: &App) -> Option<gpui::Point<gpui::Pixels>> {
1663 self.last_selected_editor()
1664 .read(cx)
1665 .pixel_position_of_cursor(cx)
1666 }
1667}
1668
1669impl SearchableItem for SplittableEditor {
1670 type Match = Range<Anchor>;
1671
1672 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1673 self.last_selected_editor().update(cx, |editor, cx| {
1674 editor.clear_matches(window, cx);
1675 });
1676 }
1677
1678 fn update_matches(
1679 &mut self,
1680 matches: &[Self::Match],
1681 active_match_index: Option<usize>,
1682 window: &mut Window,
1683 cx: &mut Context<Self>,
1684 ) {
1685 self.last_selected_editor().update(cx, |editor, cx| {
1686 editor.update_matches(matches, active_match_index, window, cx);
1687 });
1688 }
1689
1690 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1691 self.last_selected_editor()
1692 .update(cx, |editor, cx| editor.query_suggestion(window, cx))
1693 }
1694
1695 fn activate_match(
1696 &mut self,
1697 index: usize,
1698 matches: &[Self::Match],
1699 window: &mut Window,
1700 cx: &mut Context<Self>,
1701 ) {
1702 self.last_selected_editor().update(cx, |editor, cx| {
1703 editor.activate_match(index, matches, window, cx);
1704 });
1705 }
1706
1707 fn select_matches(
1708 &mut self,
1709 matches: &[Self::Match],
1710 window: &mut Window,
1711 cx: &mut Context<Self>,
1712 ) {
1713 self.last_selected_editor().update(cx, |editor, cx| {
1714 editor.select_matches(matches, window, cx);
1715 });
1716 }
1717
1718 fn replace(
1719 &mut self,
1720 identifier: &Self::Match,
1721 query: &project::search::SearchQuery,
1722 window: &mut Window,
1723 cx: &mut Context<Self>,
1724 ) {
1725 self.last_selected_editor().update(cx, |editor, cx| {
1726 editor.replace(identifier, query, window, cx);
1727 });
1728 }
1729
1730 fn find_matches(
1731 &mut self,
1732 query: Arc<project::search::SearchQuery>,
1733 window: &mut Window,
1734 cx: &mut Context<Self>,
1735 ) -> gpui::Task<Vec<Self::Match>> {
1736 self.last_selected_editor()
1737 .update(cx, |editor, cx| editor.find_matches(query, window, cx))
1738 }
1739
1740 fn active_match_index(
1741 &mut self,
1742 direction: workspace::searchable::Direction,
1743 matches: &[Self::Match],
1744 window: &mut Window,
1745 cx: &mut Context<Self>,
1746 ) -> Option<usize> {
1747 self.last_selected_editor().update(cx, |editor, cx| {
1748 editor.active_match_index(direction, matches, window, cx)
1749 })
1750 }
1751}
1752
1753impl EventEmitter<EditorEvent> for SplittableEditor {}
1754impl EventEmitter<SearchEvent> for SplittableEditor {}
1755impl Focusable for SplittableEditor {
1756 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1757 self.last_selected_editor().read(cx).focus_handle(cx)
1758 }
1759}
1760
1761// impl Item for SplittableEditor {
1762// type Event = EditorEvent;
1763
1764// fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1765// self.rhs_editor().tab_content_text(detail, cx)
1766// }
1767
1768// fn as_searchable(&self, _this: &Entity<Self>, cx: &App) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
1769// Some(Box::new(self.last_selected_editor().clone()))
1770// }
1771// }
1772
1773impl Render for SplittableEditor {
1774 fn render(
1775 &mut self,
1776 _window: &mut ui::Window,
1777 cx: &mut ui::Context<Self>,
1778 ) -> impl ui::IntoElement {
1779 let inner = if self.lhs.is_some() {
1780 let style = self.rhs_editor.read(cx).create_style(cx);
1781 SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
1782 } else {
1783 self.rhs_editor.clone().into_any_element()
1784 };
1785 div()
1786 .id("splittable-editor")
1787 .on_action(cx.listener(Self::toggle_split))
1788 .on_action(cx.listener(Self::activate_pane_left))
1789 .on_action(cx.listener(Self::activate_pane_right))
1790 .on_action(cx.listener(Self::intercept_toggle_breakpoint))
1791 .on_action(cx.listener(Self::intercept_enable_breakpoint))
1792 .on_action(cx.listener(Self::intercept_disable_breakpoint))
1793 .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
1794 .on_action(cx.listener(Self::intercept_inline_assist))
1795 .capture_action(cx.listener(Self::toggle_soft_wrap))
1796 .size_full()
1797 .child(inner)
1798 }
1799}
1800
1801impl LhsEditor {
1802 fn update_path_excerpts_from_rhs(
1803 path_key: PathKey,
1804 rhs_multibuffer: &MultiBuffer,
1805 lhs_multibuffer: &mut MultiBuffer,
1806 diff: Entity<BufferDiff>,
1807 lhs_cx: &mut Context<MultiBuffer>,
1808 ) -> Vec<(ExcerptId, ExcerptId)> {
1809 let rhs_excerpt_ids: Vec<ExcerptId> =
1810 rhs_multibuffer.excerpts_for_path(&path_key).collect();
1811
1812 let Some(excerpt_id) = rhs_multibuffer.excerpts_for_path(&path_key).next() else {
1813 lhs_multibuffer.remove_excerpts_for_path(path_key, lhs_cx);
1814 return Vec::new();
1815 };
1816
1817 let rhs_multibuffer_snapshot = rhs_multibuffer.snapshot(lhs_cx);
1818 let main_buffer = rhs_multibuffer_snapshot
1819 .buffer_for_excerpt(excerpt_id)
1820 .unwrap();
1821 let base_text_buffer = diff.read(lhs_cx).base_text_buffer();
1822 let diff_snapshot = diff.read(lhs_cx).snapshot(lhs_cx);
1823 let base_text_buffer_snapshot = base_text_buffer.read(lhs_cx).snapshot();
1824 let new = rhs_multibuffer
1825 .excerpts_for_buffer(main_buffer.remote_id(), lhs_cx)
1826 .into_iter()
1827 .map(|(_, excerpt_range)| {
1828 let point_range_to_base_text_point_range = |range: Range<Point>| {
1829 let start = diff_snapshot
1830 .buffer_point_to_base_text_range(
1831 Point::new(range.start.row, 0),
1832 main_buffer,
1833 )
1834 .start;
1835 let end = diff_snapshot
1836 .buffer_point_to_base_text_range(Point::new(range.end.row, 0), main_buffer)
1837 .end;
1838 let end_column = diff_snapshot.base_text().line_len(end.row);
1839 Point::new(start.row, 0)..Point::new(end.row, end_column)
1840 };
1841 let rhs = excerpt_range.primary.to_point(main_buffer);
1842 let context = excerpt_range.context.to_point(main_buffer);
1843 ExcerptRange {
1844 primary: point_range_to_base_text_point_range(rhs),
1845 context: point_range_to_base_text_point_range(context),
1846 }
1847 })
1848 .collect();
1849
1850 let (ids, _) = lhs_multibuffer.update_path_excerpts(
1851 path_key.clone(),
1852 base_text_buffer.clone(),
1853 &base_text_buffer_snapshot,
1854 new,
1855 lhs_cx,
1856 );
1857 if !ids.is_empty()
1858 && lhs_multibuffer
1859 .diff_for(base_text_buffer.read(lhs_cx).remote_id())
1860 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1861 {
1862 lhs_multibuffer.add_inverted_diff(diff, lhs_cx);
1863 }
1864
1865 let lhs_excerpt_ids: Vec<ExcerptId> =
1866 lhs_multibuffer.excerpts_for_path(&path_key).collect();
1867
1868 debug_assert_eq!(rhs_excerpt_ids.len(), lhs_excerpt_ids.len());
1869
1870 lhs_excerpt_ids.into_iter().zip(rhs_excerpt_ids).collect()
1871 }
1872
1873 fn sync_path_excerpts(
1874 path_key: PathKey,
1875 rhs_multibuffer: &MultiBuffer,
1876 lhs_multibuffer: &mut MultiBuffer,
1877 diff: Entity<BufferDiff>,
1878 rhs_display_map: &Entity<DisplayMap>,
1879 lhs_cx: &mut Context<MultiBuffer>,
1880 ) {
1881 Self::remove_mappings_for_path(
1882 &path_key,
1883 rhs_multibuffer,
1884 lhs_multibuffer,
1885 rhs_display_map,
1886 lhs_cx,
1887 );
1888
1889 let mappings = Self::update_path_excerpts_from_rhs(
1890 path_key,
1891 rhs_multibuffer,
1892 lhs_multibuffer,
1893 diff.clone(),
1894 lhs_cx,
1895 );
1896
1897 let lhs_buffer_id = diff.read(lhs_cx).base_text(lhs_cx).remote_id();
1898 let rhs_buffer_id = diff.read(lhs_cx).buffer_id;
1899
1900 if let Some(companion) = rhs_display_map.read(lhs_cx).companion().cloned() {
1901 companion.update(lhs_cx, |c, _| {
1902 for (lhs, rhs) in mappings {
1903 c.add_excerpt_mapping(lhs, rhs);
1904 }
1905 c.add_buffer_mapping(lhs_buffer_id, rhs_buffer_id);
1906 });
1907 }
1908 }
1909
1910 fn remove_mappings_for_path(
1911 path_key: &PathKey,
1912 rhs_multibuffer: &MultiBuffer,
1913 lhs_multibuffer: &MultiBuffer,
1914 rhs_display_map: &Entity<DisplayMap>,
1915 cx: &mut App,
1916 ) {
1917 let rhs_excerpt_ids: Vec<ExcerptId> = rhs_multibuffer.excerpts_for_path(path_key).collect();
1918 let lhs_excerpt_ids: Vec<ExcerptId> = lhs_multibuffer.excerpts_for_path(path_key).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}