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