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