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