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