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