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