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