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