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