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