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