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