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