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(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1905 self.focused_editor()
1906 .update(cx, |editor, cx| editor.query_suggestion(window, cx))
1907 }
1908
1909 fn activate_match(
1910 &mut self,
1911 index: usize,
1912 matches: &[Self::Match],
1913 token: SearchToken,
1914 window: &mut Window,
1915 cx: &mut Context<Self>,
1916 ) {
1917 let Some(target) = self.editor_for_token(token) else {
1918 return;
1919 };
1920 target.update(cx, |editor, cx| {
1921 editor.activate_match(index, matches, token, window, cx);
1922 });
1923 }
1924
1925 fn select_matches(
1926 &mut self,
1927 matches: &[Self::Match],
1928 token: SearchToken,
1929 window: &mut Window,
1930 cx: &mut Context<Self>,
1931 ) {
1932 let Some(target) = self.editor_for_token(token) else {
1933 return;
1934 };
1935 target.update(cx, |editor, cx| {
1936 editor.select_matches(matches, token, window, cx);
1937 });
1938 }
1939
1940 fn replace(
1941 &mut self,
1942 identifier: &Self::Match,
1943 query: &project::search::SearchQuery,
1944 token: SearchToken,
1945 window: &mut Window,
1946 cx: &mut Context<Self>,
1947 ) {
1948 let Some(target) = self.editor_for_token(token) else {
1949 return;
1950 };
1951 target.update(cx, |editor, cx| {
1952 editor.replace(identifier, query, token, window, cx);
1953 });
1954 }
1955
1956 fn find_matches(
1957 &mut self,
1958 query: Arc<project::search::SearchQuery>,
1959 window: &mut Window,
1960 cx: &mut Context<Self>,
1961 ) -> gpui::Task<Vec<Self::Match>> {
1962 self.focused_editor()
1963 .update(cx, |editor, cx| editor.find_matches(query, window, cx))
1964 }
1965
1966 fn find_matches_with_token(
1967 &mut self,
1968 query: Arc<project::search::SearchQuery>,
1969 window: &mut Window,
1970 cx: &mut Context<Self>,
1971 ) -> gpui::Task<(Vec<Self::Match>, SearchToken)> {
1972 let token = self.search_token();
1973 let editor = self.focused_editor().downgrade();
1974 cx.spawn_in(window, async move |_, cx| {
1975 let Some(matches) = editor
1976 .update_in(cx, |editor, window, cx| {
1977 editor.find_matches(query, window, cx)
1978 })
1979 .ok()
1980 else {
1981 return (Vec::new(), token);
1982 };
1983 (matches.await, token)
1984 })
1985 }
1986
1987 fn active_match_index(
1988 &mut self,
1989 direction: workspace::searchable::Direction,
1990 matches: &[Self::Match],
1991 token: SearchToken,
1992 window: &mut Window,
1993 cx: &mut Context<Self>,
1994 ) -> Option<usize> {
1995 self.editor_for_token(token)?.update(cx, |editor, cx| {
1996 editor.active_match_index(direction, matches, token, window, cx)
1997 })
1998 }
1999}
2000
2001impl EventEmitter<EditorEvent> for SplittableEditor {}
2002impl EventEmitter<SearchEvent> for SplittableEditor {}
2003impl Focusable for SplittableEditor {
2004 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
2005 self.focused_editor().read(cx).focus_handle(cx)
2006 }
2007}
2008
2009impl Render for SplittableEditor {
2010 fn render(
2011 &mut self,
2012 _window: &mut ui::Window,
2013 cx: &mut ui::Context<Self>,
2014 ) -> impl ui::IntoElement {
2015 let is_split = self.lhs.is_some();
2016 let inner = if is_split {
2017 let style = self.rhs_editor.read(cx).create_style(cx);
2018 SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
2019 } else {
2020 self.rhs_editor.clone().into_any_element()
2021 };
2022
2023 let this = cx.entity().downgrade();
2024 let last_width = self.last_width;
2025
2026 div()
2027 .id("splittable-editor")
2028 .on_action(cx.listener(Self::toggle_split))
2029 .on_action(cx.listener(Self::activate_pane_left))
2030 .on_action(cx.listener(Self::activate_pane_right))
2031 .on_action(cx.listener(Self::intercept_toggle_breakpoint))
2032 .on_action(cx.listener(Self::intercept_enable_breakpoint))
2033 .on_action(cx.listener(Self::intercept_disable_breakpoint))
2034 .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
2035 .on_action(cx.listener(Self::intercept_inline_assist))
2036 .capture_action(cx.listener(Self::toggle_soft_wrap))
2037 .size_full()
2038 .child(inner)
2039 .child(
2040 canvas(
2041 move |bounds, window, cx| {
2042 let width = bounds.size.width;
2043 if last_width == Some(width) {
2044 return;
2045 }
2046 window.defer(cx, move |window, cx| {
2047 this.update(cx, |this, cx| {
2048 this.width_changed(width, window, cx);
2049 })
2050 .ok();
2051 });
2052 },
2053 |_, _, _, _| {},
2054 )
2055 .absolute()
2056 .size_full(),
2057 )
2058 }
2059}
2060
2061#[cfg(test)]
2062mod tests {
2063 use std::{any::TypeId, sync::Arc};
2064
2065 use buffer_diff::BufferDiff;
2066 use collections::{HashMap, HashSet};
2067 use fs::FakeFs;
2068 use gpui::Element as _;
2069 use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
2070 use language::language_settings::SoftWrap;
2071 use language::{Buffer, Capability};
2072 use multi_buffer::{MultiBuffer, PathKey};
2073 use pretty_assertions::assert_eq;
2074 use project::Project;
2075 use rand::rngs::StdRng;
2076 use settings::{DiffViewStyle, SettingsStore};
2077 use ui::{VisualContext as _, div, px};
2078 use util::rel_path::rel_path;
2079 use workspace::{Item, MultiWorkspace};
2080
2081 use crate::display_map::{
2082 BlockPlacement, BlockProperties, BlockStyle, Crease, FoldPlaceholder,
2083 };
2084 use crate::inlays::Inlay;
2085 use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
2086 use crate::{Editor, SplittableEditor};
2087 use multi_buffer::MultiBufferOffset;
2088
2089 async fn init_test(
2090 cx: &mut gpui::TestAppContext,
2091 soft_wrap: SoftWrap,
2092 style: DiffViewStyle,
2093 ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
2094 cx.update(|cx| {
2095 let store = SettingsStore::test(cx);
2096 cx.set_global(store);
2097 theme_settings::init(theme::LoadThemes::JustBase, cx);
2098 crate::init(cx);
2099 });
2100 let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
2101 let (multi_workspace, cx) =
2102 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2103 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2104 let rhs_multibuffer = cx.new(|cx| {
2105 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2106 multibuffer.set_all_diff_hunks_expanded(cx);
2107 multibuffer
2108 });
2109 let editor = cx.new_window_entity(|window, cx| {
2110 let editor = SplittableEditor::new(
2111 style,
2112 rhs_multibuffer.clone(),
2113 project.clone(),
2114 workspace,
2115 window,
2116 cx,
2117 );
2118 editor.rhs_editor.update(cx, |editor, cx| {
2119 editor.set_soft_wrap_mode(soft_wrap, cx);
2120 });
2121 if let Some(lhs) = &editor.lhs {
2122 lhs.editor.update(cx, |editor, cx| {
2123 editor.set_soft_wrap_mode(soft_wrap, cx);
2124 });
2125 }
2126 editor
2127 });
2128 (editor, cx)
2129 }
2130
2131 fn buffer_with_diff(
2132 base_text: &str,
2133 current_text: &str,
2134 cx: &mut VisualTestContext,
2135 ) -> (Entity<Buffer>, Entity<BufferDiff>) {
2136 let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2137 let diff = cx.new(|cx| {
2138 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
2139 });
2140 (buffer, diff)
2141 }
2142
2143 #[track_caller]
2144 fn assert_split_content(
2145 editor: &Entity<SplittableEditor>,
2146 expected_rhs: String,
2147 expected_lhs: String,
2148 cx: &mut VisualTestContext,
2149 ) {
2150 assert_split_content_with_widths(
2151 editor,
2152 px(3000.0),
2153 px(3000.0),
2154 expected_rhs,
2155 expected_lhs,
2156 cx,
2157 );
2158 }
2159
2160 #[track_caller]
2161 fn assert_split_content_with_widths(
2162 editor: &Entity<SplittableEditor>,
2163 rhs_width: Pixels,
2164 lhs_width: Pixels,
2165 expected_rhs: String,
2166 expected_lhs: String,
2167 cx: &mut VisualTestContext,
2168 ) {
2169 let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2170 let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2171 (editor.rhs_editor.clone(), lhs.editor.clone())
2172 });
2173
2174 // Make sure both sides learn if the other has soft-wrapped
2175 let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2176 cx.run_until_parked();
2177 let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2178 cx.run_until_parked();
2179
2180 let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2181 let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2182
2183 if rhs_content != expected_rhs || lhs_content != expected_lhs {
2184 editor.update(cx, |editor, cx| editor.debug_print(cx));
2185 }
2186
2187 assert_eq!(rhs_content, expected_rhs, "rhs");
2188 assert_eq!(lhs_content, expected_lhs, "lhs");
2189 }
2190
2191 #[gpui::test(iterations = 25)]
2192 async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2193 use multi_buffer::ExpandExcerptDirection;
2194 use rand::prelude::*;
2195 use util::RandomCharIter;
2196
2197 let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2198 let operations = std::env::var("OPERATIONS")
2199 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2200 .unwrap_or(10);
2201 let rng = &mut rng;
2202 for _ in 0..operations {
2203 let buffers = editor.update(cx, |editor, cx| {
2204 editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2205 });
2206
2207 if buffers.is_empty() {
2208 log::info!("creating initial buffer");
2209 let len = rng.random_range(200..1000);
2210 let base_text: String = RandomCharIter::new(&mut *rng).take(len).collect();
2211 let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
2212 let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2213 let diff =
2214 cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_snapshot, cx));
2215 let edit_count = rng.random_range(3..8);
2216 buffer.update(cx, |buffer, cx| {
2217 buffer.randomly_edit(rng, edit_count, cx);
2218 });
2219 let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2220 diff.update(cx, |diff, cx| {
2221 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2222 });
2223 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2224 let ranges = diff_snapshot
2225 .hunks(&buffer_snapshot)
2226 .map(|hunk| hunk.range)
2227 .collect::<Vec<_>>();
2228 let context_lines = rng.random_range(0..2);
2229 editor.update(cx, |editor, cx| {
2230 let path = PathKey::for_buffer(&buffer, cx);
2231 editor.update_excerpts_for_path(path, buffer, ranges, context_lines, diff, cx);
2232 });
2233 editor.update(cx, |editor, cx| {
2234 editor.check_invariants(true, cx);
2235 });
2236 continue;
2237 }
2238
2239 let mut quiesced = false;
2240
2241 match rng.random_range(0..100) {
2242 0..=14 if buffers.len() < 6 => {
2243 log::info!("creating new buffer and setting excerpts");
2244 let len = rng.random_range(200..1000);
2245 let base_text: String = RandomCharIter::new(&mut *rng).take(len).collect();
2246 let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx));
2247 let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2248 let diff = cx
2249 .new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_snapshot, cx));
2250 let edit_count = rng.random_range(3..8);
2251 buffer.update(cx, |buffer, cx| {
2252 buffer.randomly_edit(rng, edit_count, cx);
2253 });
2254 let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2255 diff.update(cx, |diff, cx| {
2256 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2257 });
2258 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2259 let ranges = diff_snapshot
2260 .hunks(&buffer_snapshot)
2261 .map(|hunk| hunk.range)
2262 .collect::<Vec<_>>();
2263 let context_lines = rng.random_range(0..2);
2264 editor.update(cx, |editor, cx| {
2265 let path = PathKey::for_buffer(&buffer, cx);
2266 editor.update_excerpts_for_path(
2267 path,
2268 buffer,
2269 ranges,
2270 context_lines,
2271 diff,
2272 cx,
2273 );
2274 });
2275 }
2276 15..=29 => {
2277 log::info!("randomly editing multibuffer");
2278 let edit_count = rng.random_range(1..5);
2279 editor.update(cx, |editor, cx| {
2280 editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2281 multibuffer.randomly_edit(rng, edit_count, cx);
2282 });
2283 });
2284 }
2285 30..=44 => {
2286 log::info!("randomly editing individual buffer");
2287 let buffer = buffers.iter().choose(rng).unwrap();
2288 let edit_count = rng.random_range(1..3);
2289 buffer.update(cx, |buffer, cx| {
2290 buffer.randomly_edit(rng, edit_count, cx);
2291 });
2292 }
2293 45..=54 => {
2294 log::info!("recalculating diff and resetting excerpts for single buffer");
2295 let buffer = buffers.iter().choose(rng).unwrap();
2296 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2297 let diff = editor.update(cx, |editor, cx| {
2298 editor
2299 .rhs_multibuffer
2300 .read(cx)
2301 .diff_for(buffer.read(cx).remote_id())
2302 .unwrap()
2303 });
2304 diff.update(cx, |diff, cx| {
2305 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2306 });
2307 cx.run_until_parked();
2308 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2309 let ranges = diff_snapshot
2310 .hunks(&buffer_snapshot)
2311 .map(|hunk| hunk.range)
2312 .collect::<Vec<_>>();
2313 let context_lines = rng.random_range(0..2);
2314 let buffer = buffer.clone();
2315 editor.update(cx, |editor, cx| {
2316 let path = PathKey::for_buffer(&buffer, cx);
2317 editor.update_excerpts_for_path(
2318 path,
2319 buffer,
2320 ranges,
2321 context_lines,
2322 diff,
2323 cx,
2324 );
2325 });
2326 }
2327 55..=64 => {
2328 log::info!("randomly undoing/redoing in single buffer");
2329 let buffer = buffers.iter().choose(rng).unwrap();
2330 buffer.update(cx, |buffer, cx| {
2331 buffer.randomly_undo_redo(rng, cx);
2332 });
2333 }
2334 65..=74 => {
2335 log::info!("removing excerpts for a random path");
2336 let ids = editor.update(cx, |editor, cx| {
2337 let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
2338 snapshot.all_buffer_ids().collect::<Vec<_>>()
2339 });
2340 if let Some(id) = ids.choose(rng) {
2341 editor.update(cx, |editor, cx| {
2342 let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
2343 let path = snapshot.path_for_buffer(*id).unwrap();
2344 editor.remove_excerpts_for_path(path.clone(), cx);
2345 });
2346 }
2347 }
2348 75..=79 => {
2349 log::info!("unsplit and resplit");
2350 editor.update_in(cx, |editor, window, cx| {
2351 editor.unsplit(window, cx);
2352 });
2353 cx.run_until_parked();
2354 editor.update_in(cx, |editor, window, cx| {
2355 editor.split(window, cx);
2356 });
2357 }
2358 80..=89 => {
2359 let snapshot = editor.update(cx, |editor, cx| {
2360 editor.rhs_multibuffer.read(cx).snapshot(cx)
2361 });
2362 let excerpts = snapshot.excerpts().collect::<Vec<_>>();
2363 if !excerpts.is_empty() {
2364 let count = rng.random_range(1..=excerpts.len().min(3));
2365 let chosen: Vec<_> =
2366 excerpts.choose_multiple(rng, count).cloned().collect();
2367 let line_count = rng.random_range(1..5);
2368 log::info!("expanding {count} excerpts by {line_count} lines");
2369 editor.update(cx, |editor, cx| {
2370 editor.expand_excerpts(
2371 chosen.into_iter().map(|excerpt| {
2372 snapshot.anchor_in_excerpt(excerpt.context.start).unwrap()
2373 }),
2374 line_count,
2375 ExpandExcerptDirection::UpAndDown,
2376 cx,
2377 );
2378 });
2379 }
2380 }
2381 _ => {
2382 log::info!("quiescing");
2383 for buffer in buffers {
2384 let buffer_snapshot =
2385 buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2386 let diff = editor.update(cx, |editor, cx| {
2387 editor
2388 .rhs_multibuffer
2389 .read(cx)
2390 .diff_for(buffer.read(cx).remote_id())
2391 .unwrap()
2392 });
2393 diff.update(cx, |diff, cx| {
2394 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2395 });
2396 cx.run_until_parked();
2397 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2398 let ranges = diff_snapshot
2399 .hunks(&buffer_snapshot)
2400 .map(|hunk| hunk.range)
2401 .collect::<Vec<_>>();
2402 editor.update(cx, |editor, cx| {
2403 let path = PathKey::for_buffer(&buffer, cx);
2404 editor.update_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2405 });
2406 }
2407 quiesced = true;
2408 }
2409 }
2410
2411 editor.update(cx, |editor, cx| {
2412 editor.check_invariants(quiesced, cx);
2413 });
2414 }
2415 }
2416
2417 #[gpui::test]
2418 async fn test_expand_excerpt_with_hunk_before_excerpt_start(cx: &mut gpui::TestAppContext) {
2419 use rope::Point;
2420
2421 let (editor, cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
2422
2423 let base_text = "aaaaaaa rest_of_line\nsecond_line\nthird_line\nfourth_line";
2424 let current_text = "aaaaaaa rest_of_line\nsecond_line\nMODIFIED\nfourth_line";
2425 let (buffer, diff) = buffer_with_diff(base_text, current_text, cx);
2426
2427 let buffer_snapshot = buffer.read_with(cx, |b, _| b.text_snapshot());
2428 diff.update(cx, |diff, cx| {
2429 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2430 });
2431 cx.run_until_parked();
2432
2433 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2434 let ranges = diff_snapshot
2435 .hunks(&buffer_snapshot)
2436 .map(|hunk| hunk.range)
2437 .collect::<Vec<_>>();
2438
2439 editor.update(cx, |editor, cx| {
2440 let path = PathKey::for_buffer(&buffer, cx);
2441 editor.update_excerpts_for_path(path, buffer.clone(), ranges, 0, diff.clone(), cx);
2442 });
2443 cx.run_until_parked();
2444
2445 buffer.update(cx, |buffer, cx| {
2446 buffer.edit(
2447 [(Point::new(0, 7)..Point::new(1, 7), "\nnew_line\n")],
2448 None,
2449 cx,
2450 );
2451 });
2452
2453 let excerpts = editor.update(cx, |editor, cx| {
2454 let snapshot = editor.rhs_multibuffer.read(cx).snapshot(cx);
2455 snapshot
2456 .excerpts()
2457 .map(|excerpt| snapshot.anchor_in_excerpt(excerpt.context.start).unwrap())
2458 .collect::<Vec<_>>()
2459 });
2460 editor.update(cx, |editor, cx| {
2461 editor.expand_excerpts(
2462 excerpts.into_iter(),
2463 2,
2464 multi_buffer::ExpandExcerptDirection::UpAndDown,
2465 cx,
2466 );
2467 });
2468 }
2469
2470 #[gpui::test]
2471 async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2472 use rope::Point;
2473 use unindent::Unindent as _;
2474
2475 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2476
2477 let base_text = "
2478 aaa
2479 bbb
2480 ccc
2481 ddd
2482 eee
2483 fff
2484 "
2485 .unindent();
2486 let current_text = "
2487 aaa
2488 ddd
2489 eee
2490 fff
2491 "
2492 .unindent();
2493
2494 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2495
2496 editor.update(cx, |editor, cx| {
2497 let path = PathKey::for_buffer(&buffer, cx);
2498 editor.update_excerpts_for_path(
2499 path,
2500 buffer.clone(),
2501 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2502 0,
2503 diff.clone(),
2504 cx,
2505 );
2506 });
2507
2508 cx.run_until_parked();
2509
2510 assert_split_content(
2511 &editor,
2512 "
2513 § <no file>
2514 § -----
2515 aaa
2516 § spacer
2517 § spacer
2518 ddd
2519 eee
2520 fff"
2521 .unindent(),
2522 "
2523 § <no file>
2524 § -----
2525 aaa
2526 bbb
2527 ccc
2528 ddd
2529 eee
2530 fff"
2531 .unindent(),
2532 &mut cx,
2533 );
2534
2535 buffer.update(cx, |buffer, cx| {
2536 buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2537 });
2538
2539 cx.run_until_parked();
2540
2541 assert_split_content(
2542 &editor,
2543 "
2544 § <no file>
2545 § -----
2546 aaa
2547 § spacer
2548 § spacer
2549 ddd
2550 eee
2551 FFF"
2552 .unindent(),
2553 "
2554 § <no file>
2555 § -----
2556 aaa
2557 bbb
2558 ccc
2559 ddd
2560 eee
2561 fff"
2562 .unindent(),
2563 &mut cx,
2564 );
2565
2566 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2567 diff.update(cx, |diff, cx| {
2568 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2569 });
2570
2571 cx.run_until_parked();
2572
2573 assert_split_content(
2574 &editor,
2575 "
2576 § <no file>
2577 § -----
2578 aaa
2579 § spacer
2580 § spacer
2581 ddd
2582 eee
2583 FFF"
2584 .unindent(),
2585 "
2586 § <no file>
2587 § -----
2588 aaa
2589 bbb
2590 ccc
2591 ddd
2592 eee
2593 fff"
2594 .unindent(),
2595 &mut cx,
2596 );
2597 }
2598
2599 #[gpui::test]
2600 async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2601 use rope::Point;
2602 use unindent::Unindent as _;
2603
2604 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2605
2606 let base_text1 = "
2607 aaa
2608 bbb
2609 ccc
2610 ddd
2611 eee"
2612 .unindent();
2613
2614 let base_text2 = "
2615 fff
2616 ggg
2617 hhh
2618 iii
2619 jjj"
2620 .unindent();
2621
2622 let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2623 let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2624
2625 editor.update(cx, |editor, cx| {
2626 let path1 = PathKey::for_buffer(&buffer1, cx);
2627 editor.update_excerpts_for_path(
2628 path1,
2629 buffer1.clone(),
2630 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2631 0,
2632 diff1.clone(),
2633 cx,
2634 );
2635 let path2 = PathKey::for_buffer(&buffer2, cx);
2636 editor.update_excerpts_for_path(
2637 path2,
2638 buffer2.clone(),
2639 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2640 1,
2641 diff2.clone(),
2642 cx,
2643 );
2644 });
2645
2646 cx.run_until_parked();
2647
2648 buffer1.update(cx, |buffer, cx| {
2649 buffer.edit(
2650 [
2651 (Point::new(0, 0)..Point::new(1, 0), ""),
2652 (Point::new(3, 0)..Point::new(4, 0), ""),
2653 ],
2654 None,
2655 cx,
2656 );
2657 });
2658 buffer2.update(cx, |buffer, cx| {
2659 buffer.edit(
2660 [
2661 (Point::new(0, 0)..Point::new(1, 0), ""),
2662 (Point::new(3, 0)..Point::new(4, 0), ""),
2663 ],
2664 None,
2665 cx,
2666 );
2667 });
2668
2669 cx.run_until_parked();
2670
2671 assert_split_content(
2672 &editor,
2673 "
2674 § <no file>
2675 § -----
2676 § spacer
2677 bbb
2678 ccc
2679 § spacer
2680 eee
2681 § <no file>
2682 § -----
2683 § spacer
2684 ggg
2685 hhh
2686 § spacer
2687 jjj"
2688 .unindent(),
2689 "
2690 § <no file>
2691 § -----
2692 aaa
2693 bbb
2694 ccc
2695 ddd
2696 eee
2697 § <no file>
2698 § -----
2699 fff
2700 ggg
2701 hhh
2702 iii
2703 jjj"
2704 .unindent(),
2705 &mut cx,
2706 );
2707
2708 let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2709 diff1.update(cx, |diff, cx| {
2710 diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2711 });
2712 let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2713 diff2.update(cx, |diff, cx| {
2714 diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2715 });
2716
2717 cx.run_until_parked();
2718
2719 assert_split_content(
2720 &editor,
2721 "
2722 § <no file>
2723 § -----
2724 § spacer
2725 bbb
2726 ccc
2727 § spacer
2728 eee
2729 § <no file>
2730 § -----
2731 § spacer
2732 ggg
2733 hhh
2734 § spacer
2735 jjj"
2736 .unindent(),
2737 "
2738 § <no file>
2739 § -----
2740 aaa
2741 bbb
2742 ccc
2743 ddd
2744 eee
2745 § <no file>
2746 § -----
2747 fff
2748 ggg
2749 hhh
2750 iii
2751 jjj"
2752 .unindent(),
2753 &mut cx,
2754 );
2755 }
2756
2757 #[gpui::test]
2758 async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2759 use rope::Point;
2760 use unindent::Unindent as _;
2761
2762 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2763
2764 let base_text = "
2765 aaa
2766 bbb
2767 ccc
2768 ddd
2769 "
2770 .unindent();
2771
2772 let current_text = "
2773 aaa
2774 NEW1
2775 NEW2
2776 ccc
2777 ddd
2778 "
2779 .unindent();
2780
2781 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2782
2783 editor.update(cx, |editor, cx| {
2784 let path = PathKey::for_buffer(&buffer, cx);
2785 editor.update_excerpts_for_path(
2786 path,
2787 buffer.clone(),
2788 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2789 0,
2790 diff.clone(),
2791 cx,
2792 );
2793 });
2794
2795 cx.run_until_parked();
2796
2797 assert_split_content(
2798 &editor,
2799 "
2800 § <no file>
2801 § -----
2802 aaa
2803 NEW1
2804 NEW2
2805 ccc
2806 ddd"
2807 .unindent(),
2808 "
2809 § <no file>
2810 § -----
2811 aaa
2812 bbb
2813 § spacer
2814 ccc
2815 ddd"
2816 .unindent(),
2817 &mut cx,
2818 );
2819
2820 buffer.update(cx, |buffer, cx| {
2821 buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2822 });
2823
2824 cx.run_until_parked();
2825
2826 assert_split_content(
2827 &editor,
2828 "
2829 § <no file>
2830 § -----
2831 aaa
2832 NEW1
2833 ccc
2834 ddd"
2835 .unindent(),
2836 "
2837 § <no file>
2838 § -----
2839 aaa
2840 bbb
2841 ccc
2842 ddd"
2843 .unindent(),
2844 &mut cx,
2845 );
2846
2847 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2848 diff.update(cx, |diff, cx| {
2849 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2850 });
2851
2852 cx.run_until_parked();
2853
2854 assert_split_content(
2855 &editor,
2856 "
2857 § <no file>
2858 § -----
2859 aaa
2860 NEW1
2861 ccc
2862 ddd"
2863 .unindent(),
2864 "
2865 § <no file>
2866 § -----
2867 aaa
2868 bbb
2869 ccc
2870 ddd"
2871 .unindent(),
2872 &mut cx,
2873 );
2874 }
2875
2876 #[gpui::test]
2877 async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2878 use rope::Point;
2879 use unindent::Unindent as _;
2880
2881 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2882
2883 let base_text = "
2884 aaa
2885 bbb
2886
2887
2888
2889
2890
2891 ccc
2892 ddd
2893 "
2894 .unindent();
2895 let current_text = "
2896 aaa
2897 bbb
2898
2899
2900
2901
2902
2903 CCC
2904 ddd
2905 "
2906 .unindent();
2907
2908 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2909
2910 editor.update(cx, |editor, cx| {
2911 let path = PathKey::for_buffer(&buffer, cx);
2912 editor.update_excerpts_for_path(
2913 path,
2914 buffer.clone(),
2915 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2916 0,
2917 diff.clone(),
2918 cx,
2919 );
2920 });
2921
2922 cx.run_until_parked();
2923
2924 buffer.update(cx, |buffer, cx| {
2925 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2926 });
2927
2928 cx.run_until_parked();
2929
2930 assert_split_content(
2931 &editor,
2932 "
2933 § <no file>
2934 § -----
2935 aaa
2936 bbb
2937
2938
2939
2940
2941
2942
2943 CCC
2944 ddd"
2945 .unindent(),
2946 "
2947 § <no file>
2948 § -----
2949 aaa
2950 bbb
2951 § spacer
2952
2953
2954
2955
2956
2957 ccc
2958 ddd"
2959 .unindent(),
2960 &mut cx,
2961 );
2962
2963 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2964 diff.update(cx, |diff, cx| {
2965 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2966 });
2967
2968 cx.run_until_parked();
2969
2970 assert_split_content(
2971 &editor,
2972 "
2973 § <no file>
2974 § -----
2975 aaa
2976 bbb
2977
2978
2979
2980
2981
2982
2983 CCC
2984 ddd"
2985 .unindent(),
2986 "
2987 § <no file>
2988 § -----
2989 aaa
2990 bbb
2991
2992
2993
2994
2995
2996 ccc
2997 § spacer
2998 ddd"
2999 .unindent(),
3000 &mut cx,
3001 );
3002 }
3003
3004 #[gpui::test]
3005 async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
3006 use git::Restore;
3007 use rope::Point;
3008 use unindent::Unindent as _;
3009
3010 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3011
3012 let base_text = "
3013 aaa
3014 bbb
3015 ccc
3016 ddd
3017 eee
3018 "
3019 .unindent();
3020 let current_text = "
3021 aaa
3022 ddd
3023 eee
3024 "
3025 .unindent();
3026
3027 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3028
3029 editor.update(cx, |editor, cx| {
3030 let path = PathKey::for_buffer(&buffer, cx);
3031 editor.update_excerpts_for_path(
3032 path,
3033 buffer.clone(),
3034 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3035 0,
3036 diff.clone(),
3037 cx,
3038 );
3039 });
3040
3041 cx.run_until_parked();
3042
3043 assert_split_content(
3044 &editor,
3045 "
3046 § <no file>
3047 § -----
3048 aaa
3049 § spacer
3050 § spacer
3051 ddd
3052 eee"
3053 .unindent(),
3054 "
3055 § <no file>
3056 § -----
3057 aaa
3058 bbb
3059 ccc
3060 ddd
3061 eee"
3062 .unindent(),
3063 &mut cx,
3064 );
3065
3066 let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
3067 cx.update_window_entity(&rhs_editor, |editor, window, cx| {
3068 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
3069 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
3070 });
3071 editor.git_restore(&Restore, window, cx);
3072 });
3073
3074 cx.run_until_parked();
3075
3076 assert_split_content(
3077 &editor,
3078 "
3079 § <no file>
3080 § -----
3081 aaa
3082 bbb
3083 ccc
3084 ddd
3085 eee"
3086 .unindent(),
3087 "
3088 § <no file>
3089 § -----
3090 aaa
3091 bbb
3092 ccc
3093 ddd
3094 eee"
3095 .unindent(),
3096 &mut cx,
3097 );
3098
3099 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3100 diff.update(cx, |diff, cx| {
3101 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3102 });
3103
3104 cx.run_until_parked();
3105
3106 assert_split_content(
3107 &editor,
3108 "
3109 § <no file>
3110 § -----
3111 aaa
3112 bbb
3113 ccc
3114 ddd
3115 eee"
3116 .unindent(),
3117 "
3118 § <no file>
3119 § -----
3120 aaa
3121 bbb
3122 ccc
3123 ddd
3124 eee"
3125 .unindent(),
3126 &mut cx,
3127 );
3128 }
3129
3130 #[gpui::test]
3131 async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
3132 use rope::Point;
3133 use unindent::Unindent as _;
3134
3135 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3136
3137 let base_text = "
3138 aaa
3139 old1
3140 old2
3141 old3
3142 old4
3143 zzz
3144 "
3145 .unindent();
3146
3147 let current_text = "
3148 aaa
3149 new1
3150 new2
3151 new3
3152 new4
3153 zzz
3154 "
3155 .unindent();
3156
3157 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3158
3159 editor.update(cx, |editor, cx| {
3160 let path = PathKey::for_buffer(&buffer, cx);
3161 editor.update_excerpts_for_path(
3162 path,
3163 buffer.clone(),
3164 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3165 0,
3166 diff.clone(),
3167 cx,
3168 );
3169 });
3170
3171 cx.run_until_parked();
3172
3173 buffer.update(cx, |buffer, cx| {
3174 buffer.edit(
3175 [
3176 (Point::new(2, 0)..Point::new(3, 0), ""),
3177 (Point::new(4, 0)..Point::new(5, 0), ""),
3178 ],
3179 None,
3180 cx,
3181 );
3182 });
3183 cx.run_until_parked();
3184
3185 assert_split_content(
3186 &editor,
3187 "
3188 § <no file>
3189 § -----
3190 aaa
3191 new1
3192 new3
3193 § spacer
3194 § spacer
3195 zzz"
3196 .unindent(),
3197 "
3198 § <no file>
3199 § -----
3200 aaa
3201 old1
3202 old2
3203 old3
3204 old4
3205 zzz"
3206 .unindent(),
3207 &mut cx,
3208 );
3209
3210 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3211 diff.update(cx, |diff, cx| {
3212 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3213 });
3214
3215 cx.run_until_parked();
3216
3217 assert_split_content(
3218 &editor,
3219 "
3220 § <no file>
3221 § -----
3222 aaa
3223 new1
3224 new3
3225 § spacer
3226 § spacer
3227 zzz"
3228 .unindent(),
3229 "
3230 § <no file>
3231 § -----
3232 aaa
3233 old1
3234 old2
3235 old3
3236 old4
3237 zzz"
3238 .unindent(),
3239 &mut cx,
3240 );
3241 }
3242
3243 #[gpui::test]
3244 async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
3245 use rope::Point;
3246 use unindent::Unindent as _;
3247
3248 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3249
3250 let text = "aaaa bbbb cccc dddd eeee ffff";
3251
3252 let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
3253 let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
3254
3255 editor.update(cx, |editor, cx| {
3256 let end = Point::new(0, text.len() as u32);
3257 let path1 = PathKey::for_buffer(&buffer1, cx);
3258 editor.update_excerpts_for_path(
3259 path1,
3260 buffer1.clone(),
3261 vec![Point::new(0, 0)..end],
3262 0,
3263 diff1.clone(),
3264 cx,
3265 );
3266 let path2 = PathKey::for_buffer(&buffer2, cx);
3267 editor.update_excerpts_for_path(
3268 path2,
3269 buffer2.clone(),
3270 vec![Point::new(0, 0)..end],
3271 0,
3272 diff2.clone(),
3273 cx,
3274 );
3275 });
3276
3277 cx.run_until_parked();
3278
3279 assert_split_content_with_widths(
3280 &editor,
3281 px(200.0),
3282 px(400.0),
3283 "
3284 § <no file>
3285 § -----
3286 aaaa bbbb\x20
3287 cccc dddd\x20
3288 eeee ffff
3289 § <no file>
3290 § -----
3291 aaaa bbbb\x20
3292 cccc dddd\x20
3293 eeee ffff"
3294 .unindent(),
3295 "
3296 § <no file>
3297 § -----
3298 aaaa bbbb cccc dddd eeee ffff
3299 § spacer
3300 § spacer
3301 § <no file>
3302 § -----
3303 aaaa bbbb cccc dddd eeee ffff
3304 § spacer
3305 § spacer"
3306 .unindent(),
3307 &mut cx,
3308 );
3309 }
3310
3311 #[gpui::test]
3312 async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
3313 use rope::Point;
3314 use unindent::Unindent as _;
3315
3316 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3317
3318 let base_text = "
3319 aaaa bbbb cccc dddd eeee ffff
3320 old line one
3321 old line two
3322 "
3323 .unindent();
3324
3325 let current_text = "
3326 aaaa bbbb cccc dddd eeee ffff
3327 new line
3328 "
3329 .unindent();
3330
3331 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3332
3333 editor.update(cx, |editor, cx| {
3334 let path = PathKey::for_buffer(&buffer, cx);
3335 editor.update_excerpts_for_path(
3336 path,
3337 buffer.clone(),
3338 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3339 0,
3340 diff.clone(),
3341 cx,
3342 );
3343 });
3344
3345 cx.run_until_parked();
3346
3347 assert_split_content_with_widths(
3348 &editor,
3349 px(200.0),
3350 px(400.0),
3351 "
3352 § <no file>
3353 § -----
3354 aaaa bbbb\x20
3355 cccc dddd\x20
3356 eeee ffff
3357 new line
3358 § spacer"
3359 .unindent(),
3360 "
3361 § <no file>
3362 § -----
3363 aaaa bbbb cccc dddd eeee ffff
3364 § spacer
3365 § spacer
3366 old line one
3367 old line two"
3368 .unindent(),
3369 &mut cx,
3370 );
3371 }
3372
3373 #[gpui::test]
3374 async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3375 use rope::Point;
3376 use unindent::Unindent as _;
3377
3378 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3379
3380 let base_text = "
3381 aaaa bbbb cccc dddd eeee ffff
3382 deleted line one
3383 deleted line two
3384 after
3385 "
3386 .unindent();
3387
3388 let current_text = "
3389 aaaa bbbb cccc dddd eeee ffff
3390 after
3391 "
3392 .unindent();
3393
3394 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3395
3396 editor.update(cx, |editor, cx| {
3397 let path = PathKey::for_buffer(&buffer, cx);
3398 editor.update_excerpts_for_path(
3399 path,
3400 buffer.clone(),
3401 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3402 0,
3403 diff.clone(),
3404 cx,
3405 );
3406 });
3407
3408 cx.run_until_parked();
3409
3410 assert_split_content_with_widths(
3411 &editor,
3412 px(400.0),
3413 px(200.0),
3414 "
3415 § <no file>
3416 § -----
3417 aaaa bbbb cccc dddd eeee ffff
3418 § spacer
3419 § spacer
3420 § spacer
3421 § spacer
3422 § spacer
3423 § spacer
3424 after"
3425 .unindent(),
3426 "
3427 § <no file>
3428 § -----
3429 aaaa bbbb\x20
3430 cccc dddd\x20
3431 eeee ffff
3432 deleted line\x20
3433 one
3434 deleted line\x20
3435 two
3436 after"
3437 .unindent(),
3438 &mut cx,
3439 );
3440 }
3441
3442 #[gpui::test]
3443 async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3444 use rope::Point;
3445 use unindent::Unindent as _;
3446
3447 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3448
3449 let text = "
3450 aaaa bbbb cccc dddd eeee ffff
3451 short
3452 "
3453 .unindent();
3454
3455 let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3456
3457 editor.update(cx, |editor, cx| {
3458 let path = PathKey::for_buffer(&buffer, cx);
3459 editor.update_excerpts_for_path(
3460 path,
3461 buffer.clone(),
3462 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3463 0,
3464 diff.clone(),
3465 cx,
3466 );
3467 });
3468
3469 cx.run_until_parked();
3470
3471 assert_split_content_with_widths(
3472 &editor,
3473 px(400.0),
3474 px(200.0),
3475 "
3476 § <no file>
3477 § -----
3478 aaaa bbbb cccc dddd eeee ffff
3479 § spacer
3480 § spacer
3481 short"
3482 .unindent(),
3483 "
3484 § <no file>
3485 § -----
3486 aaaa bbbb\x20
3487 cccc dddd\x20
3488 eeee ffff
3489 short"
3490 .unindent(),
3491 &mut cx,
3492 );
3493
3494 buffer.update(cx, |buffer, cx| {
3495 buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3496 });
3497
3498 cx.run_until_parked();
3499
3500 assert_split_content_with_widths(
3501 &editor,
3502 px(400.0),
3503 px(200.0),
3504 "
3505 § <no file>
3506 § -----
3507 aaaa bbbb cccc dddd eeee ffff
3508 § spacer
3509 § spacer
3510 modified"
3511 .unindent(),
3512 "
3513 § <no file>
3514 § -----
3515 aaaa bbbb\x20
3516 cccc dddd\x20
3517 eeee ffff
3518 short"
3519 .unindent(),
3520 &mut cx,
3521 );
3522
3523 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3524 diff.update(cx, |diff, cx| {
3525 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3526 });
3527
3528 cx.run_until_parked();
3529
3530 assert_split_content_with_widths(
3531 &editor,
3532 px(400.0),
3533 px(200.0),
3534 "
3535 § <no file>
3536 § -----
3537 aaaa bbbb cccc dddd eeee ffff
3538 § spacer
3539 § spacer
3540 modified"
3541 .unindent(),
3542 "
3543 § <no file>
3544 § -----
3545 aaaa bbbb\x20
3546 cccc dddd\x20
3547 eeee ffff
3548 short"
3549 .unindent(),
3550 &mut cx,
3551 );
3552 }
3553
3554 #[gpui::test]
3555 async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3556 use rope::Point;
3557 use unindent::Unindent as _;
3558
3559 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3560
3561 let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3562
3563 let current_text = "
3564 aaa
3565 bbb
3566 ccc
3567 "
3568 .unindent();
3569
3570 let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3571 let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3572
3573 editor.update(cx, |editor, cx| {
3574 let path1 = PathKey::for_buffer(&buffer1, cx);
3575 editor.update_excerpts_for_path(
3576 path1,
3577 buffer1.clone(),
3578 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3579 0,
3580 diff1.clone(),
3581 cx,
3582 );
3583
3584 let path2 = PathKey::for_buffer(&buffer2, cx);
3585 editor.update_excerpts_for_path(
3586 path2,
3587 buffer2.clone(),
3588 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3589 1,
3590 diff2.clone(),
3591 cx,
3592 );
3593 });
3594
3595 cx.run_until_parked();
3596
3597 assert_split_content(
3598 &editor,
3599 "
3600 § <no file>
3601 § -----
3602 xxx
3603 yyy
3604 § <no file>
3605 § -----
3606 aaa
3607 bbb
3608 ccc"
3609 .unindent(),
3610 "
3611 § <no file>
3612 § -----
3613 xxx
3614 yyy
3615 § <no file>
3616 § -----
3617 § spacer
3618 § spacer
3619 § spacer"
3620 .unindent(),
3621 &mut cx,
3622 );
3623
3624 buffer1.update(cx, |buffer, cx| {
3625 buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3626 });
3627
3628 cx.run_until_parked();
3629
3630 assert_split_content(
3631 &editor,
3632 "
3633 § <no file>
3634 § -----
3635 xxxz
3636 yyy
3637 § <no file>
3638 § -----
3639 aaa
3640 bbb
3641 ccc"
3642 .unindent(),
3643 "
3644 § <no file>
3645 § -----
3646 xxx
3647 yyy
3648 § <no file>
3649 § -----
3650 § spacer
3651 § spacer
3652 § spacer"
3653 .unindent(),
3654 &mut cx,
3655 );
3656 }
3657
3658 #[gpui::test]
3659 async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3660 use rope::Point;
3661 use unindent::Unindent as _;
3662
3663 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3664
3665 let base_text = "
3666 aaa
3667 bbb
3668 ccc
3669 "
3670 .unindent();
3671
3672 let current_text = "
3673 NEW1
3674 NEW2
3675 ccc
3676 "
3677 .unindent();
3678
3679 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3680
3681 editor.update(cx, |editor, cx| {
3682 let path = PathKey::for_buffer(&buffer, cx);
3683 editor.update_excerpts_for_path(
3684 path,
3685 buffer.clone(),
3686 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3687 0,
3688 diff.clone(),
3689 cx,
3690 );
3691 });
3692
3693 cx.run_until_parked();
3694
3695 assert_split_content(
3696 &editor,
3697 "
3698 § <no file>
3699 § -----
3700 NEW1
3701 NEW2
3702 ccc"
3703 .unindent(),
3704 "
3705 § <no file>
3706 § -----
3707 aaa
3708 bbb
3709 ccc"
3710 .unindent(),
3711 &mut cx,
3712 );
3713
3714 buffer.update(cx, |buffer, cx| {
3715 buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3716 });
3717
3718 cx.run_until_parked();
3719
3720 assert_split_content(
3721 &editor,
3722 "
3723 § <no file>
3724 § -----
3725 NEW1
3726 NEW
3727 ccc"
3728 .unindent(),
3729 "
3730 § <no file>
3731 § -----
3732 aaa
3733 bbb
3734 ccc"
3735 .unindent(),
3736 &mut cx,
3737 );
3738 }
3739
3740 #[gpui::test]
3741 async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3742 use rope::Point;
3743 use unindent::Unindent as _;
3744
3745 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3746
3747 let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3748
3749 let current_text = "
3750 aaaa bbbb cccc dddd eeee ffff
3751 added line
3752 "
3753 .unindent();
3754
3755 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3756
3757 editor.update(cx, |editor, cx| {
3758 let path = PathKey::for_buffer(&buffer, cx);
3759 editor.update_excerpts_for_path(
3760 path,
3761 buffer.clone(),
3762 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3763 0,
3764 diff.clone(),
3765 cx,
3766 );
3767 });
3768
3769 cx.run_until_parked();
3770
3771 assert_split_content_with_widths(
3772 &editor,
3773 px(400.0),
3774 px(200.0),
3775 "
3776 § <no file>
3777 § -----
3778 aaaa bbbb cccc dddd eeee ffff
3779 § spacer
3780 § spacer
3781 added line"
3782 .unindent(),
3783 "
3784 § <no file>
3785 § -----
3786 aaaa bbbb\x20
3787 cccc dddd\x20
3788 eeee ffff
3789 § spacer"
3790 .unindent(),
3791 &mut cx,
3792 );
3793
3794 assert_split_content_with_widths(
3795 &editor,
3796 px(200.0),
3797 px(400.0),
3798 "
3799 § <no file>
3800 § -----
3801 aaaa bbbb\x20
3802 cccc dddd\x20
3803 eeee ffff
3804 added line"
3805 .unindent(),
3806 "
3807 § <no file>
3808 § -----
3809 aaaa bbbb cccc dddd eeee ffff
3810 § spacer
3811 § spacer
3812 § spacer"
3813 .unindent(),
3814 &mut cx,
3815 );
3816 }
3817
3818 #[gpui::test]
3819 #[ignore]
3820 async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3821 use rope::Point;
3822 use unindent::Unindent as _;
3823
3824 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3825
3826 let base_text = "
3827 aaa
3828 bbb
3829 ccc
3830 ddd
3831 eee
3832 "
3833 .unindent();
3834
3835 let current_text = "
3836 aaa
3837 NEW
3838 eee
3839 "
3840 .unindent();
3841
3842 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3843
3844 editor.update(cx, |editor, cx| {
3845 let path = PathKey::for_buffer(&buffer, cx);
3846 editor.update_excerpts_for_path(
3847 path,
3848 buffer.clone(),
3849 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3850 0,
3851 diff.clone(),
3852 cx,
3853 );
3854 });
3855
3856 cx.run_until_parked();
3857
3858 assert_split_content(
3859 &editor,
3860 "
3861 § <no file>
3862 § -----
3863 aaa
3864 NEW
3865 § spacer
3866 § spacer
3867 eee"
3868 .unindent(),
3869 "
3870 § <no file>
3871 § -----
3872 aaa
3873 bbb
3874 ccc
3875 ddd
3876 eee"
3877 .unindent(),
3878 &mut cx,
3879 );
3880
3881 buffer.update(cx, |buffer, cx| {
3882 buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3883 });
3884
3885 cx.run_until_parked();
3886
3887 assert_split_content(
3888 &editor,
3889 "
3890 § <no file>
3891 § -----
3892 aaa
3893 § spacer
3894 § spacer
3895 § spacer
3896 NEWeee"
3897 .unindent(),
3898 "
3899 § <no file>
3900 § -----
3901 aaa
3902 bbb
3903 ccc
3904 ddd
3905 eee"
3906 .unindent(),
3907 &mut cx,
3908 );
3909
3910 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3911 diff.update(cx, |diff, cx| {
3912 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3913 });
3914
3915 cx.run_until_parked();
3916
3917 assert_split_content(
3918 &editor,
3919 "
3920 § <no file>
3921 § -----
3922 aaa
3923 NEWeee
3924 § spacer
3925 § spacer
3926 § spacer"
3927 .unindent(),
3928 "
3929 § <no file>
3930 § -----
3931 aaa
3932 bbb
3933 ccc
3934 ddd
3935 eee"
3936 .unindent(),
3937 &mut cx,
3938 );
3939 }
3940
3941 #[gpui::test]
3942 async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3943 use rope::Point;
3944 use unindent::Unindent as _;
3945
3946 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3947
3948 let base_text = "";
3949 let current_text = "
3950 aaaa bbbb cccc dddd eeee ffff
3951 bbb
3952 ccc
3953 "
3954 .unindent();
3955
3956 let (buffer, diff) = buffer_with_diff(base_text, ¤t_text, &mut cx);
3957
3958 editor.update(cx, |editor, cx| {
3959 let path = PathKey::for_buffer(&buffer, cx);
3960 editor.update_excerpts_for_path(
3961 path,
3962 buffer.clone(),
3963 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3964 0,
3965 diff.clone(),
3966 cx,
3967 );
3968 });
3969
3970 cx.run_until_parked();
3971
3972 assert_split_content(
3973 &editor,
3974 "
3975 § <no file>
3976 § -----
3977 aaaa bbbb cccc dddd eeee ffff
3978 bbb
3979 ccc"
3980 .unindent(),
3981 "
3982 § <no file>
3983 § -----
3984 § spacer
3985 § spacer
3986 § spacer"
3987 .unindent(),
3988 &mut cx,
3989 );
3990
3991 assert_split_content_with_widths(
3992 &editor,
3993 px(200.0),
3994 px(200.0),
3995 "
3996 § <no file>
3997 § -----
3998 aaaa bbbb\x20
3999 cccc dddd\x20
4000 eeee ffff
4001 bbb
4002 ccc"
4003 .unindent(),
4004 "
4005 § <no file>
4006 § -----
4007 § spacer
4008 § spacer
4009 § spacer
4010 § spacer
4011 § spacer"
4012 .unindent(),
4013 &mut cx,
4014 );
4015 }
4016
4017 #[gpui::test]
4018 async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
4019 use rope::Point;
4020 use unindent::Unindent as _;
4021
4022 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
4023
4024 let base_text = "
4025 aaa
4026 bbb
4027 ccc
4028 "
4029 .unindent();
4030
4031 let current_text = "
4032 aaa
4033 bbb
4034 xxx
4035 yyy
4036 ccc
4037 "
4038 .unindent();
4039
4040 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4041
4042 editor.update(cx, |editor, cx| {
4043 let path = PathKey::for_buffer(&buffer, cx);
4044 editor.update_excerpts_for_path(
4045 path,
4046 buffer.clone(),
4047 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4048 0,
4049 diff.clone(),
4050 cx,
4051 );
4052 });
4053
4054 cx.run_until_parked();
4055
4056 assert_split_content(
4057 &editor,
4058 "
4059 § <no file>
4060 § -----
4061 aaa
4062 bbb
4063 xxx
4064 yyy
4065 ccc"
4066 .unindent(),
4067 "
4068 § <no file>
4069 § -----
4070 aaa
4071 bbb
4072 § spacer
4073 § spacer
4074 ccc"
4075 .unindent(),
4076 &mut cx,
4077 );
4078
4079 buffer.update(cx, |buffer, cx| {
4080 buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
4081 });
4082
4083 cx.run_until_parked();
4084
4085 assert_split_content(
4086 &editor,
4087 "
4088 § <no file>
4089 § -----
4090 aaa
4091 bbb
4092 xxx
4093 yyy
4094 zzz
4095 ccc"
4096 .unindent(),
4097 "
4098 § <no file>
4099 § -----
4100 aaa
4101 bbb
4102 § spacer
4103 § spacer
4104 § spacer
4105 ccc"
4106 .unindent(),
4107 &mut cx,
4108 );
4109 }
4110
4111 #[gpui::test]
4112 async fn test_scrolling(cx: &mut gpui::TestAppContext) {
4113 use crate::test::editor_content_with_blocks_and_size;
4114 use gpui::size;
4115 use rope::Point;
4116
4117 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4118
4119 let long_line = "x".repeat(200);
4120 let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
4121 lines[25] = long_line;
4122 let content = lines.join("\n");
4123
4124 let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
4125
4126 editor.update(cx, |editor, cx| {
4127 let path = PathKey::for_buffer(&buffer, cx);
4128 editor.update_excerpts_for_path(
4129 path,
4130 buffer.clone(),
4131 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4132 0,
4133 diff.clone(),
4134 cx,
4135 );
4136 });
4137
4138 cx.run_until_parked();
4139
4140 let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
4141 let lhs = editor.lhs.as_ref().expect("should have lhs editor");
4142 (editor.rhs_editor.clone(), lhs.editor.clone())
4143 });
4144
4145 rhs_editor.update_in(cx, |e, window, cx| {
4146 e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
4147 });
4148
4149 let rhs_pos =
4150 rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4151 let lhs_pos =
4152 lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4153 assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
4154 assert_eq!(
4155 lhs_pos.y, rhs_pos.y,
4156 "LHS should have same scroll position as RHS after set_scroll_position"
4157 );
4158
4159 let draw_size = size(px(300.), px(300.));
4160
4161 rhs_editor.update_in(cx, |e, window, cx| {
4162 e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
4163 s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
4164 });
4165 });
4166
4167 let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
4168 cx.run_until_parked();
4169 let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
4170 cx.run_until_parked();
4171
4172 let rhs_pos =
4173 rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4174 let lhs_pos =
4175 lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4176
4177 assert!(
4178 rhs_pos.y > 0.,
4179 "RHS should have scrolled vertically to show cursor at row 25"
4180 );
4181 assert!(
4182 rhs_pos.x > 0.,
4183 "RHS should have scrolled horizontally to show cursor at column 150"
4184 );
4185 assert_eq!(
4186 lhs_pos.y, rhs_pos.y,
4187 "LHS should have same vertical scroll position as RHS after autoscroll"
4188 );
4189 assert_eq!(
4190 lhs_pos.x, rhs_pos.x,
4191 "LHS should have same horizontal scroll position as RHS after autoscroll"
4192 )
4193 }
4194
4195 #[gpui::test]
4196 async fn test_edit_line_before_soft_wrapped_line_preceding_hunk(cx: &mut gpui::TestAppContext) {
4197 use rope::Point;
4198 use unindent::Unindent as _;
4199
4200 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
4201
4202 let base_text = "
4203 first line
4204 aaaa bbbb cccc dddd eeee ffff
4205 original
4206 "
4207 .unindent();
4208
4209 let current_text = "
4210 first line
4211 aaaa bbbb cccc dddd eeee ffff
4212 modified
4213 "
4214 .unindent();
4215
4216 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4217
4218 editor.update(cx, |editor, cx| {
4219 let path = PathKey::for_buffer(&buffer, cx);
4220 editor.update_excerpts_for_path(
4221 path,
4222 buffer.clone(),
4223 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4224 0,
4225 diff.clone(),
4226 cx,
4227 );
4228 });
4229
4230 cx.run_until_parked();
4231
4232 assert_split_content_with_widths(
4233 &editor,
4234 px(400.0),
4235 px(200.0),
4236 "
4237 § <no file>
4238 § -----
4239 first line
4240 aaaa bbbb cccc dddd eeee ffff
4241 § spacer
4242 § spacer
4243 modified"
4244 .unindent(),
4245 "
4246 § <no file>
4247 § -----
4248 first line
4249 aaaa bbbb\x20
4250 cccc dddd\x20
4251 eeee ffff
4252 original"
4253 .unindent(),
4254 &mut cx,
4255 );
4256
4257 buffer.update(cx, |buffer, cx| {
4258 buffer.edit(
4259 [(Point::new(0, 0)..Point::new(0, 10), "edited first")],
4260 None,
4261 cx,
4262 );
4263 });
4264
4265 cx.run_until_parked();
4266
4267 assert_split_content_with_widths(
4268 &editor,
4269 px(400.0),
4270 px(200.0),
4271 "
4272 § <no file>
4273 § -----
4274 edited first
4275 aaaa bbbb cccc dddd eeee ffff
4276 § spacer
4277 § spacer
4278 modified"
4279 .unindent(),
4280 "
4281 § <no file>
4282 § -----
4283 first line
4284 aaaa bbbb\x20
4285 cccc dddd\x20
4286 eeee ffff
4287 original"
4288 .unindent(),
4289 &mut cx,
4290 );
4291
4292 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4293 diff.update(cx, |diff, cx| {
4294 diff.recalculate_diff_sync(&buffer_snapshot, cx);
4295 });
4296
4297 cx.run_until_parked();
4298
4299 assert_split_content_with_widths(
4300 &editor,
4301 px(400.0),
4302 px(200.0),
4303 "
4304 § <no file>
4305 § -----
4306 edited first
4307 aaaa bbbb cccc dddd eeee ffff
4308 § spacer
4309 § spacer
4310 modified"
4311 .unindent(),
4312 "
4313 § <no file>
4314 § -----
4315 first line
4316 aaaa bbbb\x20
4317 cccc dddd\x20
4318 eeee ffff
4319 original"
4320 .unindent(),
4321 &mut cx,
4322 );
4323 }
4324
4325 #[gpui::test]
4326 async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
4327 use rope::Point;
4328 use unindent::Unindent as _;
4329
4330 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4331
4332 let base_text = "
4333 bbb
4334 ccc
4335 "
4336 .unindent();
4337 let current_text = "
4338 aaa
4339 bbb
4340 ccc
4341 "
4342 .unindent();
4343
4344 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4345
4346 editor.update(cx, |editor, cx| {
4347 let path = PathKey::for_buffer(&buffer, cx);
4348 editor.update_excerpts_for_path(
4349 path,
4350 buffer.clone(),
4351 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4352 0,
4353 diff.clone(),
4354 cx,
4355 );
4356 });
4357
4358 cx.run_until_parked();
4359
4360 assert_split_content(
4361 &editor,
4362 "
4363 § <no file>
4364 § -----
4365 aaa
4366 bbb
4367 ccc"
4368 .unindent(),
4369 "
4370 § <no file>
4371 § -----
4372 § spacer
4373 bbb
4374 ccc"
4375 .unindent(),
4376 &mut cx,
4377 );
4378
4379 let block_ids = editor.update(cx, |splittable_editor, cx| {
4380 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4381 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4382 let anchor = snapshot.anchor_before(Point::new(2, 0));
4383 rhs_editor.insert_blocks(
4384 [BlockProperties {
4385 placement: BlockPlacement::Above(anchor),
4386 height: Some(1),
4387 style: BlockStyle::Fixed,
4388 render: Arc::new(|_| div().into_any()),
4389 priority: 0,
4390 }],
4391 None,
4392 cx,
4393 )
4394 })
4395 });
4396
4397 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4398 let lhs_editor =
4399 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4400
4401 cx.update(|_, cx| {
4402 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4403 "custom block".to_string()
4404 });
4405 });
4406
4407 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
4408 let display_map = lhs_editor.display_map.read(cx);
4409 let companion = display_map.companion().unwrap().read(cx);
4410 let mapping = companion
4411 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4412 *mapping.borrow().get(&block_ids[0]).unwrap()
4413 });
4414
4415 cx.update(|_, cx| {
4416 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
4417 "custom block".to_string()
4418 });
4419 });
4420
4421 cx.run_until_parked();
4422
4423 assert_split_content(
4424 &editor,
4425 "
4426 § <no file>
4427 § -----
4428 aaa
4429 bbb
4430 § custom block
4431 ccc"
4432 .unindent(),
4433 "
4434 § <no file>
4435 § -----
4436 § spacer
4437 bbb
4438 § custom block
4439 ccc"
4440 .unindent(),
4441 &mut cx,
4442 );
4443
4444 editor.update(cx, |splittable_editor, cx| {
4445 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4446 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
4447 });
4448 });
4449
4450 cx.run_until_parked();
4451
4452 assert_split_content(
4453 &editor,
4454 "
4455 § <no file>
4456 § -----
4457 aaa
4458 bbb
4459 ccc"
4460 .unindent(),
4461 "
4462 § <no file>
4463 § -----
4464 § spacer
4465 bbb
4466 ccc"
4467 .unindent(),
4468 &mut cx,
4469 );
4470 }
4471
4472 #[gpui::test]
4473 async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
4474 use rope::Point;
4475 use unindent::Unindent as _;
4476
4477 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4478
4479 let base_text = "
4480 bbb
4481 ccc
4482 "
4483 .unindent();
4484 let current_text = "
4485 aaa
4486 bbb
4487 ccc
4488 "
4489 .unindent();
4490
4491 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4492
4493 editor.update(cx, |editor, cx| {
4494 let path = PathKey::for_buffer(&buffer, cx);
4495 editor.update_excerpts_for_path(
4496 path,
4497 buffer.clone(),
4498 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4499 0,
4500 diff.clone(),
4501 cx,
4502 );
4503 });
4504
4505 cx.run_until_parked();
4506
4507 assert_split_content(
4508 &editor,
4509 "
4510 § <no file>
4511 § -----
4512 aaa
4513 bbb
4514 ccc"
4515 .unindent(),
4516 "
4517 § <no file>
4518 § -----
4519 § spacer
4520 bbb
4521 ccc"
4522 .unindent(),
4523 &mut cx,
4524 );
4525
4526 let block_ids = editor.update(cx, |splittable_editor, cx| {
4527 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4528 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4529 let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4530 let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4531 rhs_editor.insert_blocks(
4532 [
4533 BlockProperties {
4534 placement: BlockPlacement::Above(anchor1),
4535 height: Some(1),
4536 style: BlockStyle::Fixed,
4537 render: Arc::new(|_| div().into_any()),
4538 priority: 0,
4539 },
4540 BlockProperties {
4541 placement: BlockPlacement::Above(anchor2),
4542 height: Some(1),
4543 style: BlockStyle::Fixed,
4544 render: Arc::new(|_| div().into_any()),
4545 priority: 0,
4546 },
4547 ],
4548 None,
4549 cx,
4550 )
4551 })
4552 });
4553
4554 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4555 let lhs_editor =
4556 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4557
4558 cx.update(|_, cx| {
4559 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4560 "custom block 1".to_string()
4561 });
4562 set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4563 "custom block 2".to_string()
4564 });
4565 });
4566
4567 let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4568 let display_map = lhs_editor.display_map.read(cx);
4569 let companion = display_map.companion().unwrap().read(cx);
4570 let mapping = companion
4571 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4572 (
4573 *mapping.borrow().get(&block_ids[0]).unwrap(),
4574 *mapping.borrow().get(&block_ids[1]).unwrap(),
4575 )
4576 });
4577
4578 cx.update(|_, cx| {
4579 set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4580 "custom block 1".to_string()
4581 });
4582 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4583 "custom block 2".to_string()
4584 });
4585 });
4586
4587 cx.run_until_parked();
4588
4589 assert_split_content(
4590 &editor,
4591 "
4592 § <no file>
4593 § -----
4594 aaa
4595 bbb
4596 § custom block 1
4597 ccc
4598 § custom block 2"
4599 .unindent(),
4600 "
4601 § <no file>
4602 § -----
4603 § spacer
4604 bbb
4605 § custom block 1
4606 ccc
4607 § custom block 2"
4608 .unindent(),
4609 &mut cx,
4610 );
4611
4612 editor.update(cx, |splittable_editor, cx| {
4613 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4614 rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4615 });
4616 });
4617
4618 cx.run_until_parked();
4619
4620 assert_split_content(
4621 &editor,
4622 "
4623 § <no file>
4624 § -----
4625 aaa
4626 bbb
4627 ccc
4628 § custom block 2"
4629 .unindent(),
4630 "
4631 § <no file>
4632 § -----
4633 § spacer
4634 bbb
4635 ccc
4636 § custom block 2"
4637 .unindent(),
4638 &mut cx,
4639 );
4640
4641 editor.update_in(cx, |splittable_editor, window, cx| {
4642 splittable_editor.unsplit(window, cx);
4643 });
4644
4645 cx.run_until_parked();
4646
4647 editor.update_in(cx, |splittable_editor, window, cx| {
4648 splittable_editor.split(window, cx);
4649 });
4650
4651 cx.run_until_parked();
4652
4653 let lhs_editor =
4654 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4655
4656 let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4657 let display_map = lhs_editor.display_map.read(cx);
4658 let companion = display_map.companion().unwrap().read(cx);
4659 let mapping = companion
4660 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4661 *mapping.borrow().get(&block_ids[1]).unwrap()
4662 });
4663
4664 cx.update(|_, cx| {
4665 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4666 "custom block 2".to_string()
4667 });
4668 });
4669
4670 cx.run_until_parked();
4671
4672 assert_split_content(
4673 &editor,
4674 "
4675 § <no file>
4676 § -----
4677 aaa
4678 bbb
4679 ccc
4680 § custom block 2"
4681 .unindent(),
4682 "
4683 § <no file>
4684 § -----
4685 § spacer
4686 bbb
4687 ccc
4688 § custom block 2"
4689 .unindent(),
4690 &mut cx,
4691 );
4692 }
4693
4694 #[gpui::test]
4695 async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
4696 use rope::Point;
4697 use unindent::Unindent as _;
4698
4699 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4700
4701 let base_text = "
4702 bbb
4703 ccc
4704 "
4705 .unindent();
4706 let current_text = "
4707 aaa
4708 bbb
4709 ccc
4710 "
4711 .unindent();
4712
4713 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4714
4715 editor.update(cx, |editor, cx| {
4716 let path = PathKey::for_buffer(&buffer, cx);
4717 editor.update_excerpts_for_path(
4718 path,
4719 buffer.clone(),
4720 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4721 0,
4722 diff.clone(),
4723 cx,
4724 );
4725 });
4726
4727 cx.run_until_parked();
4728
4729 editor.update_in(cx, |splittable_editor, window, cx| {
4730 splittable_editor.unsplit(window, cx);
4731 });
4732
4733 cx.run_until_parked();
4734
4735 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4736
4737 let block_ids = editor.update(cx, |splittable_editor, cx| {
4738 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4739 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4740 let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4741 let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4742 rhs_editor.insert_blocks(
4743 [
4744 BlockProperties {
4745 placement: BlockPlacement::Above(anchor1),
4746 height: Some(1),
4747 style: BlockStyle::Fixed,
4748 render: Arc::new(|_| div().into_any()),
4749 priority: 0,
4750 },
4751 BlockProperties {
4752 placement: BlockPlacement::Above(anchor2),
4753 height: Some(1),
4754 style: BlockStyle::Fixed,
4755 render: Arc::new(|_| div().into_any()),
4756 priority: 0,
4757 },
4758 ],
4759 None,
4760 cx,
4761 )
4762 })
4763 });
4764
4765 cx.update(|_, cx| {
4766 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4767 "custom block 1".to_string()
4768 });
4769 set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4770 "custom block 2".to_string()
4771 });
4772 });
4773
4774 cx.run_until_parked();
4775
4776 let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
4777 assert_eq!(
4778 rhs_content,
4779 "
4780 § <no file>
4781 § -----
4782 aaa
4783 bbb
4784 § custom block 1
4785 ccc
4786 § custom block 2"
4787 .unindent(),
4788 "rhs content before split"
4789 );
4790
4791 editor.update_in(cx, |splittable_editor, window, cx| {
4792 splittable_editor.split(window, cx);
4793 });
4794
4795 cx.run_until_parked();
4796
4797 let lhs_editor =
4798 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4799
4800 let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4801 let display_map = lhs_editor.display_map.read(cx);
4802 let companion = display_map.companion().unwrap().read(cx);
4803 let mapping = companion
4804 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4805 (
4806 *mapping.borrow().get(&block_ids[0]).unwrap(),
4807 *mapping.borrow().get(&block_ids[1]).unwrap(),
4808 )
4809 });
4810
4811 cx.update(|_, cx| {
4812 set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4813 "custom block 1".to_string()
4814 });
4815 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4816 "custom block 2".to_string()
4817 });
4818 });
4819
4820 cx.run_until_parked();
4821
4822 assert_split_content(
4823 &editor,
4824 "
4825 § <no file>
4826 § -----
4827 aaa
4828 bbb
4829 § custom block 1
4830 ccc
4831 § custom block 2"
4832 .unindent(),
4833 "
4834 § <no file>
4835 § -----
4836 § spacer
4837 bbb
4838 § custom block 1
4839 ccc
4840 § custom block 2"
4841 .unindent(),
4842 &mut cx,
4843 );
4844
4845 editor.update(cx, |splittable_editor, cx| {
4846 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4847 rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4848 });
4849 });
4850
4851 cx.run_until_parked();
4852
4853 assert_split_content(
4854 &editor,
4855 "
4856 § <no file>
4857 § -----
4858 aaa
4859 bbb
4860 ccc
4861 § custom block 2"
4862 .unindent(),
4863 "
4864 § <no file>
4865 § -----
4866 § spacer
4867 bbb
4868 ccc
4869 § custom block 2"
4870 .unindent(),
4871 &mut cx,
4872 );
4873
4874 editor.update_in(cx, |splittable_editor, window, cx| {
4875 splittable_editor.unsplit(window, cx);
4876 });
4877
4878 cx.run_until_parked();
4879
4880 editor.update_in(cx, |splittable_editor, window, cx| {
4881 splittable_editor.split(window, cx);
4882 });
4883
4884 cx.run_until_parked();
4885
4886 let lhs_editor =
4887 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4888
4889 let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4890 let display_map = lhs_editor.display_map.read(cx);
4891 let companion = display_map.companion().unwrap().read(cx);
4892 let mapping = companion
4893 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4894 *mapping.borrow().get(&block_ids[1]).unwrap()
4895 });
4896
4897 cx.update(|_, cx| {
4898 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4899 "custom block 2".to_string()
4900 });
4901 });
4902
4903 cx.run_until_parked();
4904
4905 assert_split_content(
4906 &editor,
4907 "
4908 § <no file>
4909 § -----
4910 aaa
4911 bbb
4912 ccc
4913 § custom block 2"
4914 .unindent(),
4915 "
4916 § <no file>
4917 § -----
4918 § spacer
4919 bbb
4920 ccc
4921 § custom block 2"
4922 .unindent(),
4923 &mut cx,
4924 );
4925
4926 let new_block_ids = editor.update(cx, |splittable_editor, cx| {
4927 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4928 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4929 let anchor = snapshot.anchor_before(Point::new(2, 0));
4930 rhs_editor.insert_blocks(
4931 [BlockProperties {
4932 placement: BlockPlacement::Above(anchor),
4933 height: Some(1),
4934 style: BlockStyle::Fixed,
4935 render: Arc::new(|_| div().into_any()),
4936 priority: 0,
4937 }],
4938 None,
4939 cx,
4940 )
4941 })
4942 });
4943
4944 cx.update(|_, cx| {
4945 set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
4946 "custom block 3".to_string()
4947 });
4948 });
4949
4950 let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4951 let display_map = lhs_editor.display_map.read(cx);
4952 let companion = display_map.companion().unwrap().read(cx);
4953 let mapping = companion
4954 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4955 *mapping.borrow().get(&new_block_ids[0]).unwrap()
4956 });
4957
4958 cx.update(|_, cx| {
4959 set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
4960 "custom block 3".to_string()
4961 });
4962 });
4963
4964 cx.run_until_parked();
4965
4966 assert_split_content(
4967 &editor,
4968 "
4969 § <no file>
4970 § -----
4971 aaa
4972 bbb
4973 § custom block 3
4974 ccc
4975 § custom block 2"
4976 .unindent(),
4977 "
4978 § <no file>
4979 § -----
4980 § spacer
4981 bbb
4982 § custom block 3
4983 ccc
4984 § custom block 2"
4985 .unindent(),
4986 &mut cx,
4987 );
4988
4989 editor.update(cx, |splittable_editor, cx| {
4990 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4991 rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
4992 });
4993 });
4994
4995 cx.run_until_parked();
4996
4997 assert_split_content(
4998 &editor,
4999 "
5000 § <no file>
5001 § -----
5002 aaa
5003 bbb
5004 ccc
5005 § custom block 2"
5006 .unindent(),
5007 "
5008 § <no file>
5009 § -----
5010 § spacer
5011 bbb
5012 ccc
5013 § custom block 2"
5014 .unindent(),
5015 &mut cx,
5016 );
5017 }
5018
5019 #[gpui::test]
5020 async fn test_buffer_folding_sync(cx: &mut gpui::TestAppContext) {
5021 use rope::Point;
5022 use unindent::Unindent as _;
5023
5024 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5025
5026 let base_text1 = "
5027 aaa
5028 bbb
5029 ccc"
5030 .unindent();
5031 let current_text1 = "
5032 aaa
5033 bbb
5034 ccc"
5035 .unindent();
5036
5037 let base_text2 = "
5038 ddd
5039 eee
5040 fff"
5041 .unindent();
5042 let current_text2 = "
5043 ddd
5044 eee
5045 fff"
5046 .unindent();
5047
5048 let (buffer1, diff1) = buffer_with_diff(&base_text1, ¤t_text1, &mut cx);
5049 let (buffer2, diff2) = buffer_with_diff(&base_text2, ¤t_text2, &mut cx);
5050
5051 let buffer1_id = buffer1.read_with(cx, |buffer, _| buffer.remote_id());
5052 let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
5053
5054 editor.update(cx, |editor, cx| {
5055 let path1 = PathKey::for_buffer(&buffer1, cx);
5056 editor.update_excerpts_for_path(
5057 path1,
5058 buffer1.clone(),
5059 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
5060 0,
5061 diff1.clone(),
5062 cx,
5063 );
5064 let path2 = PathKey::for_buffer(&buffer2, cx);
5065 editor.update_excerpts_for_path(
5066 path2,
5067 buffer2.clone(),
5068 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
5069 1,
5070 diff2.clone(),
5071 cx,
5072 );
5073 });
5074
5075 cx.run_until_parked();
5076
5077 editor.update(cx, |editor, cx| {
5078 editor.rhs_editor.update(cx, |rhs_editor, cx| {
5079 rhs_editor.fold_buffer(buffer1_id, cx);
5080 });
5081 });
5082
5083 cx.run_until_parked();
5084
5085 let rhs_buffer1_folded = editor.read_with(cx, |editor, cx| {
5086 editor.rhs_editor.read(cx).is_buffer_folded(buffer1_id, cx)
5087 });
5088 assert!(
5089 rhs_buffer1_folded,
5090 "buffer1 should be folded in rhs before split"
5091 );
5092
5093 editor.update_in(cx, |editor, window, cx| {
5094 editor.split(window, cx);
5095 });
5096
5097 cx.run_until_parked();
5098
5099 let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
5100 (
5101 editor.rhs_editor.clone(),
5102 editor.lhs.as_ref().unwrap().editor.clone(),
5103 )
5104 });
5105
5106 let rhs_buffer1_folded =
5107 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
5108 assert!(
5109 rhs_buffer1_folded,
5110 "buffer1 should be folded in rhs after split"
5111 );
5112
5113 let base_buffer1_id = diff1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
5114 let lhs_buffer1_folded = lhs_editor.read_with(cx, |editor, cx| {
5115 editor.is_buffer_folded(base_buffer1_id, cx)
5116 });
5117 assert!(
5118 lhs_buffer1_folded,
5119 "buffer1 should be folded in lhs after split"
5120 );
5121
5122 assert_split_content(
5123 &editor,
5124 "
5125 § <no file>
5126 § -----
5127 § <no file>
5128 § -----
5129 ddd
5130 eee
5131 fff"
5132 .unindent(),
5133 "
5134 § <no file>
5135 § -----
5136 § <no file>
5137 § -----
5138 ddd
5139 eee
5140 fff"
5141 .unindent(),
5142 &mut cx,
5143 );
5144
5145 editor.update(cx, |editor, cx| {
5146 editor.rhs_editor.update(cx, |rhs_editor, cx| {
5147 rhs_editor.fold_buffer(buffer2_id, cx);
5148 });
5149 });
5150
5151 cx.run_until_parked();
5152
5153 let rhs_buffer2_folded =
5154 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer2_id, cx));
5155 assert!(rhs_buffer2_folded, "buffer2 should be folded in rhs");
5156
5157 let base_buffer2_id = diff2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
5158 let lhs_buffer2_folded = lhs_editor.read_with(cx, |editor, cx| {
5159 editor.is_buffer_folded(base_buffer2_id, cx)
5160 });
5161 assert!(lhs_buffer2_folded, "buffer2 should be folded in lhs");
5162
5163 let rhs_buffer1_still_folded =
5164 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
5165 assert!(
5166 rhs_buffer1_still_folded,
5167 "buffer1 should still be folded in rhs"
5168 );
5169
5170 let lhs_buffer1_still_folded = lhs_editor.read_with(cx, |editor, cx| {
5171 editor.is_buffer_folded(base_buffer1_id, cx)
5172 });
5173 assert!(
5174 lhs_buffer1_still_folded,
5175 "buffer1 should still be folded in lhs"
5176 );
5177
5178 assert_split_content(
5179 &editor,
5180 "
5181 § <no file>
5182 § -----
5183 § <no file>
5184 § -----"
5185 .unindent(),
5186 "
5187 § <no file>
5188 § -----
5189 § <no file>
5190 § -----"
5191 .unindent(),
5192 &mut cx,
5193 );
5194 }
5195
5196 #[gpui::test]
5197 async fn test_custom_block_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5198 use rope::Point;
5199 use unindent::Unindent as _;
5200
5201 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5202
5203 let base_text = "
5204 ddd
5205 eee
5206 "
5207 .unindent();
5208 let current_text = "
5209 aaa
5210 bbb
5211 ccc
5212 ddd
5213 eee
5214 "
5215 .unindent();
5216
5217 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5218
5219 editor.update(cx, |editor, cx| {
5220 let path = PathKey::for_buffer(&buffer, cx);
5221 editor.update_excerpts_for_path(
5222 path,
5223 buffer.clone(),
5224 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5225 0,
5226 diff.clone(),
5227 cx,
5228 );
5229 });
5230
5231 cx.run_until_parked();
5232
5233 assert_split_content(
5234 &editor,
5235 "
5236 § <no file>
5237 § -----
5238 aaa
5239 bbb
5240 ccc
5241 ddd
5242 eee"
5243 .unindent(),
5244 "
5245 § <no file>
5246 § -----
5247 § spacer
5248 § spacer
5249 § spacer
5250 ddd
5251 eee"
5252 .unindent(),
5253 &mut cx,
5254 );
5255
5256 let block_ids = editor.update(cx, |splittable_editor, cx| {
5257 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5258 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5259 let anchor = snapshot.anchor_before(Point::new(2, 0));
5260 rhs_editor.insert_blocks(
5261 [BlockProperties {
5262 placement: BlockPlacement::Above(anchor),
5263 height: Some(1),
5264 style: BlockStyle::Fixed,
5265 render: Arc::new(|_| div().into_any()),
5266 priority: 0,
5267 }],
5268 None,
5269 cx,
5270 )
5271 })
5272 });
5273
5274 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5275 let lhs_editor =
5276 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5277
5278 cx.update(|_, cx| {
5279 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5280 "custom block".to_string()
5281 });
5282 });
5283
5284 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5285 let display_map = lhs_editor.display_map.read(cx);
5286 let companion = display_map.companion().unwrap().read(cx);
5287 let mapping = companion
5288 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5289 *mapping.borrow().get(&block_ids[0]).unwrap()
5290 });
5291
5292 cx.update(|_, cx| {
5293 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5294 "custom block".to_string()
5295 });
5296 });
5297
5298 cx.run_until_parked();
5299
5300 assert_split_content(
5301 &editor,
5302 "
5303 § <no file>
5304 § -----
5305 aaa
5306 bbb
5307 § custom block
5308 ccc
5309 ddd
5310 eee"
5311 .unindent(),
5312 "
5313 § <no file>
5314 § -----
5315 § spacer
5316 § spacer
5317 § spacer
5318 § custom block
5319 ddd
5320 eee"
5321 .unindent(),
5322 &mut cx,
5323 );
5324
5325 editor.update(cx, |splittable_editor, cx| {
5326 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5327 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5328 });
5329 });
5330
5331 cx.run_until_parked();
5332
5333 assert_split_content(
5334 &editor,
5335 "
5336 § <no file>
5337 § -----
5338 aaa
5339 bbb
5340 ccc
5341 ddd
5342 eee"
5343 .unindent(),
5344 "
5345 § <no file>
5346 § -----
5347 § spacer
5348 § spacer
5349 § spacer
5350 ddd
5351 eee"
5352 .unindent(),
5353 &mut cx,
5354 );
5355 }
5356
5357 #[gpui::test]
5358 async fn test_custom_block_below_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5359 use rope::Point;
5360 use unindent::Unindent as _;
5361
5362 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5363
5364 let base_text = "
5365 ddd
5366 eee
5367 "
5368 .unindent();
5369 let current_text = "
5370 aaa
5371 bbb
5372 ccc
5373 ddd
5374 eee
5375 "
5376 .unindent();
5377
5378 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5379
5380 editor.update(cx, |editor, cx| {
5381 let path = PathKey::for_buffer(&buffer, cx);
5382 editor.update_excerpts_for_path(
5383 path,
5384 buffer.clone(),
5385 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5386 0,
5387 diff.clone(),
5388 cx,
5389 );
5390 });
5391
5392 cx.run_until_parked();
5393
5394 assert_split_content(
5395 &editor,
5396 "
5397 § <no file>
5398 § -----
5399 aaa
5400 bbb
5401 ccc
5402 ddd
5403 eee"
5404 .unindent(),
5405 "
5406 § <no file>
5407 § -----
5408 § spacer
5409 § spacer
5410 § spacer
5411 ddd
5412 eee"
5413 .unindent(),
5414 &mut cx,
5415 );
5416
5417 let block_ids = editor.update(cx, |splittable_editor, cx| {
5418 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5419 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5420 let anchor = snapshot.anchor_after(Point::new(1, 3));
5421 rhs_editor.insert_blocks(
5422 [BlockProperties {
5423 placement: BlockPlacement::Below(anchor),
5424 height: Some(1),
5425 style: BlockStyle::Fixed,
5426 render: Arc::new(|_| div().into_any()),
5427 priority: 0,
5428 }],
5429 None,
5430 cx,
5431 )
5432 })
5433 });
5434
5435 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5436 let lhs_editor =
5437 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5438
5439 cx.update(|_, cx| {
5440 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5441 "custom block".to_string()
5442 });
5443 });
5444
5445 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5446 let display_map = lhs_editor.display_map.read(cx);
5447 let companion = display_map.companion().unwrap().read(cx);
5448 let mapping = companion
5449 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5450 *mapping.borrow().get(&block_ids[0]).unwrap()
5451 });
5452
5453 cx.update(|_, cx| {
5454 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5455 "custom block".to_string()
5456 });
5457 });
5458
5459 cx.run_until_parked();
5460
5461 assert_split_content(
5462 &editor,
5463 "
5464 § <no file>
5465 § -----
5466 aaa
5467 bbb
5468 § custom block
5469 ccc
5470 ddd
5471 eee"
5472 .unindent(),
5473 "
5474 § <no file>
5475 § -----
5476 § spacer
5477 § spacer
5478 § spacer
5479 § custom block
5480 ddd
5481 eee"
5482 .unindent(),
5483 &mut cx,
5484 );
5485
5486 editor.update(cx, |splittable_editor, cx| {
5487 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5488 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5489 });
5490 });
5491
5492 cx.run_until_parked();
5493
5494 assert_split_content(
5495 &editor,
5496 "
5497 § <no file>
5498 § -----
5499 aaa
5500 bbb
5501 ccc
5502 ddd
5503 eee"
5504 .unindent(),
5505 "
5506 § <no file>
5507 § -----
5508 § spacer
5509 § spacer
5510 § spacer
5511 ddd
5512 eee"
5513 .unindent(),
5514 &mut cx,
5515 );
5516 }
5517
5518 #[gpui::test]
5519 async fn test_custom_block_resize_syncs_balancing_block(cx: &mut gpui::TestAppContext) {
5520 use rope::Point;
5521 use unindent::Unindent as _;
5522
5523 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5524
5525 let base_text = "
5526 bbb
5527 ccc
5528 "
5529 .unindent();
5530 let current_text = "
5531 aaa
5532 bbb
5533 ccc
5534 "
5535 .unindent();
5536
5537 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5538
5539 editor.update(cx, |editor, cx| {
5540 let path = PathKey::for_buffer(&buffer, cx);
5541 editor.update_excerpts_for_path(
5542 path,
5543 buffer.clone(),
5544 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5545 0,
5546 diff.clone(),
5547 cx,
5548 );
5549 });
5550
5551 cx.run_until_parked();
5552
5553 let block_ids = editor.update(cx, |splittable_editor, cx| {
5554 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5555 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5556 let anchor = snapshot.anchor_before(Point::new(2, 0));
5557 rhs_editor.insert_blocks(
5558 [BlockProperties {
5559 placement: BlockPlacement::Above(anchor),
5560 height: Some(1),
5561 style: BlockStyle::Fixed,
5562 render: Arc::new(|_| div().into_any()),
5563 priority: 0,
5564 }],
5565 None,
5566 cx,
5567 )
5568 })
5569 });
5570
5571 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5572 let lhs_editor =
5573 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5574
5575 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5576 let display_map = lhs_editor.display_map.read(cx);
5577 let companion = display_map.companion().unwrap().read(cx);
5578 let mapping = companion
5579 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5580 *mapping.borrow().get(&block_ids[0]).unwrap()
5581 });
5582
5583 cx.run_until_parked();
5584
5585 let get_block_height = |editor: &Entity<crate::Editor>,
5586 block_id: crate::CustomBlockId,
5587 cx: &mut VisualTestContext| {
5588 editor.update_in(cx, |editor, window, cx| {
5589 let snapshot = editor.snapshot(window, cx);
5590 snapshot
5591 .block_for_id(crate::BlockId::Custom(block_id))
5592 .map(|block| block.height())
5593 })
5594 };
5595
5596 assert_eq!(
5597 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5598 Some(1)
5599 );
5600 assert_eq!(
5601 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5602 Some(1)
5603 );
5604
5605 editor.update(cx, |splittable_editor, cx| {
5606 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5607 let mut heights = HashMap::default();
5608 heights.insert(block_ids[0], 3);
5609 rhs_editor.resize_blocks(heights, None, cx);
5610 });
5611 });
5612
5613 cx.run_until_parked();
5614
5615 assert_eq!(
5616 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5617 Some(3)
5618 );
5619 assert_eq!(
5620 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5621 Some(3)
5622 );
5623
5624 editor.update(cx, |splittable_editor, cx| {
5625 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5626 let mut heights = HashMap::default();
5627 heights.insert(block_ids[0], 5);
5628 rhs_editor.resize_blocks(heights, None, cx);
5629 });
5630 });
5631
5632 cx.run_until_parked();
5633
5634 assert_eq!(
5635 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5636 Some(5)
5637 );
5638 assert_eq!(
5639 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5640 Some(5)
5641 );
5642 }
5643
5644 #[gpui::test]
5645 async fn test_edit_spanning_excerpt_boundaries_then_resplit(cx: &mut gpui::TestAppContext) {
5646 use rope::Point;
5647 use unindent::Unindent as _;
5648
5649 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5650
5651 let base_text = "
5652 aaa
5653 bbb
5654 ccc
5655 ddd
5656 eee
5657 fff
5658 ggg
5659 hhh
5660 iii
5661 jjj
5662 kkk
5663 lll
5664 "
5665 .unindent();
5666 let current_text = base_text.clone();
5667
5668 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5669
5670 editor.update(cx, |editor, cx| {
5671 let path = PathKey::for_buffer(&buffer, cx);
5672 editor.update_excerpts_for_path(
5673 path,
5674 buffer.clone(),
5675 vec![
5676 Point::new(0, 0)..Point::new(3, 3),
5677 Point::new(5, 0)..Point::new(8, 3),
5678 Point::new(10, 0)..Point::new(11, 3),
5679 ],
5680 0,
5681 diff.clone(),
5682 cx,
5683 );
5684 });
5685
5686 cx.run_until_parked();
5687
5688 buffer.update(cx, |buffer, cx| {
5689 buffer.edit([(Point::new(1, 0)..Point::new(10, 0), "")], None, cx);
5690 });
5691
5692 cx.run_until_parked();
5693
5694 editor.update_in(cx, |splittable_editor, window, cx| {
5695 splittable_editor.unsplit(window, cx);
5696 });
5697
5698 cx.run_until_parked();
5699
5700 editor.update_in(cx, |splittable_editor, window, cx| {
5701 splittable_editor.split(window, cx);
5702 });
5703
5704 cx.run_until_parked();
5705 }
5706
5707 #[gpui::test]
5708 async fn test_range_folds_removed_on_split(cx: &mut gpui::TestAppContext) {
5709 use rope::Point;
5710 use unindent::Unindent as _;
5711
5712 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5713
5714 let base_text = "
5715 aaa
5716 bbb
5717 ccc
5718 ddd
5719 eee"
5720 .unindent();
5721 let current_text = "
5722 aaa
5723 bbb
5724 ccc
5725 ddd
5726 eee"
5727 .unindent();
5728
5729 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5730
5731 editor.update(cx, |editor, cx| {
5732 let path = PathKey::for_buffer(&buffer, cx);
5733 editor.update_excerpts_for_path(
5734 path,
5735 buffer.clone(),
5736 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5737 0,
5738 diff.clone(),
5739 cx,
5740 );
5741 });
5742
5743 cx.run_until_parked();
5744
5745 editor.update_in(cx, |editor, window, cx| {
5746 editor.rhs_editor.update(cx, |rhs_editor, cx| {
5747 rhs_editor.fold_creases(
5748 vec![Crease::simple(
5749 Point::new(1, 0)..Point::new(3, 0),
5750 FoldPlaceholder::test(),
5751 )],
5752 false,
5753 window,
5754 cx,
5755 );
5756 });
5757 });
5758
5759 cx.run_until_parked();
5760
5761 editor.update_in(cx, |editor, window, cx| {
5762 editor.split(window, cx);
5763 });
5764
5765 cx.run_until_parked();
5766
5767 let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
5768 (
5769 editor.rhs_editor.clone(),
5770 editor.lhs.as_ref().unwrap().editor.clone(),
5771 )
5772 });
5773
5774 let rhs_has_folds_after_split = rhs_editor.update(cx, |editor, cx| {
5775 let snapshot = editor.display_snapshot(cx);
5776 snapshot
5777 .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5778 .next()
5779 .is_some()
5780 });
5781 assert!(
5782 !rhs_has_folds_after_split,
5783 "rhs should not have range folds after split"
5784 );
5785
5786 let lhs_has_folds = lhs_editor.update(cx, |editor, cx| {
5787 let snapshot = editor.display_snapshot(cx);
5788 snapshot
5789 .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5790 .next()
5791 .is_some()
5792 });
5793 assert!(!lhs_has_folds, "lhs should not have any range folds");
5794 }
5795
5796 #[gpui::test]
5797 async fn test_multiline_inlays_create_spacers(cx: &mut gpui::TestAppContext) {
5798 use rope::Point;
5799 use unindent::Unindent as _;
5800
5801 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5802
5803 let base_text = "
5804 aaa
5805 bbb
5806 ccc
5807 ddd
5808 "
5809 .unindent();
5810 let current_text = base_text.clone();
5811
5812 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5813
5814 editor.update(cx, |editor, cx| {
5815 let path = PathKey::for_buffer(&buffer, cx);
5816 editor.update_excerpts_for_path(
5817 path,
5818 buffer.clone(),
5819 vec![Point::new(0, 0)..Point::new(3, 3)],
5820 0,
5821 diff.clone(),
5822 cx,
5823 );
5824 });
5825
5826 cx.run_until_parked();
5827
5828 let rhs_editor = editor.read_with(cx, |e, _| e.rhs_editor.clone());
5829 rhs_editor.update(cx, |rhs_editor, cx| {
5830 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5831 rhs_editor.splice_inlays(
5832 &[],
5833 vec![
5834 Inlay::edit_prediction(
5835 0,
5836 snapshot.anchor_after(Point::new(0, 3)),
5837 "\nINLAY_WITHIN",
5838 ),
5839 Inlay::edit_prediction(
5840 1,
5841 snapshot.anchor_after(Point::new(1, 3)),
5842 "\nINLAY_MID_1\nINLAY_MID_2",
5843 ),
5844 Inlay::edit_prediction(
5845 2,
5846 snapshot.anchor_after(Point::new(3, 3)),
5847 "\nINLAY_END_1\nINLAY_END_2",
5848 ),
5849 ],
5850 cx,
5851 );
5852 });
5853
5854 cx.run_until_parked();
5855
5856 assert_split_content(
5857 &editor,
5858 "
5859 § <no file>
5860 § -----
5861 aaa
5862 INLAY_WITHIN
5863 bbb
5864 INLAY_MID_1
5865 INLAY_MID_2
5866 ccc
5867 ddd
5868 INLAY_END_1
5869 INLAY_END_2"
5870 .unindent(),
5871 "
5872 § <no file>
5873 § -----
5874 aaa
5875 § spacer
5876 bbb
5877 § spacer
5878 § spacer
5879 ccc
5880 ddd
5881 § spacer
5882 § spacer"
5883 .unindent(),
5884 &mut cx,
5885 );
5886 }
5887
5888 #[gpui::test]
5889 async fn test_split_after_removing_folded_buffer(cx: &mut gpui::TestAppContext) {
5890 use rope::Point;
5891 use unindent::Unindent as _;
5892
5893 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5894
5895 let base_text_a = "
5896 aaa
5897 bbb
5898 ccc
5899 "
5900 .unindent();
5901 let current_text_a = "
5902 aaa
5903 bbb modified
5904 ccc
5905 "
5906 .unindent();
5907
5908 let base_text_b = "
5909 xxx
5910 yyy
5911 zzz
5912 "
5913 .unindent();
5914 let current_text_b = "
5915 xxx
5916 yyy modified
5917 zzz
5918 "
5919 .unindent();
5920
5921 let (buffer_a, diff_a) = buffer_with_diff(&base_text_a, ¤t_text_a, &mut cx);
5922 let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, ¤t_text_b, &mut cx);
5923
5924 let path_a = cx.read(|cx| PathKey::for_buffer(&buffer_a, cx));
5925 let path_b = cx.read(|cx| PathKey::for_buffer(&buffer_b, cx));
5926
5927 editor.update(cx, |editor, cx| {
5928 editor.update_excerpts_for_path(
5929 path_a.clone(),
5930 buffer_a.clone(),
5931 vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
5932 0,
5933 diff_a.clone(),
5934 cx,
5935 );
5936 editor.update_excerpts_for_path(
5937 path_b.clone(),
5938 buffer_b.clone(),
5939 vec![Point::new(0, 0)..buffer_b.read(cx).max_point()],
5940 0,
5941 diff_b.clone(),
5942 cx,
5943 );
5944 });
5945
5946 cx.run_until_parked();
5947
5948 let buffer_a_id = buffer_a.read_with(cx, |buffer, _| buffer.remote_id());
5949 editor.update(cx, |editor, cx| {
5950 editor.rhs_editor().update(cx, |right_editor, cx| {
5951 right_editor.fold_buffer(buffer_a_id, cx)
5952 });
5953 });
5954
5955 cx.run_until_parked();
5956
5957 editor.update(cx, |editor, cx| {
5958 editor.remove_excerpts_for_path(path_a.clone(), cx);
5959 });
5960 cx.run_until_parked();
5961
5962 editor.update_in(cx, |editor, window, cx| editor.split(window, cx));
5963 cx.run_until_parked();
5964
5965 editor.update(cx, |editor, cx| {
5966 editor.update_excerpts_for_path(
5967 path_a.clone(),
5968 buffer_a.clone(),
5969 vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
5970 0,
5971 diff_a.clone(),
5972 cx,
5973 );
5974 assert!(
5975 !editor
5976 .lhs_editor()
5977 .unwrap()
5978 .read(cx)
5979 .is_buffer_folded(buffer_a_id, cx)
5980 );
5981 assert!(
5982 !editor
5983 .rhs_editor()
5984 .read(cx)
5985 .is_buffer_folded(buffer_a_id, cx)
5986 );
5987 });
5988 }
5989
5990 #[gpui::test]
5991 async fn test_two_path_keys_for_one_buffer(cx: &mut gpui::TestAppContext) {
5992 use multi_buffer::PathKey;
5993 use rope::Point;
5994 use unindent::Unindent as _;
5995
5996 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5997
5998 let base_text = "
5999 aaa
6000 bbb
6001 ccc
6002 "
6003 .unindent();
6004 let current_text = "
6005 aaa
6006 bbb modified
6007 ccc
6008 "
6009 .unindent();
6010
6011 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
6012
6013 let path_key_1 = PathKey {
6014 sort_prefix: Some(0),
6015 path: rel_path("file1.txt").into(),
6016 };
6017 let path_key_2 = PathKey {
6018 sort_prefix: Some(1),
6019 path: rel_path("file1.txt").into(),
6020 };
6021
6022 editor.update(cx, |editor, cx| {
6023 editor.update_excerpts_for_path(
6024 path_key_1.clone(),
6025 buffer.clone(),
6026 vec![Point::new(0, 0)..Point::new(1, 0)],
6027 0,
6028 diff.clone(),
6029 cx,
6030 );
6031 editor.update_excerpts_for_path(
6032 path_key_2.clone(),
6033 buffer.clone(),
6034 vec![Point::new(1, 0)..buffer.read(cx).max_point()],
6035 1,
6036 diff.clone(),
6037 cx,
6038 );
6039 });
6040
6041 cx.run_until_parked();
6042 }
6043
6044 #[gpui::test]
6045 async fn test_act_as_type(cx: &mut gpui::TestAppContext) {
6046 let (splittable_editor, cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
6047 let editor = splittable_editor.read_with(cx, |editor, cx| {
6048 editor.act_as_type(TypeId::of::<Editor>(), &splittable_editor, cx)
6049 });
6050
6051 assert!(
6052 editor.is_some(),
6053 "SplittableEditor should be able to act as Editor"
6054 );
6055 }
6056}