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