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