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