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