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