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