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