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