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