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