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