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