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