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