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