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 lhs_multibuffer.add_inverted_diff(diff, lhs_cx);
2080 }
2081
2082 let rhs_merge_groups: Vec<Vec<ExcerptId>> = {
2083 let mut groups = Vec::new();
2084 let mut current_group = Vec::new();
2085 let mut last_id = None;
2086
2087 for (lhs_id, rhs_id) in lhs_result.excerpt_ids.iter().zip(rhs_excerpt_ids) {
2088 if last_id == Some(lhs_id) {
2089 current_group.push(rhs_id);
2090 } else {
2091 if !current_group.is_empty() {
2092 groups.push(current_group);
2093 }
2094 current_group = vec![rhs_id];
2095 last_id = Some(lhs_id);
2096 }
2097 }
2098 if !current_group.is_empty() {
2099 groups.push(current_group);
2100 }
2101 groups
2102 };
2103
2104 let deduplicated_lhs_ids: Vec<ExcerptId> =
2105 lhs_result.excerpt_ids.iter().dedup().copied().collect();
2106
2107 Some((deduplicated_lhs_ids, rhs_merge_groups))
2108 }
2109
2110 fn sync_path_excerpts(
2111 path_key: PathKey,
2112 old_rhs_excerpt_ids: Vec<ExcerptId>,
2113 rhs_multibuffer: &MultiBuffer,
2114 lhs_multibuffer: &mut MultiBuffer,
2115 diff: Entity<BufferDiff>,
2116 rhs_display_map: &Entity<DisplayMap>,
2117 lhs_cx: &mut Context<MultiBuffer>,
2118 ) -> Option<(Vec<ExcerptId>, Vec<Vec<ExcerptId>>)> {
2119 let old_lhs_excerpt_ids: Vec<ExcerptId> =
2120 lhs_multibuffer.excerpts_for_path(&path_key).collect();
2121
2122 if let Some(companion) = rhs_display_map.read(lhs_cx).companion().cloned() {
2123 companion.update(lhs_cx, |c, _| {
2124 c.remove_excerpt_mappings(old_lhs_excerpt_ids, old_rhs_excerpt_ids);
2125 });
2126 }
2127
2128 Self::update_path_excerpts_from_rhs(
2129 path_key,
2130 rhs_multibuffer,
2131 lhs_multibuffer,
2132 diff,
2133 lhs_cx,
2134 )
2135 }
2136}
2137
2138#[cfg(test)]
2139mod tests {
2140 use std::sync::Arc;
2141
2142 use buffer_diff::BufferDiff;
2143 use collections::{HashMap, HashSet};
2144 use fs::FakeFs;
2145 use gpui::Element as _;
2146 use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
2147 use language::language_settings::SoftWrap;
2148 use language::{Buffer, Capability};
2149 use multi_buffer::{MultiBuffer, PathKey};
2150 use pretty_assertions::assert_eq;
2151 use project::Project;
2152 use rand::rngs::StdRng;
2153 use settings::{DiffViewStyle, SettingsStore};
2154 use ui::{VisualContext as _, div, px};
2155 use util::rel_path::rel_path;
2156 use workspace::MultiWorkspace;
2157
2158 use crate::SplittableEditor;
2159 use crate::display_map::{
2160 BlockPlacement, BlockProperties, BlockStyle, Crease, FoldPlaceholder,
2161 };
2162 use crate::inlays::Inlay;
2163 use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
2164 use multi_buffer::MultiBufferOffset;
2165
2166 async fn init_test(
2167 cx: &mut gpui::TestAppContext,
2168 soft_wrap: SoftWrap,
2169 style: DiffViewStyle,
2170 ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
2171 cx.update(|cx| {
2172 let store = SettingsStore::test(cx);
2173 cx.set_global(store);
2174 theme::init(theme::LoadThemes::JustBase, cx);
2175 crate::init(cx);
2176 });
2177 let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
2178 let (multi_workspace, cx) =
2179 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2180 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2181 let rhs_multibuffer = cx.new(|cx| {
2182 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2183 multibuffer.set_all_diff_hunks_expanded(cx);
2184 multibuffer
2185 });
2186 let editor = cx.new_window_entity(|window, cx| {
2187 let editor = SplittableEditor::new(
2188 style,
2189 rhs_multibuffer.clone(),
2190 project.clone(),
2191 workspace,
2192 window,
2193 cx,
2194 );
2195 editor.rhs_editor.update(cx, |editor, cx| {
2196 editor.set_soft_wrap_mode(soft_wrap, cx);
2197 });
2198 if let Some(lhs) = &editor.lhs {
2199 lhs.editor.update(cx, |editor, cx| {
2200 editor.set_soft_wrap_mode(soft_wrap, cx);
2201 });
2202 }
2203 editor
2204 });
2205 (editor, cx)
2206 }
2207
2208 fn buffer_with_diff(
2209 base_text: &str,
2210 current_text: &str,
2211 cx: &mut VisualTestContext,
2212 ) -> (Entity<Buffer>, Entity<BufferDiff>) {
2213 let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2214 let diff = cx.new(|cx| {
2215 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
2216 });
2217 (buffer, diff)
2218 }
2219
2220 #[track_caller]
2221 fn assert_split_content(
2222 editor: &Entity<SplittableEditor>,
2223 expected_rhs: String,
2224 expected_lhs: String,
2225 cx: &mut VisualTestContext,
2226 ) {
2227 assert_split_content_with_widths(
2228 editor,
2229 px(3000.0),
2230 px(3000.0),
2231 expected_rhs,
2232 expected_lhs,
2233 cx,
2234 );
2235 }
2236
2237 #[track_caller]
2238 fn assert_split_content_with_widths(
2239 editor: &Entity<SplittableEditor>,
2240 rhs_width: Pixels,
2241 lhs_width: Pixels,
2242 expected_rhs: String,
2243 expected_lhs: String,
2244 cx: &mut VisualTestContext,
2245 ) {
2246 let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2247 let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2248 (editor.rhs_editor.clone(), lhs.editor.clone())
2249 });
2250
2251 // Make sure both sides learn if the other has soft-wrapped
2252 let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2253 cx.run_until_parked();
2254 let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2255 cx.run_until_parked();
2256
2257 let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2258 let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2259
2260 if rhs_content != expected_rhs || lhs_content != expected_lhs {
2261 editor.update(cx, |editor, cx| editor.debug_print(cx));
2262 }
2263
2264 assert_eq!(rhs_content, expected_rhs, "rhs");
2265 assert_eq!(lhs_content, expected_lhs, "lhs");
2266 }
2267
2268 #[gpui::test(iterations = 25)]
2269 async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2270 use rand::prelude::*;
2271
2272 let (editor, cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2273 let operations = std::env::var("OPERATIONS")
2274 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2275 .unwrap_or(10);
2276 let rng = &mut rng;
2277 for _ in 0..operations {
2278 let buffers = editor.update(cx, |editor, cx| {
2279 editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2280 });
2281
2282 if buffers.is_empty() {
2283 log::info!("adding excerpts to empty multibuffer");
2284 editor.update(cx, |editor, cx| {
2285 editor.randomly_edit_excerpts(rng, 2, cx);
2286 editor.check_invariants(true, cx);
2287 });
2288 continue;
2289 }
2290
2291 let mut quiesced = false;
2292
2293 match rng.random_range(0..100) {
2294 0..=44 => {
2295 log::info!("randomly editing multibuffer");
2296 editor.update(cx, |editor, cx| {
2297 editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2298 multibuffer.randomly_edit(rng, 5, cx);
2299 })
2300 })
2301 }
2302 45..=64 => {
2303 log::info!("randomly undoing/redoing in single buffer");
2304 let buffer = buffers.iter().choose(rng).unwrap();
2305 buffer.update(cx, |buffer, cx| {
2306 buffer.randomly_undo_redo(rng, cx);
2307 });
2308 }
2309 65..=79 => {
2310 log::info!("mutating excerpts");
2311 editor.update(cx, |editor, cx| {
2312 editor.randomly_edit_excerpts(rng, 2, cx);
2313 });
2314 }
2315 _ => {
2316 log::info!("quiescing");
2317 for buffer in buffers {
2318 let buffer_snapshot =
2319 buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2320 let diff = editor.update(cx, |editor, cx| {
2321 editor
2322 .rhs_multibuffer
2323 .read(cx)
2324 .diff_for(buffer.read(cx).remote_id())
2325 .unwrap()
2326 });
2327 diff.update(cx, |diff, cx| {
2328 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2329 });
2330 cx.run_until_parked();
2331 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2332 let ranges = diff_snapshot
2333 .hunks(&buffer_snapshot)
2334 .map(|hunk| hunk.range)
2335 .collect::<Vec<_>>();
2336 editor.update(cx, |editor, cx| {
2337 let path = PathKey::for_buffer(&buffer, cx);
2338 editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2339 });
2340 }
2341 quiesced = true;
2342 }
2343 }
2344
2345 editor.update(cx, |editor, cx| {
2346 editor.check_invariants(quiesced, cx);
2347 });
2348 }
2349 }
2350
2351 #[gpui::test]
2352 async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2353 use rope::Point;
2354 use unindent::Unindent as _;
2355
2356 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2357
2358 let base_text = "
2359 aaa
2360 bbb
2361 ccc
2362 ddd
2363 eee
2364 fff
2365 "
2366 .unindent();
2367 let current_text = "
2368 aaa
2369 ddd
2370 eee
2371 fff
2372 "
2373 .unindent();
2374
2375 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2376
2377 editor.update(cx, |editor, cx| {
2378 let path = PathKey::for_buffer(&buffer, cx);
2379 editor.set_excerpts_for_path(
2380 path,
2381 buffer.clone(),
2382 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2383 0,
2384 diff.clone(),
2385 cx,
2386 );
2387 });
2388
2389 cx.run_until_parked();
2390
2391 assert_split_content(
2392 &editor,
2393 "
2394 § <no file>
2395 § -----
2396 aaa
2397 § spacer
2398 § spacer
2399 ddd
2400 eee
2401 fff"
2402 .unindent(),
2403 "
2404 § <no file>
2405 § -----
2406 aaa
2407 bbb
2408 ccc
2409 ddd
2410 eee
2411 fff"
2412 .unindent(),
2413 &mut cx,
2414 );
2415
2416 buffer.update(cx, |buffer, cx| {
2417 buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2418 });
2419
2420 cx.run_until_parked();
2421
2422 assert_split_content(
2423 &editor,
2424 "
2425 § <no file>
2426 § -----
2427 aaa
2428 § spacer
2429 § spacer
2430 ddd
2431 eee
2432 FFF"
2433 .unindent(),
2434 "
2435 § <no file>
2436 § -----
2437 aaa
2438 bbb
2439 ccc
2440 ddd
2441 eee
2442 fff"
2443 .unindent(),
2444 &mut cx,
2445 );
2446
2447 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2448 diff.update(cx, |diff, cx| {
2449 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2450 });
2451
2452 cx.run_until_parked();
2453
2454 assert_split_content(
2455 &editor,
2456 "
2457 § <no file>
2458 § -----
2459 aaa
2460 § spacer
2461 § spacer
2462 ddd
2463 eee
2464 FFF"
2465 .unindent(),
2466 "
2467 § <no file>
2468 § -----
2469 aaa
2470 bbb
2471 ccc
2472 ddd
2473 eee
2474 fff"
2475 .unindent(),
2476 &mut cx,
2477 );
2478 }
2479
2480 #[gpui::test]
2481 async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2482 use rope::Point;
2483 use unindent::Unindent as _;
2484
2485 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2486
2487 let base_text1 = "
2488 aaa
2489 bbb
2490 ccc
2491 ddd
2492 eee"
2493 .unindent();
2494
2495 let base_text2 = "
2496 fff
2497 ggg
2498 hhh
2499 iii
2500 jjj"
2501 .unindent();
2502
2503 let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2504 let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2505
2506 editor.update(cx, |editor, cx| {
2507 let path1 = PathKey::for_buffer(&buffer1, cx);
2508 editor.set_excerpts_for_path(
2509 path1,
2510 buffer1.clone(),
2511 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2512 0,
2513 diff1.clone(),
2514 cx,
2515 );
2516 let path2 = PathKey::for_buffer(&buffer2, cx);
2517 editor.set_excerpts_for_path(
2518 path2,
2519 buffer2.clone(),
2520 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2521 1,
2522 diff2.clone(),
2523 cx,
2524 );
2525 });
2526
2527 cx.run_until_parked();
2528
2529 buffer1.update(cx, |buffer, cx| {
2530 buffer.edit(
2531 [
2532 (Point::new(0, 0)..Point::new(1, 0), ""),
2533 (Point::new(3, 0)..Point::new(4, 0), ""),
2534 ],
2535 None,
2536 cx,
2537 );
2538 });
2539 buffer2.update(cx, |buffer, cx| {
2540 buffer.edit(
2541 [
2542 (Point::new(0, 0)..Point::new(1, 0), ""),
2543 (Point::new(3, 0)..Point::new(4, 0), ""),
2544 ],
2545 None,
2546 cx,
2547 );
2548 });
2549
2550 cx.run_until_parked();
2551
2552 assert_split_content(
2553 &editor,
2554 "
2555 § <no file>
2556 § -----
2557 § spacer
2558 bbb
2559 ccc
2560 § spacer
2561 eee
2562 § <no file>
2563 § -----
2564 § spacer
2565 ggg
2566 hhh
2567 § spacer
2568 jjj"
2569 .unindent(),
2570 "
2571 § <no file>
2572 § -----
2573 aaa
2574 bbb
2575 ccc
2576 ddd
2577 eee
2578 § <no file>
2579 § -----
2580 fff
2581 ggg
2582 hhh
2583 iii
2584 jjj"
2585 .unindent(),
2586 &mut cx,
2587 );
2588
2589 let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2590 diff1.update(cx, |diff, cx| {
2591 diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2592 });
2593 let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2594 diff2.update(cx, |diff, cx| {
2595 diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2596 });
2597
2598 cx.run_until_parked();
2599
2600 assert_split_content(
2601 &editor,
2602 "
2603 § <no file>
2604 § -----
2605 § spacer
2606 bbb
2607 ccc
2608 § spacer
2609 eee
2610 § <no file>
2611 § -----
2612 § spacer
2613 ggg
2614 hhh
2615 § spacer
2616 jjj"
2617 .unindent(),
2618 "
2619 § <no file>
2620 § -----
2621 aaa
2622 bbb
2623 ccc
2624 ddd
2625 eee
2626 § <no file>
2627 § -----
2628 fff
2629 ggg
2630 hhh
2631 iii
2632 jjj"
2633 .unindent(),
2634 &mut cx,
2635 );
2636 }
2637
2638 #[gpui::test]
2639 async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2640 use rope::Point;
2641 use unindent::Unindent as _;
2642
2643 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2644
2645 let base_text = "
2646 aaa
2647 bbb
2648 ccc
2649 ddd
2650 "
2651 .unindent();
2652
2653 let current_text = "
2654 aaa
2655 NEW1
2656 NEW2
2657 ccc
2658 ddd
2659 "
2660 .unindent();
2661
2662 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2663
2664 editor.update(cx, |editor, cx| {
2665 let path = PathKey::for_buffer(&buffer, cx);
2666 editor.set_excerpts_for_path(
2667 path,
2668 buffer.clone(),
2669 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2670 0,
2671 diff.clone(),
2672 cx,
2673 );
2674 });
2675
2676 cx.run_until_parked();
2677
2678 assert_split_content(
2679 &editor,
2680 "
2681 § <no file>
2682 § -----
2683 aaa
2684 NEW1
2685 NEW2
2686 ccc
2687 ddd"
2688 .unindent(),
2689 "
2690 § <no file>
2691 § -----
2692 aaa
2693 bbb
2694 § spacer
2695 ccc
2696 ddd"
2697 .unindent(),
2698 &mut cx,
2699 );
2700
2701 buffer.update(cx, |buffer, cx| {
2702 buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2703 });
2704
2705 cx.run_until_parked();
2706
2707 assert_split_content(
2708 &editor,
2709 "
2710 § <no file>
2711 § -----
2712 aaa
2713 NEW1
2714 ccc
2715 ddd"
2716 .unindent(),
2717 "
2718 § <no file>
2719 § -----
2720 aaa
2721 bbb
2722 ccc
2723 ddd"
2724 .unindent(),
2725 &mut cx,
2726 );
2727
2728 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2729 diff.update(cx, |diff, cx| {
2730 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2731 });
2732
2733 cx.run_until_parked();
2734
2735 assert_split_content(
2736 &editor,
2737 "
2738 § <no file>
2739 § -----
2740 aaa
2741 NEW1
2742 ccc
2743 ddd"
2744 .unindent(),
2745 "
2746 § <no file>
2747 § -----
2748 aaa
2749 bbb
2750 ccc
2751 ddd"
2752 .unindent(),
2753 &mut cx,
2754 );
2755 }
2756
2757 #[gpui::test]
2758 async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2759 use rope::Point;
2760 use unindent::Unindent as _;
2761
2762 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2763
2764 let base_text = "
2765 aaa
2766 bbb
2767
2768
2769
2770
2771
2772 ccc
2773 ddd
2774 "
2775 .unindent();
2776 let current_text = "
2777 aaa
2778 bbb
2779
2780
2781
2782
2783
2784 CCC
2785 ddd
2786 "
2787 .unindent();
2788
2789 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2790
2791 editor.update(cx, |editor, cx| {
2792 let path = PathKey::for_buffer(&buffer, cx);
2793 editor.set_excerpts_for_path(
2794 path,
2795 buffer.clone(),
2796 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2797 0,
2798 diff.clone(),
2799 cx,
2800 );
2801 });
2802
2803 cx.run_until_parked();
2804
2805 buffer.update(cx, |buffer, cx| {
2806 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2807 });
2808
2809 cx.run_until_parked();
2810
2811 assert_split_content(
2812 &editor,
2813 "
2814 § <no file>
2815 § -----
2816 aaa
2817 bbb
2818
2819
2820
2821
2822
2823
2824 CCC
2825 ddd"
2826 .unindent(),
2827 "
2828 § <no file>
2829 § -----
2830 aaa
2831 bbb
2832 § spacer
2833
2834
2835
2836
2837
2838 ccc
2839 ddd"
2840 .unindent(),
2841 &mut cx,
2842 );
2843
2844 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2845 diff.update(cx, |diff, cx| {
2846 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2847 });
2848
2849 cx.run_until_parked();
2850
2851 assert_split_content(
2852 &editor,
2853 "
2854 § <no file>
2855 § -----
2856 aaa
2857 bbb
2858
2859
2860
2861
2862
2863
2864 CCC
2865 ddd"
2866 .unindent(),
2867 "
2868 § <no file>
2869 § -----
2870 aaa
2871 bbb
2872
2873
2874
2875
2876
2877 ccc
2878 § spacer
2879 ddd"
2880 .unindent(),
2881 &mut cx,
2882 );
2883 }
2884
2885 #[gpui::test]
2886 async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2887 use git::Restore;
2888 use rope::Point;
2889 use unindent::Unindent as _;
2890
2891 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
2892
2893 let base_text = "
2894 aaa
2895 bbb
2896 ccc
2897 ddd
2898 eee
2899 "
2900 .unindent();
2901 let current_text = "
2902 aaa
2903 ddd
2904 eee
2905 "
2906 .unindent();
2907
2908 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2909
2910 editor.update(cx, |editor, cx| {
2911 let path = PathKey::for_buffer(&buffer, cx);
2912 editor.set_excerpts_for_path(
2913 path,
2914 buffer.clone(),
2915 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2916 0,
2917 diff.clone(),
2918 cx,
2919 );
2920 });
2921
2922 cx.run_until_parked();
2923
2924 assert_split_content(
2925 &editor,
2926 "
2927 § <no file>
2928 § -----
2929 aaa
2930 § spacer
2931 § spacer
2932 ddd
2933 eee"
2934 .unindent(),
2935 "
2936 § <no file>
2937 § -----
2938 aaa
2939 bbb
2940 ccc
2941 ddd
2942 eee"
2943 .unindent(),
2944 &mut cx,
2945 );
2946
2947 let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
2948 cx.update_window_entity(&rhs_editor, |editor, window, cx| {
2949 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2950 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2951 });
2952 editor.git_restore(&Restore, window, cx);
2953 });
2954
2955 cx.run_until_parked();
2956
2957 assert_split_content(
2958 &editor,
2959 "
2960 § <no file>
2961 § -----
2962 aaa
2963 bbb
2964 ccc
2965 ddd
2966 eee"
2967 .unindent(),
2968 "
2969 § <no file>
2970 § -----
2971 aaa
2972 bbb
2973 ccc
2974 ddd
2975 eee"
2976 .unindent(),
2977 &mut cx,
2978 );
2979
2980 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2981 diff.update(cx, |diff, cx| {
2982 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2983 });
2984
2985 cx.run_until_parked();
2986
2987 assert_split_content(
2988 &editor,
2989 "
2990 § <no file>
2991 § -----
2992 aaa
2993 bbb
2994 ccc
2995 ddd
2996 eee"
2997 .unindent(),
2998 "
2999 § <no file>
3000 § -----
3001 aaa
3002 bbb
3003 ccc
3004 ddd
3005 eee"
3006 .unindent(),
3007 &mut cx,
3008 );
3009 }
3010
3011 #[gpui::test]
3012 async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
3013 use rope::Point;
3014 use unindent::Unindent as _;
3015
3016 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3017
3018 let base_text = "
3019 aaa
3020 old1
3021 old2
3022 old3
3023 old4
3024 zzz
3025 "
3026 .unindent();
3027
3028 let current_text = "
3029 aaa
3030 new1
3031 new2
3032 new3
3033 new4
3034 zzz
3035 "
3036 .unindent();
3037
3038 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3039
3040 editor.update(cx, |editor, cx| {
3041 let path = PathKey::for_buffer(&buffer, cx);
3042 editor.set_excerpts_for_path(
3043 path,
3044 buffer.clone(),
3045 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3046 0,
3047 diff.clone(),
3048 cx,
3049 );
3050 });
3051
3052 cx.run_until_parked();
3053
3054 buffer.update(cx, |buffer, cx| {
3055 buffer.edit(
3056 [
3057 (Point::new(2, 0)..Point::new(3, 0), ""),
3058 (Point::new(4, 0)..Point::new(5, 0), ""),
3059 ],
3060 None,
3061 cx,
3062 );
3063 });
3064 cx.run_until_parked();
3065
3066 assert_split_content(
3067 &editor,
3068 "
3069 § <no file>
3070 § -----
3071 aaa
3072 new1
3073 new3
3074 § spacer
3075 § spacer
3076 zzz"
3077 .unindent(),
3078 "
3079 § <no file>
3080 § -----
3081 aaa
3082 old1
3083 old2
3084 old3
3085 old4
3086 zzz"
3087 .unindent(),
3088 &mut cx,
3089 );
3090
3091 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3092 diff.update(cx, |diff, cx| {
3093 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3094 });
3095
3096 cx.run_until_parked();
3097
3098 assert_split_content(
3099 &editor,
3100 "
3101 § <no file>
3102 § -----
3103 aaa
3104 new1
3105 new3
3106 § spacer
3107 § spacer
3108 zzz"
3109 .unindent(),
3110 "
3111 § <no file>
3112 § -----
3113 aaa
3114 old1
3115 old2
3116 old3
3117 old4
3118 zzz"
3119 .unindent(),
3120 &mut cx,
3121 );
3122 }
3123
3124 #[gpui::test]
3125 async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
3126 use rope::Point;
3127 use unindent::Unindent as _;
3128
3129 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3130
3131 let text = "aaaa bbbb cccc dddd eeee ffff";
3132
3133 let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
3134 let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
3135
3136 editor.update(cx, |editor, cx| {
3137 let end = Point::new(0, text.len() as u32);
3138 let path1 = PathKey::for_buffer(&buffer1, cx);
3139 editor.set_excerpts_for_path(
3140 path1,
3141 buffer1.clone(),
3142 vec![Point::new(0, 0)..end],
3143 0,
3144 diff1.clone(),
3145 cx,
3146 );
3147 let path2 = PathKey::for_buffer(&buffer2, cx);
3148 editor.set_excerpts_for_path(
3149 path2,
3150 buffer2.clone(),
3151 vec![Point::new(0, 0)..end],
3152 0,
3153 diff2.clone(),
3154 cx,
3155 );
3156 });
3157
3158 cx.run_until_parked();
3159
3160 assert_split_content_with_widths(
3161 &editor,
3162 px(200.0),
3163 px(400.0),
3164 "
3165 § <no file>
3166 § -----
3167 aaaa bbbb\x20
3168 cccc dddd\x20
3169 eeee ffff
3170 § <no file>
3171 § -----
3172 aaaa bbbb\x20
3173 cccc dddd\x20
3174 eeee ffff"
3175 .unindent(),
3176 "
3177 § <no file>
3178 § -----
3179 aaaa bbbb cccc dddd eeee ffff
3180 § spacer
3181 § spacer
3182 § <no file>
3183 § -----
3184 aaaa bbbb cccc dddd eeee ffff
3185 § spacer
3186 § spacer"
3187 .unindent(),
3188 &mut cx,
3189 );
3190 }
3191
3192 #[gpui::test]
3193 async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
3194 use rope::Point;
3195 use unindent::Unindent as _;
3196
3197 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3198
3199 let base_text = "
3200 aaaa bbbb cccc dddd eeee ffff
3201 old line one
3202 old line two
3203 "
3204 .unindent();
3205
3206 let current_text = "
3207 aaaa bbbb cccc dddd eeee ffff
3208 new line
3209 "
3210 .unindent();
3211
3212 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3213
3214 editor.update(cx, |editor, cx| {
3215 let path = PathKey::for_buffer(&buffer, cx);
3216 editor.set_excerpts_for_path(
3217 path,
3218 buffer.clone(),
3219 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3220 0,
3221 diff.clone(),
3222 cx,
3223 );
3224 });
3225
3226 cx.run_until_parked();
3227
3228 assert_split_content_with_widths(
3229 &editor,
3230 px(200.0),
3231 px(400.0),
3232 "
3233 § <no file>
3234 § -----
3235 aaaa bbbb\x20
3236 cccc dddd\x20
3237 eeee ffff
3238 new line
3239 § spacer"
3240 .unindent(),
3241 "
3242 § <no file>
3243 § -----
3244 aaaa bbbb cccc dddd eeee ffff
3245 § spacer
3246 § spacer
3247 old line one
3248 old line two"
3249 .unindent(),
3250 &mut cx,
3251 );
3252 }
3253
3254 #[gpui::test]
3255 async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3256 use rope::Point;
3257 use unindent::Unindent as _;
3258
3259 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3260
3261 let base_text = "
3262 aaaa bbbb cccc dddd eeee ffff
3263 deleted line one
3264 deleted line two
3265 after
3266 "
3267 .unindent();
3268
3269 let current_text = "
3270 aaaa bbbb cccc dddd eeee ffff
3271 after
3272 "
3273 .unindent();
3274
3275 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3276
3277 editor.update(cx, |editor, cx| {
3278 let path = PathKey::for_buffer(&buffer, cx);
3279 editor.set_excerpts_for_path(
3280 path,
3281 buffer.clone(),
3282 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3283 0,
3284 diff.clone(),
3285 cx,
3286 );
3287 });
3288
3289 cx.run_until_parked();
3290
3291 assert_split_content_with_widths(
3292 &editor,
3293 px(400.0),
3294 px(200.0),
3295 "
3296 § <no file>
3297 § -----
3298 aaaa bbbb cccc dddd eeee ffff
3299 § spacer
3300 § spacer
3301 § spacer
3302 § spacer
3303 § spacer
3304 § spacer
3305 after"
3306 .unindent(),
3307 "
3308 § <no file>
3309 § -----
3310 aaaa bbbb\x20
3311 cccc dddd\x20
3312 eeee ffff
3313 deleted line\x20
3314 one
3315 deleted line\x20
3316 two
3317 after"
3318 .unindent(),
3319 &mut cx,
3320 );
3321 }
3322
3323 #[gpui::test]
3324 async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3325 use rope::Point;
3326 use unindent::Unindent as _;
3327
3328 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3329
3330 let text = "
3331 aaaa bbbb cccc dddd eeee ffff
3332 short
3333 "
3334 .unindent();
3335
3336 let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3337
3338 editor.update(cx, |editor, cx| {
3339 let path = PathKey::for_buffer(&buffer, cx);
3340 editor.set_excerpts_for_path(
3341 path,
3342 buffer.clone(),
3343 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3344 0,
3345 diff.clone(),
3346 cx,
3347 );
3348 });
3349
3350 cx.run_until_parked();
3351
3352 assert_split_content_with_widths(
3353 &editor,
3354 px(400.0),
3355 px(200.0),
3356 "
3357 § <no file>
3358 § -----
3359 aaaa bbbb cccc dddd eeee ffff
3360 § spacer
3361 § spacer
3362 short"
3363 .unindent(),
3364 "
3365 § <no file>
3366 § -----
3367 aaaa bbbb\x20
3368 cccc dddd\x20
3369 eeee ffff
3370 short"
3371 .unindent(),
3372 &mut cx,
3373 );
3374
3375 buffer.update(cx, |buffer, cx| {
3376 buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3377 });
3378
3379 cx.run_until_parked();
3380
3381 assert_split_content_with_widths(
3382 &editor,
3383 px(400.0),
3384 px(200.0),
3385 "
3386 § <no file>
3387 § -----
3388 aaaa bbbb cccc dddd eeee ffff
3389 § spacer
3390 § spacer
3391 modified"
3392 .unindent(),
3393 "
3394 § <no file>
3395 § -----
3396 aaaa bbbb\x20
3397 cccc dddd\x20
3398 eeee ffff
3399 short"
3400 .unindent(),
3401 &mut cx,
3402 );
3403
3404 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3405 diff.update(cx, |diff, cx| {
3406 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3407 });
3408
3409 cx.run_until_parked();
3410
3411 assert_split_content_with_widths(
3412 &editor,
3413 px(400.0),
3414 px(200.0),
3415 "
3416 § <no file>
3417 § -----
3418 aaaa bbbb cccc dddd eeee ffff
3419 § spacer
3420 § spacer
3421 modified"
3422 .unindent(),
3423 "
3424 § <no file>
3425 § -----
3426 aaaa bbbb\x20
3427 cccc dddd\x20
3428 eeee ffff
3429 short"
3430 .unindent(),
3431 &mut cx,
3432 );
3433 }
3434
3435 #[gpui::test]
3436 async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3437 use rope::Point;
3438 use unindent::Unindent as _;
3439
3440 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3441
3442 let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3443
3444 let current_text = "
3445 aaa
3446 bbb
3447 ccc
3448 "
3449 .unindent();
3450
3451 let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3452 let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3453
3454 editor.update(cx, |editor, cx| {
3455 let path1 = PathKey::for_buffer(&buffer1, cx);
3456 editor.set_excerpts_for_path(
3457 path1,
3458 buffer1.clone(),
3459 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3460 0,
3461 diff1.clone(),
3462 cx,
3463 );
3464
3465 let path2 = PathKey::for_buffer(&buffer2, cx);
3466 editor.set_excerpts_for_path(
3467 path2,
3468 buffer2.clone(),
3469 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3470 1,
3471 diff2.clone(),
3472 cx,
3473 );
3474 });
3475
3476 cx.run_until_parked();
3477
3478 assert_split_content(
3479 &editor,
3480 "
3481 § <no file>
3482 § -----
3483 xxx
3484 yyy
3485 § <no file>
3486 § -----
3487 aaa
3488 bbb
3489 ccc"
3490 .unindent(),
3491 "
3492 § <no file>
3493 § -----
3494 xxx
3495 yyy
3496 § <no file>
3497 § -----
3498 § spacer
3499 § spacer
3500 § spacer"
3501 .unindent(),
3502 &mut cx,
3503 );
3504
3505 buffer1.update(cx, |buffer, cx| {
3506 buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3507 });
3508
3509 cx.run_until_parked();
3510
3511 assert_split_content(
3512 &editor,
3513 "
3514 § <no file>
3515 § -----
3516 xxxz
3517 yyy
3518 § <no file>
3519 § -----
3520 aaa
3521 bbb
3522 ccc"
3523 .unindent(),
3524 "
3525 § <no file>
3526 § -----
3527 xxx
3528 yyy
3529 § <no file>
3530 § -----
3531 § spacer
3532 § spacer
3533 § spacer"
3534 .unindent(),
3535 &mut cx,
3536 );
3537 }
3538
3539 #[gpui::test]
3540 async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3541 use rope::Point;
3542 use unindent::Unindent as _;
3543
3544 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3545
3546 let base_text = "
3547 aaa
3548 bbb
3549 ccc
3550 "
3551 .unindent();
3552
3553 let current_text = "
3554 NEW1
3555 NEW2
3556 ccc
3557 "
3558 .unindent();
3559
3560 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3561
3562 editor.update(cx, |editor, cx| {
3563 let path = PathKey::for_buffer(&buffer, cx);
3564 editor.set_excerpts_for_path(
3565 path,
3566 buffer.clone(),
3567 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3568 0,
3569 diff.clone(),
3570 cx,
3571 );
3572 });
3573
3574 cx.run_until_parked();
3575
3576 assert_split_content(
3577 &editor,
3578 "
3579 § <no file>
3580 § -----
3581 NEW1
3582 NEW2
3583 ccc"
3584 .unindent(),
3585 "
3586 § <no file>
3587 § -----
3588 aaa
3589 bbb
3590 ccc"
3591 .unindent(),
3592 &mut cx,
3593 );
3594
3595 buffer.update(cx, |buffer, cx| {
3596 buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3597 });
3598
3599 cx.run_until_parked();
3600
3601 assert_split_content(
3602 &editor,
3603 "
3604 § <no file>
3605 § -----
3606 NEW1
3607 NEW
3608 ccc"
3609 .unindent(),
3610 "
3611 § <no file>
3612 § -----
3613 aaa
3614 bbb
3615 ccc"
3616 .unindent(),
3617 &mut cx,
3618 );
3619 }
3620
3621 #[gpui::test]
3622 async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3623 use rope::Point;
3624 use unindent::Unindent as _;
3625
3626 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3627
3628 let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3629
3630 let current_text = "
3631 aaaa bbbb cccc dddd eeee ffff
3632 added line
3633 "
3634 .unindent();
3635
3636 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3637
3638 editor.update(cx, |editor, cx| {
3639 let path = PathKey::for_buffer(&buffer, cx);
3640 editor.set_excerpts_for_path(
3641 path,
3642 buffer.clone(),
3643 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3644 0,
3645 diff.clone(),
3646 cx,
3647 );
3648 });
3649
3650 cx.run_until_parked();
3651
3652 assert_split_content_with_widths(
3653 &editor,
3654 px(400.0),
3655 px(200.0),
3656 "
3657 § <no file>
3658 § -----
3659 aaaa bbbb cccc dddd eeee ffff
3660 § spacer
3661 § spacer
3662 added line"
3663 .unindent(),
3664 "
3665 § <no file>
3666 § -----
3667 aaaa bbbb\x20
3668 cccc dddd\x20
3669 eeee ffff
3670 § spacer"
3671 .unindent(),
3672 &mut cx,
3673 );
3674
3675 assert_split_content_with_widths(
3676 &editor,
3677 px(200.0),
3678 px(400.0),
3679 "
3680 § <no file>
3681 § -----
3682 aaaa bbbb\x20
3683 cccc dddd\x20
3684 eeee ffff
3685 added line"
3686 .unindent(),
3687 "
3688 § <no file>
3689 § -----
3690 aaaa bbbb cccc dddd eeee ffff
3691 § spacer
3692 § spacer
3693 § spacer"
3694 .unindent(),
3695 &mut cx,
3696 );
3697 }
3698
3699 #[gpui::test]
3700 #[ignore]
3701 async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3702 use rope::Point;
3703 use unindent::Unindent as _;
3704
3705 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3706
3707 let base_text = "
3708 aaa
3709 bbb
3710 ccc
3711 ddd
3712 eee
3713 "
3714 .unindent();
3715
3716 let current_text = "
3717 aaa
3718 NEW
3719 eee
3720 "
3721 .unindent();
3722
3723 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3724
3725 editor.update(cx, |editor, cx| {
3726 let path = PathKey::for_buffer(&buffer, cx);
3727 editor.set_excerpts_for_path(
3728 path,
3729 buffer.clone(),
3730 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3731 0,
3732 diff.clone(),
3733 cx,
3734 );
3735 });
3736
3737 cx.run_until_parked();
3738
3739 assert_split_content(
3740 &editor,
3741 "
3742 § <no file>
3743 § -----
3744 aaa
3745 NEW
3746 § spacer
3747 § spacer
3748 eee"
3749 .unindent(),
3750 "
3751 § <no file>
3752 § -----
3753 aaa
3754 bbb
3755 ccc
3756 ddd
3757 eee"
3758 .unindent(),
3759 &mut cx,
3760 );
3761
3762 buffer.update(cx, |buffer, cx| {
3763 buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3764 });
3765
3766 cx.run_until_parked();
3767
3768 assert_split_content(
3769 &editor,
3770 "
3771 § <no file>
3772 § -----
3773 aaa
3774 § spacer
3775 § spacer
3776 § spacer
3777 NEWeee"
3778 .unindent(),
3779 "
3780 § <no file>
3781 § -----
3782 aaa
3783 bbb
3784 ccc
3785 ddd
3786 eee"
3787 .unindent(),
3788 &mut cx,
3789 );
3790
3791 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3792 diff.update(cx, |diff, cx| {
3793 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3794 });
3795
3796 cx.run_until_parked();
3797
3798 assert_split_content(
3799 &editor,
3800 "
3801 § <no file>
3802 § -----
3803 aaa
3804 NEWeee
3805 § spacer
3806 § spacer
3807 § spacer"
3808 .unindent(),
3809 "
3810 § <no file>
3811 § -----
3812 aaa
3813 bbb
3814 ccc
3815 ddd
3816 eee"
3817 .unindent(),
3818 &mut cx,
3819 );
3820 }
3821
3822 #[gpui::test]
3823 async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3824 use rope::Point;
3825 use unindent::Unindent as _;
3826
3827 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3828
3829 let base_text = "";
3830 let current_text = "
3831 aaaa bbbb cccc dddd eeee ffff
3832 bbb
3833 ccc
3834 "
3835 .unindent();
3836
3837 let (buffer, diff) = buffer_with_diff(base_text, ¤t_text, &mut cx);
3838
3839 editor.update(cx, |editor, cx| {
3840 let path = PathKey::for_buffer(&buffer, cx);
3841 editor.set_excerpts_for_path(
3842 path,
3843 buffer.clone(),
3844 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3845 0,
3846 diff.clone(),
3847 cx,
3848 );
3849 });
3850
3851 cx.run_until_parked();
3852
3853 assert_split_content(
3854 &editor,
3855 "
3856 § <no file>
3857 § -----
3858 aaaa bbbb cccc dddd eeee ffff
3859 bbb
3860 ccc"
3861 .unindent(),
3862 "
3863 § <no file>
3864 § -----
3865 § spacer
3866 § spacer
3867 § spacer"
3868 .unindent(),
3869 &mut cx,
3870 );
3871
3872 assert_split_content_with_widths(
3873 &editor,
3874 px(200.0),
3875 px(200.0),
3876 "
3877 § <no file>
3878 § -----
3879 aaaa bbbb\x20
3880 cccc dddd\x20
3881 eeee ffff
3882 bbb
3883 ccc"
3884 .unindent(),
3885 "
3886 § <no file>
3887 § -----
3888 § spacer
3889 § spacer
3890 § spacer
3891 § spacer
3892 § spacer"
3893 .unindent(),
3894 &mut cx,
3895 );
3896 }
3897
3898 #[gpui::test]
3899 async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
3900 use rope::Point;
3901 use unindent::Unindent as _;
3902
3903 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
3904
3905 let base_text = "
3906 aaa
3907 bbb
3908 ccc
3909 "
3910 .unindent();
3911
3912 let current_text = "
3913 aaa
3914 bbb
3915 xxx
3916 yyy
3917 ccc
3918 "
3919 .unindent();
3920
3921 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3922
3923 editor.update(cx, |editor, cx| {
3924 let path = PathKey::for_buffer(&buffer, cx);
3925 editor.set_excerpts_for_path(
3926 path,
3927 buffer.clone(),
3928 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3929 0,
3930 diff.clone(),
3931 cx,
3932 );
3933 });
3934
3935 cx.run_until_parked();
3936
3937 assert_split_content(
3938 &editor,
3939 "
3940 § <no file>
3941 § -----
3942 aaa
3943 bbb
3944 xxx
3945 yyy
3946 ccc"
3947 .unindent(),
3948 "
3949 § <no file>
3950 § -----
3951 aaa
3952 bbb
3953 § spacer
3954 § spacer
3955 ccc"
3956 .unindent(),
3957 &mut cx,
3958 );
3959
3960 buffer.update(cx, |buffer, cx| {
3961 buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
3962 });
3963
3964 cx.run_until_parked();
3965
3966 assert_split_content(
3967 &editor,
3968 "
3969 § <no file>
3970 § -----
3971 aaa
3972 bbb
3973 xxx
3974 yyy
3975 zzz
3976 ccc"
3977 .unindent(),
3978 "
3979 § <no file>
3980 § -----
3981 aaa
3982 bbb
3983 § spacer
3984 § spacer
3985 § spacer
3986 ccc"
3987 .unindent(),
3988 &mut cx,
3989 );
3990 }
3991
3992 #[gpui::test]
3993 async fn test_scrolling(cx: &mut gpui::TestAppContext) {
3994 use crate::test::editor_content_with_blocks_and_size;
3995 use gpui::size;
3996 use rope::Point;
3997
3998 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
3999
4000 let long_line = "x".repeat(200);
4001 let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
4002 lines[25] = long_line;
4003 let content = lines.join("\n");
4004
4005 let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
4006
4007 editor.update(cx, |editor, cx| {
4008 let path = PathKey::for_buffer(&buffer, cx);
4009 editor.set_excerpts_for_path(
4010 path,
4011 buffer.clone(),
4012 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4013 0,
4014 diff.clone(),
4015 cx,
4016 );
4017 });
4018
4019 cx.run_until_parked();
4020
4021 let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
4022 let lhs = editor.lhs.as_ref().expect("should have lhs editor");
4023 (editor.rhs_editor.clone(), lhs.editor.clone())
4024 });
4025
4026 rhs_editor.update_in(cx, |e, window, cx| {
4027 e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
4028 });
4029
4030 let rhs_pos =
4031 rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4032 let lhs_pos =
4033 lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4034 assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
4035 assert_eq!(
4036 lhs_pos.y, rhs_pos.y,
4037 "LHS should have same scroll position as RHS after set_scroll_position"
4038 );
4039
4040 let draw_size = size(px(300.), px(300.));
4041
4042 rhs_editor.update_in(cx, |e, window, cx| {
4043 e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
4044 s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
4045 });
4046 });
4047
4048 let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
4049 cx.run_until_parked();
4050 let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
4051 cx.run_until_parked();
4052
4053 let rhs_pos =
4054 rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4055 let lhs_pos =
4056 lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
4057
4058 assert!(
4059 rhs_pos.y > 0.,
4060 "RHS should have scrolled vertically to show cursor at row 25"
4061 );
4062 assert!(
4063 rhs_pos.x > 0.,
4064 "RHS should have scrolled horizontally to show cursor at column 150"
4065 );
4066 assert_eq!(
4067 lhs_pos.y, rhs_pos.y,
4068 "LHS should have same vertical scroll position as RHS after autoscroll"
4069 );
4070 assert_eq!(
4071 lhs_pos.x, rhs_pos.x,
4072 "LHS should have same horizontal scroll position as RHS after autoscroll"
4073 )
4074 }
4075
4076 #[gpui::test]
4077 async fn test_edit_line_before_soft_wrapped_line_preceding_hunk(cx: &mut gpui::TestAppContext) {
4078 use rope::Point;
4079 use unindent::Unindent as _;
4080
4081 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth, DiffViewStyle::Split).await;
4082
4083 let base_text = "
4084 first line
4085 aaaa bbbb cccc dddd eeee ffff
4086 original
4087 "
4088 .unindent();
4089
4090 let current_text = "
4091 first line
4092 aaaa bbbb cccc dddd eeee ffff
4093 modified
4094 "
4095 .unindent();
4096
4097 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4098
4099 editor.update(cx, |editor, cx| {
4100 let path = PathKey::for_buffer(&buffer, cx);
4101 editor.set_excerpts_for_path(
4102 path,
4103 buffer.clone(),
4104 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4105 0,
4106 diff.clone(),
4107 cx,
4108 );
4109 });
4110
4111 cx.run_until_parked();
4112
4113 assert_split_content_with_widths(
4114 &editor,
4115 px(400.0),
4116 px(200.0),
4117 "
4118 § <no file>
4119 § -----
4120 first line
4121 aaaa bbbb cccc dddd eeee ffff
4122 § spacer
4123 § spacer
4124 modified"
4125 .unindent(),
4126 "
4127 § <no file>
4128 § -----
4129 first line
4130 aaaa bbbb\x20
4131 cccc dddd\x20
4132 eeee ffff
4133 original"
4134 .unindent(),
4135 &mut cx,
4136 );
4137
4138 buffer.update(cx, |buffer, cx| {
4139 buffer.edit(
4140 [(Point::new(0, 0)..Point::new(0, 10), "edited first")],
4141 None,
4142 cx,
4143 );
4144 });
4145
4146 cx.run_until_parked();
4147
4148 assert_split_content_with_widths(
4149 &editor,
4150 px(400.0),
4151 px(200.0),
4152 "
4153 § <no file>
4154 § -----
4155 edited first
4156 aaaa bbbb cccc dddd eeee ffff
4157 § spacer
4158 § spacer
4159 modified"
4160 .unindent(),
4161 "
4162 § <no file>
4163 § -----
4164 first line
4165 aaaa bbbb\x20
4166 cccc dddd\x20
4167 eeee ffff
4168 original"
4169 .unindent(),
4170 &mut cx,
4171 );
4172
4173 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
4174 diff.update(cx, |diff, cx| {
4175 diff.recalculate_diff_sync(&buffer_snapshot, cx);
4176 });
4177
4178 cx.run_until_parked();
4179
4180 assert_split_content_with_widths(
4181 &editor,
4182 px(400.0),
4183 px(200.0),
4184 "
4185 § <no file>
4186 § -----
4187 edited first
4188 aaaa bbbb cccc dddd eeee ffff
4189 § spacer
4190 § spacer
4191 modified"
4192 .unindent(),
4193 "
4194 § <no file>
4195 § -----
4196 first line
4197 aaaa bbbb\x20
4198 cccc dddd\x20
4199 eeee ffff
4200 original"
4201 .unindent(),
4202 &mut cx,
4203 );
4204 }
4205
4206 #[gpui::test]
4207 async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
4208 use rope::Point;
4209 use unindent::Unindent as _;
4210
4211 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4212
4213 let base_text = "
4214 bbb
4215 ccc
4216 "
4217 .unindent();
4218 let current_text = "
4219 aaa
4220 bbb
4221 ccc
4222 "
4223 .unindent();
4224
4225 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4226
4227 editor.update(cx, |editor, cx| {
4228 let path = PathKey::for_buffer(&buffer, cx);
4229 editor.set_excerpts_for_path(
4230 path,
4231 buffer.clone(),
4232 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4233 0,
4234 diff.clone(),
4235 cx,
4236 );
4237 });
4238
4239 cx.run_until_parked();
4240
4241 assert_split_content(
4242 &editor,
4243 "
4244 § <no file>
4245 § -----
4246 aaa
4247 bbb
4248 ccc"
4249 .unindent(),
4250 "
4251 § <no file>
4252 § -----
4253 § spacer
4254 bbb
4255 ccc"
4256 .unindent(),
4257 &mut cx,
4258 );
4259
4260 let block_ids = editor.update(cx, |splittable_editor, cx| {
4261 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4262 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4263 let anchor = snapshot.anchor_before(Point::new(2, 0));
4264 rhs_editor.insert_blocks(
4265 [BlockProperties {
4266 placement: BlockPlacement::Above(anchor),
4267 height: Some(1),
4268 style: BlockStyle::Fixed,
4269 render: Arc::new(|_| div().into_any()),
4270 priority: 0,
4271 }],
4272 None,
4273 cx,
4274 )
4275 })
4276 });
4277
4278 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4279 let lhs_editor =
4280 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4281
4282 cx.update(|_, cx| {
4283 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4284 "custom block".to_string()
4285 });
4286 });
4287
4288 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
4289 let display_map = lhs_editor.display_map.read(cx);
4290 let companion = display_map.companion().unwrap().read(cx);
4291 let mapping = companion
4292 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4293 *mapping.borrow().get(&block_ids[0]).unwrap()
4294 });
4295
4296 cx.update(|_, cx| {
4297 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
4298 "custom block".to_string()
4299 });
4300 });
4301
4302 cx.run_until_parked();
4303
4304 assert_split_content(
4305 &editor,
4306 "
4307 § <no file>
4308 § -----
4309 aaa
4310 bbb
4311 § custom block
4312 ccc"
4313 .unindent(),
4314 "
4315 § <no file>
4316 § -----
4317 § spacer
4318 bbb
4319 § custom block
4320 ccc"
4321 .unindent(),
4322 &mut cx,
4323 );
4324
4325 editor.update(cx, |splittable_editor, cx| {
4326 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4327 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
4328 });
4329 });
4330
4331 cx.run_until_parked();
4332
4333 assert_split_content(
4334 &editor,
4335 "
4336 § <no file>
4337 § -----
4338 aaa
4339 bbb
4340 ccc"
4341 .unindent(),
4342 "
4343 § <no file>
4344 § -----
4345 § spacer
4346 bbb
4347 ccc"
4348 .unindent(),
4349 &mut cx,
4350 );
4351 }
4352
4353 #[gpui::test]
4354 async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
4355 use rope::Point;
4356 use unindent::Unindent as _;
4357
4358 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4359
4360 let base_text = "
4361 bbb
4362 ccc
4363 "
4364 .unindent();
4365 let current_text = "
4366 aaa
4367 bbb
4368 ccc
4369 "
4370 .unindent();
4371
4372 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4373
4374 editor.update(cx, |editor, cx| {
4375 let path = PathKey::for_buffer(&buffer, cx);
4376 editor.set_excerpts_for_path(
4377 path,
4378 buffer.clone(),
4379 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4380 0,
4381 diff.clone(),
4382 cx,
4383 );
4384 });
4385
4386 cx.run_until_parked();
4387
4388 assert_split_content(
4389 &editor,
4390 "
4391 § <no file>
4392 § -----
4393 aaa
4394 bbb
4395 ccc"
4396 .unindent(),
4397 "
4398 § <no file>
4399 § -----
4400 § spacer
4401 bbb
4402 ccc"
4403 .unindent(),
4404 &mut cx,
4405 );
4406
4407 let block_ids = editor.update(cx, |splittable_editor, cx| {
4408 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4409 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4410 let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4411 let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4412 rhs_editor.insert_blocks(
4413 [
4414 BlockProperties {
4415 placement: BlockPlacement::Above(anchor1),
4416 height: Some(1),
4417 style: BlockStyle::Fixed,
4418 render: Arc::new(|_| div().into_any()),
4419 priority: 0,
4420 },
4421 BlockProperties {
4422 placement: BlockPlacement::Above(anchor2),
4423 height: Some(1),
4424 style: BlockStyle::Fixed,
4425 render: Arc::new(|_| div().into_any()),
4426 priority: 0,
4427 },
4428 ],
4429 None,
4430 cx,
4431 )
4432 })
4433 });
4434
4435 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4436 let lhs_editor =
4437 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4438
4439 cx.update(|_, cx| {
4440 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4441 "custom block 1".to_string()
4442 });
4443 set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4444 "custom block 2".to_string()
4445 });
4446 });
4447
4448 let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4449 let display_map = lhs_editor.display_map.read(cx);
4450 let companion = display_map.companion().unwrap().read(cx);
4451 let mapping = companion
4452 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4453 (
4454 *mapping.borrow().get(&block_ids[0]).unwrap(),
4455 *mapping.borrow().get(&block_ids[1]).unwrap(),
4456 )
4457 });
4458
4459 cx.update(|_, cx| {
4460 set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4461 "custom block 1".to_string()
4462 });
4463 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4464 "custom block 2".to_string()
4465 });
4466 });
4467
4468 cx.run_until_parked();
4469
4470 assert_split_content(
4471 &editor,
4472 "
4473 § <no file>
4474 § -----
4475 aaa
4476 bbb
4477 § custom block 1
4478 ccc
4479 § custom block 2"
4480 .unindent(),
4481 "
4482 § <no file>
4483 § -----
4484 § spacer
4485 bbb
4486 § custom block 1
4487 ccc
4488 § custom block 2"
4489 .unindent(),
4490 &mut cx,
4491 );
4492
4493 editor.update(cx, |splittable_editor, cx| {
4494 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4495 rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4496 });
4497 });
4498
4499 cx.run_until_parked();
4500
4501 assert_split_content(
4502 &editor,
4503 "
4504 § <no file>
4505 § -----
4506 aaa
4507 bbb
4508 ccc
4509 § custom block 2"
4510 .unindent(),
4511 "
4512 § <no file>
4513 § -----
4514 § spacer
4515 bbb
4516 ccc
4517 § custom block 2"
4518 .unindent(),
4519 &mut cx,
4520 );
4521
4522 editor.update_in(cx, |splittable_editor, window, cx| {
4523 splittable_editor.unsplit(window, cx);
4524 });
4525
4526 cx.run_until_parked();
4527
4528 editor.update_in(cx, |splittable_editor, window, cx| {
4529 splittable_editor.split(window, cx);
4530 });
4531
4532 cx.run_until_parked();
4533
4534 let lhs_editor =
4535 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4536
4537 let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4538 let display_map = lhs_editor.display_map.read(cx);
4539 let companion = display_map.companion().unwrap().read(cx);
4540 let mapping = companion
4541 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4542 *mapping.borrow().get(&block_ids[1]).unwrap()
4543 });
4544
4545 cx.update(|_, cx| {
4546 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4547 "custom block 2".to_string()
4548 });
4549 });
4550
4551 cx.run_until_parked();
4552
4553 assert_split_content(
4554 &editor,
4555 "
4556 § <no file>
4557 § -----
4558 aaa
4559 bbb
4560 ccc
4561 § custom block 2"
4562 .unindent(),
4563 "
4564 § <no file>
4565 § -----
4566 § spacer
4567 bbb
4568 ccc
4569 § custom block 2"
4570 .unindent(),
4571 &mut cx,
4572 );
4573 }
4574
4575 #[gpui::test]
4576 async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
4577 use rope::Point;
4578 use unindent::Unindent as _;
4579
4580 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
4581
4582 let base_text = "
4583 bbb
4584 ccc
4585 "
4586 .unindent();
4587 let current_text = "
4588 aaa
4589 bbb
4590 ccc
4591 "
4592 .unindent();
4593
4594 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4595
4596 editor.update(cx, |editor, cx| {
4597 let path = PathKey::for_buffer(&buffer, cx);
4598 editor.set_excerpts_for_path(
4599 path,
4600 buffer.clone(),
4601 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4602 0,
4603 diff.clone(),
4604 cx,
4605 );
4606 });
4607
4608 cx.run_until_parked();
4609
4610 editor.update_in(cx, |splittable_editor, window, cx| {
4611 splittable_editor.unsplit(window, cx);
4612 });
4613
4614 cx.run_until_parked();
4615
4616 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4617
4618 let block_ids = editor.update(cx, |splittable_editor, cx| {
4619 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4620 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4621 let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4622 let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4623 rhs_editor.insert_blocks(
4624 [
4625 BlockProperties {
4626 placement: BlockPlacement::Above(anchor1),
4627 height: Some(1),
4628 style: BlockStyle::Fixed,
4629 render: Arc::new(|_| div().into_any()),
4630 priority: 0,
4631 },
4632 BlockProperties {
4633 placement: BlockPlacement::Above(anchor2),
4634 height: Some(1),
4635 style: BlockStyle::Fixed,
4636 render: Arc::new(|_| div().into_any()),
4637 priority: 0,
4638 },
4639 ],
4640 None,
4641 cx,
4642 )
4643 })
4644 });
4645
4646 cx.update(|_, cx| {
4647 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4648 "custom block 1".to_string()
4649 });
4650 set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4651 "custom block 2".to_string()
4652 });
4653 });
4654
4655 cx.run_until_parked();
4656
4657 let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
4658 assert_eq!(
4659 rhs_content,
4660 "
4661 § <no file>
4662 § -----
4663 aaa
4664 bbb
4665 § custom block 1
4666 ccc
4667 § custom block 2"
4668 .unindent(),
4669 "rhs content before split"
4670 );
4671
4672 editor.update_in(cx, |splittable_editor, window, cx| {
4673 splittable_editor.split(window, cx);
4674 });
4675
4676 cx.run_until_parked();
4677
4678 let lhs_editor =
4679 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4680
4681 let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4682 let display_map = lhs_editor.display_map.read(cx);
4683 let companion = display_map.companion().unwrap().read(cx);
4684 let mapping = companion
4685 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4686 (
4687 *mapping.borrow().get(&block_ids[0]).unwrap(),
4688 *mapping.borrow().get(&block_ids[1]).unwrap(),
4689 )
4690 });
4691
4692 cx.update(|_, cx| {
4693 set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4694 "custom block 1".to_string()
4695 });
4696 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4697 "custom block 2".to_string()
4698 });
4699 });
4700
4701 cx.run_until_parked();
4702
4703 assert_split_content(
4704 &editor,
4705 "
4706 § <no file>
4707 § -----
4708 aaa
4709 bbb
4710 § custom block 1
4711 ccc
4712 § custom block 2"
4713 .unindent(),
4714 "
4715 § <no file>
4716 § -----
4717 § spacer
4718 bbb
4719 § custom block 1
4720 ccc
4721 § custom block 2"
4722 .unindent(),
4723 &mut cx,
4724 );
4725
4726 editor.update(cx, |splittable_editor, cx| {
4727 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4728 rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4729 });
4730 });
4731
4732 cx.run_until_parked();
4733
4734 assert_split_content(
4735 &editor,
4736 "
4737 § <no file>
4738 § -----
4739 aaa
4740 bbb
4741 ccc
4742 § custom block 2"
4743 .unindent(),
4744 "
4745 § <no file>
4746 § -----
4747 § spacer
4748 bbb
4749 ccc
4750 § custom block 2"
4751 .unindent(),
4752 &mut cx,
4753 );
4754
4755 editor.update_in(cx, |splittable_editor, window, cx| {
4756 splittable_editor.unsplit(window, cx);
4757 });
4758
4759 cx.run_until_parked();
4760
4761 editor.update_in(cx, |splittable_editor, window, cx| {
4762 splittable_editor.split(window, cx);
4763 });
4764
4765 cx.run_until_parked();
4766
4767 let lhs_editor =
4768 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4769
4770 let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4771 let display_map = lhs_editor.display_map.read(cx);
4772 let companion = display_map.companion().unwrap().read(cx);
4773 let mapping = companion
4774 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4775 *mapping.borrow().get(&block_ids[1]).unwrap()
4776 });
4777
4778 cx.update(|_, cx| {
4779 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4780 "custom block 2".to_string()
4781 });
4782 });
4783
4784 cx.run_until_parked();
4785
4786 assert_split_content(
4787 &editor,
4788 "
4789 § <no file>
4790 § -----
4791 aaa
4792 bbb
4793 ccc
4794 § custom block 2"
4795 .unindent(),
4796 "
4797 § <no file>
4798 § -----
4799 § spacer
4800 bbb
4801 ccc
4802 § custom block 2"
4803 .unindent(),
4804 &mut cx,
4805 );
4806
4807 let new_block_ids = editor.update(cx, |splittable_editor, cx| {
4808 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4809 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4810 let anchor = snapshot.anchor_before(Point::new(2, 0));
4811 rhs_editor.insert_blocks(
4812 [BlockProperties {
4813 placement: BlockPlacement::Above(anchor),
4814 height: Some(1),
4815 style: BlockStyle::Fixed,
4816 render: Arc::new(|_| div().into_any()),
4817 priority: 0,
4818 }],
4819 None,
4820 cx,
4821 )
4822 })
4823 });
4824
4825 cx.update(|_, cx| {
4826 set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
4827 "custom block 3".to_string()
4828 });
4829 });
4830
4831 let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4832 let display_map = lhs_editor.display_map.read(cx);
4833 let companion = display_map.companion().unwrap().read(cx);
4834 let mapping = companion
4835 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
4836 *mapping.borrow().get(&new_block_ids[0]).unwrap()
4837 });
4838
4839 cx.update(|_, cx| {
4840 set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
4841 "custom block 3".to_string()
4842 });
4843 });
4844
4845 cx.run_until_parked();
4846
4847 assert_split_content(
4848 &editor,
4849 "
4850 § <no file>
4851 § -----
4852 aaa
4853 bbb
4854 § custom block 3
4855 ccc
4856 § custom block 2"
4857 .unindent(),
4858 "
4859 § <no file>
4860 § -----
4861 § spacer
4862 bbb
4863 § custom block 3
4864 ccc
4865 § custom block 2"
4866 .unindent(),
4867 &mut cx,
4868 );
4869
4870 editor.update(cx, |splittable_editor, cx| {
4871 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4872 rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
4873 });
4874 });
4875
4876 cx.run_until_parked();
4877
4878 assert_split_content(
4879 &editor,
4880 "
4881 § <no file>
4882 § -----
4883 aaa
4884 bbb
4885 ccc
4886 § custom block 2"
4887 .unindent(),
4888 "
4889 § <no file>
4890 § -----
4891 § spacer
4892 bbb
4893 ccc
4894 § custom block 2"
4895 .unindent(),
4896 &mut cx,
4897 );
4898 }
4899
4900 #[gpui::test]
4901 async fn test_buffer_folding_sync(cx: &mut gpui::TestAppContext) {
4902 use rope::Point;
4903 use unindent::Unindent as _;
4904
4905 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
4906
4907 let base_text1 = "
4908 aaa
4909 bbb
4910 ccc"
4911 .unindent();
4912 let current_text1 = "
4913 aaa
4914 bbb
4915 ccc"
4916 .unindent();
4917
4918 let base_text2 = "
4919 ddd
4920 eee
4921 fff"
4922 .unindent();
4923 let current_text2 = "
4924 ddd
4925 eee
4926 fff"
4927 .unindent();
4928
4929 let (buffer1, diff1) = buffer_with_diff(&base_text1, ¤t_text1, &mut cx);
4930 let (buffer2, diff2) = buffer_with_diff(&base_text2, ¤t_text2, &mut cx);
4931
4932 let buffer1_id = buffer1.read_with(cx, |buffer, _| buffer.remote_id());
4933 let buffer2_id = buffer2.read_with(cx, |buffer, _| buffer.remote_id());
4934
4935 editor.update(cx, |editor, cx| {
4936 let path1 = PathKey::for_buffer(&buffer1, cx);
4937 editor.set_excerpts_for_path(
4938 path1,
4939 buffer1.clone(),
4940 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
4941 0,
4942 diff1.clone(),
4943 cx,
4944 );
4945 let path2 = PathKey::for_buffer(&buffer2, cx);
4946 editor.set_excerpts_for_path(
4947 path2,
4948 buffer2.clone(),
4949 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
4950 1,
4951 diff2.clone(),
4952 cx,
4953 );
4954 });
4955
4956 cx.run_until_parked();
4957
4958 editor.update(cx, |editor, cx| {
4959 editor.rhs_editor.update(cx, |rhs_editor, cx| {
4960 rhs_editor.fold_buffer(buffer1_id, cx);
4961 });
4962 });
4963
4964 cx.run_until_parked();
4965
4966 let rhs_buffer1_folded = editor.read_with(cx, |editor, cx| {
4967 editor.rhs_editor.read(cx).is_buffer_folded(buffer1_id, cx)
4968 });
4969 assert!(
4970 rhs_buffer1_folded,
4971 "buffer1 should be folded in rhs before split"
4972 );
4973
4974 editor.update_in(cx, |editor, window, cx| {
4975 editor.split(window, cx);
4976 });
4977
4978 cx.run_until_parked();
4979
4980 let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
4981 (
4982 editor.rhs_editor.clone(),
4983 editor.lhs.as_ref().unwrap().editor.clone(),
4984 )
4985 });
4986
4987 let rhs_buffer1_folded =
4988 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
4989 assert!(
4990 rhs_buffer1_folded,
4991 "buffer1 should be folded in rhs after split"
4992 );
4993
4994 let base_buffer1_id = diff1.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
4995 let lhs_buffer1_folded = lhs_editor.read_with(cx, |editor, cx| {
4996 editor.is_buffer_folded(base_buffer1_id, cx)
4997 });
4998 assert!(
4999 lhs_buffer1_folded,
5000 "buffer1 should be folded in lhs after split"
5001 );
5002
5003 assert_split_content(
5004 &editor,
5005 "
5006 § <no file>
5007 § -----
5008 § <no file>
5009 § -----
5010 ddd
5011 eee
5012 fff"
5013 .unindent(),
5014 "
5015 § <no file>
5016 § -----
5017 § <no file>
5018 § -----
5019 ddd
5020 eee
5021 fff"
5022 .unindent(),
5023 &mut cx,
5024 );
5025
5026 editor.update(cx, |editor, cx| {
5027 editor.rhs_editor.update(cx, |rhs_editor, cx| {
5028 rhs_editor.fold_buffer(buffer2_id, cx);
5029 });
5030 });
5031
5032 cx.run_until_parked();
5033
5034 let rhs_buffer2_folded =
5035 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer2_id, cx));
5036 assert!(rhs_buffer2_folded, "buffer2 should be folded in rhs");
5037
5038 let base_buffer2_id = diff2.read_with(cx, |diff, cx| diff.base_text(cx).remote_id());
5039 let lhs_buffer2_folded = lhs_editor.read_with(cx, |editor, cx| {
5040 editor.is_buffer_folded(base_buffer2_id, cx)
5041 });
5042 assert!(lhs_buffer2_folded, "buffer2 should be folded in lhs");
5043
5044 let rhs_buffer1_still_folded =
5045 rhs_editor.read_with(cx, |editor, cx| editor.is_buffer_folded(buffer1_id, cx));
5046 assert!(
5047 rhs_buffer1_still_folded,
5048 "buffer1 should still be folded in rhs"
5049 );
5050
5051 let lhs_buffer1_still_folded = lhs_editor.read_with(cx, |editor, cx| {
5052 editor.is_buffer_folded(base_buffer1_id, cx)
5053 });
5054 assert!(
5055 lhs_buffer1_still_folded,
5056 "buffer1 should still be folded in lhs"
5057 );
5058
5059 assert_split_content(
5060 &editor,
5061 "
5062 § <no file>
5063 § -----
5064 § <no file>
5065 § -----"
5066 .unindent(),
5067 "
5068 § <no file>
5069 § -----
5070 § <no file>
5071 § -----"
5072 .unindent(),
5073 &mut cx,
5074 );
5075 }
5076
5077 #[gpui::test]
5078 async fn test_custom_block_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5079 use rope::Point;
5080 use unindent::Unindent as _;
5081
5082 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5083
5084 let base_text = "
5085 ddd
5086 eee
5087 "
5088 .unindent();
5089 let current_text = "
5090 aaa
5091 bbb
5092 ccc
5093 ddd
5094 eee
5095 "
5096 .unindent();
5097
5098 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5099
5100 editor.update(cx, |editor, cx| {
5101 let path = PathKey::for_buffer(&buffer, cx);
5102 editor.set_excerpts_for_path(
5103 path,
5104 buffer.clone(),
5105 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5106 0,
5107 diff.clone(),
5108 cx,
5109 );
5110 });
5111
5112 cx.run_until_parked();
5113
5114 assert_split_content(
5115 &editor,
5116 "
5117 § <no file>
5118 § -----
5119 aaa
5120 bbb
5121 ccc
5122 ddd
5123 eee"
5124 .unindent(),
5125 "
5126 § <no file>
5127 § -----
5128 § spacer
5129 § spacer
5130 § spacer
5131 ddd
5132 eee"
5133 .unindent(),
5134 &mut cx,
5135 );
5136
5137 let block_ids = editor.update(cx, |splittable_editor, cx| {
5138 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5139 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5140 let anchor = snapshot.anchor_before(Point::new(2, 0));
5141 rhs_editor.insert_blocks(
5142 [BlockProperties {
5143 placement: BlockPlacement::Above(anchor),
5144 height: Some(1),
5145 style: BlockStyle::Fixed,
5146 render: Arc::new(|_| div().into_any()),
5147 priority: 0,
5148 }],
5149 None,
5150 cx,
5151 )
5152 })
5153 });
5154
5155 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5156 let lhs_editor =
5157 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5158
5159 cx.update(|_, cx| {
5160 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5161 "custom block".to_string()
5162 });
5163 });
5164
5165 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5166 let display_map = lhs_editor.display_map.read(cx);
5167 let companion = display_map.companion().unwrap().read(cx);
5168 let mapping = companion
5169 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5170 *mapping.borrow().get(&block_ids[0]).unwrap()
5171 });
5172
5173 cx.update(|_, cx| {
5174 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5175 "custom block".to_string()
5176 });
5177 });
5178
5179 cx.run_until_parked();
5180
5181 assert_split_content(
5182 &editor,
5183 "
5184 § <no file>
5185 § -----
5186 aaa
5187 bbb
5188 § custom block
5189 ccc
5190 ddd
5191 eee"
5192 .unindent(),
5193 "
5194 § <no file>
5195 § -----
5196 § spacer
5197 § spacer
5198 § spacer
5199 § custom block
5200 ddd
5201 eee"
5202 .unindent(),
5203 &mut cx,
5204 );
5205
5206 editor.update(cx, |splittable_editor, cx| {
5207 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5208 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5209 });
5210 });
5211
5212 cx.run_until_parked();
5213
5214 assert_split_content(
5215 &editor,
5216 "
5217 § <no file>
5218 § -----
5219 aaa
5220 bbb
5221 ccc
5222 ddd
5223 eee"
5224 .unindent(),
5225 "
5226 § <no file>
5227 § -----
5228 § spacer
5229 § spacer
5230 § spacer
5231 ddd
5232 eee"
5233 .unindent(),
5234 &mut cx,
5235 );
5236 }
5237
5238 #[gpui::test]
5239 async fn test_custom_block_below_in_middle_of_added_hunk(cx: &mut gpui::TestAppContext) {
5240 use rope::Point;
5241 use unindent::Unindent as _;
5242
5243 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5244
5245 let base_text = "
5246 ddd
5247 eee
5248 "
5249 .unindent();
5250 let current_text = "
5251 aaa
5252 bbb
5253 ccc
5254 ddd
5255 eee
5256 "
5257 .unindent();
5258
5259 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5260
5261 editor.update(cx, |editor, cx| {
5262 let path = PathKey::for_buffer(&buffer, cx);
5263 editor.set_excerpts_for_path(
5264 path,
5265 buffer.clone(),
5266 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5267 0,
5268 diff.clone(),
5269 cx,
5270 );
5271 });
5272
5273 cx.run_until_parked();
5274
5275 assert_split_content(
5276 &editor,
5277 "
5278 § <no file>
5279 § -----
5280 aaa
5281 bbb
5282 ccc
5283 ddd
5284 eee"
5285 .unindent(),
5286 "
5287 § <no file>
5288 § -----
5289 § spacer
5290 § spacer
5291 § spacer
5292 ddd
5293 eee"
5294 .unindent(),
5295 &mut cx,
5296 );
5297
5298 let block_ids = editor.update(cx, |splittable_editor, cx| {
5299 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5300 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5301 let anchor = snapshot.anchor_after(Point::new(1, 3));
5302 rhs_editor.insert_blocks(
5303 [BlockProperties {
5304 placement: BlockPlacement::Below(anchor),
5305 height: Some(1),
5306 style: BlockStyle::Fixed,
5307 render: Arc::new(|_| div().into_any()),
5308 priority: 0,
5309 }],
5310 None,
5311 cx,
5312 )
5313 })
5314 });
5315
5316 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5317 let lhs_editor =
5318 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5319
5320 cx.update(|_, cx| {
5321 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
5322 "custom block".to_string()
5323 });
5324 });
5325
5326 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5327 let display_map = lhs_editor.display_map.read(cx);
5328 let companion = display_map.companion().unwrap().read(cx);
5329 let mapping = companion
5330 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5331 *mapping.borrow().get(&block_ids[0]).unwrap()
5332 });
5333
5334 cx.update(|_, cx| {
5335 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
5336 "custom block".to_string()
5337 });
5338 });
5339
5340 cx.run_until_parked();
5341
5342 assert_split_content(
5343 &editor,
5344 "
5345 § <no file>
5346 § -----
5347 aaa
5348 bbb
5349 § custom block
5350 ccc
5351 ddd
5352 eee"
5353 .unindent(),
5354 "
5355 § <no file>
5356 § -----
5357 § spacer
5358 § spacer
5359 § spacer
5360 § custom block
5361 ddd
5362 eee"
5363 .unindent(),
5364 &mut cx,
5365 );
5366
5367 editor.update(cx, |splittable_editor, cx| {
5368 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5369 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
5370 });
5371 });
5372
5373 cx.run_until_parked();
5374
5375 assert_split_content(
5376 &editor,
5377 "
5378 § <no file>
5379 § -----
5380 aaa
5381 bbb
5382 ccc
5383 ddd
5384 eee"
5385 .unindent(),
5386 "
5387 § <no file>
5388 § -----
5389 § spacer
5390 § spacer
5391 § spacer
5392 ddd
5393 eee"
5394 .unindent(),
5395 &mut cx,
5396 );
5397 }
5398
5399 #[gpui::test]
5400 async fn test_custom_block_resize_syncs_balancing_block(cx: &mut gpui::TestAppContext) {
5401 use rope::Point;
5402 use unindent::Unindent as _;
5403
5404 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5405
5406 let base_text = "
5407 bbb
5408 ccc
5409 "
5410 .unindent();
5411 let current_text = "
5412 aaa
5413 bbb
5414 ccc
5415 "
5416 .unindent();
5417
5418 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5419
5420 editor.update(cx, |editor, cx| {
5421 let path = PathKey::for_buffer(&buffer, cx);
5422 editor.set_excerpts_for_path(
5423 path,
5424 buffer.clone(),
5425 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5426 0,
5427 diff.clone(),
5428 cx,
5429 );
5430 });
5431
5432 cx.run_until_parked();
5433
5434 let block_ids = editor.update(cx, |splittable_editor, cx| {
5435 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5436 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5437 let anchor = snapshot.anchor_before(Point::new(2, 0));
5438 rhs_editor.insert_blocks(
5439 [BlockProperties {
5440 placement: BlockPlacement::Above(anchor),
5441 height: Some(1),
5442 style: BlockStyle::Fixed,
5443 render: Arc::new(|_| div().into_any()),
5444 priority: 0,
5445 }],
5446 None,
5447 cx,
5448 )
5449 })
5450 });
5451
5452 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
5453 let lhs_editor =
5454 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
5455
5456 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
5457 let display_map = lhs_editor.display_map.read(cx);
5458 let companion = display_map.companion().unwrap().read(cx);
5459 let mapping = companion
5460 .custom_block_to_balancing_block(rhs_editor.read(cx).display_map.entity_id());
5461 *mapping.borrow().get(&block_ids[0]).unwrap()
5462 });
5463
5464 cx.run_until_parked();
5465
5466 let get_block_height = |editor: &Entity<crate::Editor>,
5467 block_id: crate::CustomBlockId,
5468 cx: &mut VisualTestContext| {
5469 editor.update_in(cx, |editor, window, cx| {
5470 let snapshot = editor.snapshot(window, cx);
5471 snapshot
5472 .block_for_id(crate::BlockId::Custom(block_id))
5473 .map(|block| block.height())
5474 })
5475 };
5476
5477 assert_eq!(
5478 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5479 Some(1)
5480 );
5481 assert_eq!(
5482 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5483 Some(1)
5484 );
5485
5486 editor.update(cx, |splittable_editor, cx| {
5487 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5488 let mut heights = HashMap::default();
5489 heights.insert(block_ids[0], 3);
5490 rhs_editor.resize_blocks(heights, None, cx);
5491 });
5492 });
5493
5494 cx.run_until_parked();
5495
5496 assert_eq!(
5497 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5498 Some(3)
5499 );
5500 assert_eq!(
5501 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5502 Some(3)
5503 );
5504
5505 editor.update(cx, |splittable_editor, cx| {
5506 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
5507 let mut heights = HashMap::default();
5508 heights.insert(block_ids[0], 5);
5509 rhs_editor.resize_blocks(heights, None, cx);
5510 });
5511 });
5512
5513 cx.run_until_parked();
5514
5515 assert_eq!(
5516 get_block_height(&rhs_editor, block_ids[0], &mut cx),
5517 Some(5)
5518 );
5519 assert_eq!(
5520 get_block_height(&lhs_editor, lhs_block_id, &mut cx),
5521 Some(5)
5522 );
5523 }
5524
5525 #[gpui::test]
5526 async fn test_edit_spanning_excerpt_boundaries_then_resplit(cx: &mut gpui::TestAppContext) {
5527 use rope::Point;
5528 use unindent::Unindent as _;
5529
5530 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5531
5532 let base_text = "
5533 aaa
5534 bbb
5535 ccc
5536 ddd
5537 eee
5538 fff
5539 ggg
5540 hhh
5541 iii
5542 jjj
5543 kkk
5544 lll
5545 "
5546 .unindent();
5547 let current_text = base_text.clone();
5548
5549 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5550
5551 editor.update(cx, |editor, cx| {
5552 let path = PathKey::for_buffer(&buffer, cx);
5553 editor.set_excerpts_for_path(
5554 path,
5555 buffer.clone(),
5556 vec![
5557 Point::new(0, 0)..Point::new(3, 3),
5558 Point::new(5, 0)..Point::new(8, 3),
5559 Point::new(10, 0)..Point::new(11, 3),
5560 ],
5561 0,
5562 diff.clone(),
5563 cx,
5564 );
5565 });
5566
5567 cx.run_until_parked();
5568
5569 buffer.update(cx, |buffer, cx| {
5570 buffer.edit([(Point::new(1, 0)..Point::new(10, 0), "")], None, cx);
5571 });
5572
5573 cx.run_until_parked();
5574
5575 editor.update_in(cx, |splittable_editor, window, cx| {
5576 splittable_editor.unsplit(window, cx);
5577 });
5578
5579 cx.run_until_parked();
5580
5581 editor.update_in(cx, |splittable_editor, window, cx| {
5582 splittable_editor.split(window, cx);
5583 });
5584
5585 cx.run_until_parked();
5586 }
5587
5588 #[gpui::test]
5589 async fn test_range_folds_removed_on_split(cx: &mut gpui::TestAppContext) {
5590 use rope::Point;
5591 use unindent::Unindent as _;
5592
5593 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5594
5595 let base_text = "
5596 aaa
5597 bbb
5598 ccc
5599 ddd
5600 eee"
5601 .unindent();
5602 let current_text = "
5603 aaa
5604 bbb
5605 ccc
5606 ddd
5607 eee"
5608 .unindent();
5609
5610 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5611
5612 editor.update(cx, |editor, cx| {
5613 let path = PathKey::for_buffer(&buffer, cx);
5614 editor.set_excerpts_for_path(
5615 path,
5616 buffer.clone(),
5617 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
5618 0,
5619 diff.clone(),
5620 cx,
5621 );
5622 });
5623
5624 cx.run_until_parked();
5625
5626 editor.update_in(cx, |editor, window, cx| {
5627 editor.rhs_editor.update(cx, |rhs_editor, cx| {
5628 rhs_editor.fold_creases(
5629 vec![Crease::simple(
5630 Point::new(1, 0)..Point::new(3, 0),
5631 FoldPlaceholder::test(),
5632 )],
5633 false,
5634 window,
5635 cx,
5636 );
5637 });
5638 });
5639
5640 cx.run_until_parked();
5641
5642 editor.update_in(cx, |editor, window, cx| {
5643 editor.split(window, cx);
5644 });
5645
5646 cx.run_until_parked();
5647
5648 let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
5649 (
5650 editor.rhs_editor.clone(),
5651 editor.lhs.as_ref().unwrap().editor.clone(),
5652 )
5653 });
5654
5655 let rhs_has_folds_after_split = rhs_editor.update(cx, |editor, cx| {
5656 let snapshot = editor.display_snapshot(cx);
5657 snapshot
5658 .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5659 .next()
5660 .is_some()
5661 });
5662 assert!(
5663 !rhs_has_folds_after_split,
5664 "rhs should not have range folds after split"
5665 );
5666
5667 let lhs_has_folds = lhs_editor.update(cx, |editor, cx| {
5668 let snapshot = editor.display_snapshot(cx);
5669 snapshot
5670 .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
5671 .next()
5672 .is_some()
5673 });
5674 assert!(!lhs_has_folds, "lhs should not have any range folds");
5675 }
5676
5677 #[gpui::test]
5678 async fn test_multiline_inlays_create_spacers(cx: &mut gpui::TestAppContext) {
5679 use rope::Point;
5680 use unindent::Unindent as _;
5681
5682 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5683
5684 let base_text = "
5685 aaa
5686 bbb
5687 ccc
5688 ddd
5689 "
5690 .unindent();
5691 let current_text = base_text.clone();
5692
5693 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5694
5695 editor.update(cx, |editor, cx| {
5696 let path = PathKey::for_buffer(&buffer, cx);
5697 editor.set_excerpts_for_path(
5698 path,
5699 buffer.clone(),
5700 vec![Point::new(0, 0)..Point::new(3, 3)],
5701 0,
5702 diff.clone(),
5703 cx,
5704 );
5705 });
5706
5707 cx.run_until_parked();
5708
5709 let rhs_editor = editor.read_with(cx, |e, _| e.rhs_editor.clone());
5710 rhs_editor.update(cx, |rhs_editor, cx| {
5711 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
5712 rhs_editor.splice_inlays(
5713 &[],
5714 vec![
5715 Inlay::edit_prediction(
5716 0,
5717 snapshot.anchor_after(Point::new(0, 3)),
5718 "\nINLAY_WITHIN",
5719 ),
5720 Inlay::edit_prediction(
5721 1,
5722 snapshot.anchor_after(Point::new(1, 3)),
5723 "\nINLAY_MID_1\nINLAY_MID_2",
5724 ),
5725 Inlay::edit_prediction(
5726 2,
5727 snapshot.anchor_after(Point::new(3, 3)),
5728 "\nINLAY_END_1\nINLAY_END_2",
5729 ),
5730 ],
5731 cx,
5732 );
5733 });
5734
5735 cx.run_until_parked();
5736
5737 assert_split_content(
5738 &editor,
5739 "
5740 § <no file>
5741 § -----
5742 aaa
5743 INLAY_WITHIN
5744 bbb
5745 INLAY_MID_1
5746 INLAY_MID_2
5747 ccc
5748 ddd
5749 INLAY_END_1
5750 INLAY_END_2"
5751 .unindent(),
5752 "
5753 § <no file>
5754 § -----
5755 aaa
5756 § spacer
5757 bbb
5758 § spacer
5759 § spacer
5760 ccc
5761 ddd
5762 § spacer
5763 § spacer"
5764 .unindent(),
5765 &mut cx,
5766 );
5767 }
5768
5769 #[gpui::test]
5770 async fn test_split_after_removing_folded_buffer(cx: &mut gpui::TestAppContext) {
5771 use rope::Point;
5772 use unindent::Unindent as _;
5773
5774 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
5775
5776 let base_text_a = "
5777 aaa
5778 bbb
5779 ccc
5780 "
5781 .unindent();
5782 let current_text_a = "
5783 aaa
5784 bbb modified
5785 ccc
5786 "
5787 .unindent();
5788
5789 let base_text_b = "
5790 xxx
5791 yyy
5792 zzz
5793 "
5794 .unindent();
5795 let current_text_b = "
5796 xxx
5797 yyy modified
5798 zzz
5799 "
5800 .unindent();
5801
5802 let (buffer_a, diff_a) = buffer_with_diff(&base_text_a, ¤t_text_a, &mut cx);
5803 let (buffer_b, diff_b) = buffer_with_diff(&base_text_b, ¤t_text_b, &mut cx);
5804
5805 let path_a = cx.read(|cx| PathKey::for_buffer(&buffer_a, cx));
5806 let path_b = cx.read(|cx| PathKey::for_buffer(&buffer_b, cx));
5807
5808 editor.update(cx, |editor, cx| {
5809 editor.set_excerpts_for_path(
5810 path_a.clone(),
5811 buffer_a.clone(),
5812 vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
5813 0,
5814 diff_a.clone(),
5815 cx,
5816 );
5817 editor.set_excerpts_for_path(
5818 path_b.clone(),
5819 buffer_b.clone(),
5820 vec![Point::new(0, 0)..buffer_b.read(cx).max_point()],
5821 0,
5822 diff_b.clone(),
5823 cx,
5824 );
5825 });
5826
5827 cx.run_until_parked();
5828
5829 let buffer_a_id = buffer_a.read_with(cx, |buffer, _| buffer.remote_id());
5830 editor.update(cx, |editor, cx| {
5831 editor.rhs_editor().update(cx, |right_editor, cx| {
5832 right_editor.fold_buffer(buffer_a_id, cx)
5833 });
5834 });
5835
5836 cx.run_until_parked();
5837
5838 editor.update(cx, |editor, cx| {
5839 editor.remove_excerpts_for_path(path_a.clone(), cx);
5840 });
5841 cx.run_until_parked();
5842
5843 editor.update_in(cx, |editor, window, cx| editor.split(window, cx));
5844 cx.run_until_parked();
5845
5846 editor.update(cx, |editor, cx| {
5847 editor.set_excerpts_for_path(
5848 path_a.clone(),
5849 buffer_a.clone(),
5850 vec![Point::new(0, 0)..buffer_a.read(cx).max_point()],
5851 0,
5852 diff_a.clone(),
5853 cx,
5854 );
5855 assert!(
5856 !editor
5857 .lhs_editor()
5858 .unwrap()
5859 .read(cx)
5860 .is_buffer_folded(buffer_a_id, cx)
5861 );
5862 assert!(
5863 !editor
5864 .rhs_editor()
5865 .read(cx)
5866 .is_buffer_folded(buffer_a_id, cx)
5867 );
5868 });
5869 }
5870
5871 #[gpui::test]
5872 async fn test_two_path_keys_for_one_buffer(cx: &mut gpui::TestAppContext) {
5873 use multi_buffer::PathKey;
5874 use rope::Point;
5875 use unindent::Unindent as _;
5876
5877 let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Split).await;
5878
5879 let base_text = "
5880 aaa
5881 bbb
5882 ccc
5883 "
5884 .unindent();
5885 let current_text = "
5886 aaa
5887 bbb modified
5888 ccc
5889 "
5890 .unindent();
5891
5892 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
5893
5894 let path_key_1 = PathKey {
5895 sort_prefix: Some(0),
5896 path: rel_path("file1.txt").into(),
5897 };
5898 let path_key_2 = PathKey {
5899 sort_prefix: Some(1),
5900 path: rel_path("file1.txt").into(),
5901 };
5902
5903 editor.update(cx, |editor, cx| {
5904 editor.set_excerpts_for_path(
5905 path_key_1.clone(),
5906 buffer.clone(),
5907 vec![Point::new(0, 0)..Point::new(1, 0)],
5908 0,
5909 diff.clone(),
5910 cx,
5911 );
5912 editor.set_excerpts_for_path(
5913 path_key_2.clone(),
5914 buffer.clone(),
5915 vec![Point::new(1, 0)..buffer.read(cx).max_point()],
5916 1,
5917 diff.clone(),
5918 cx,
5919 );
5920 });
5921
5922 cx.run_until_parked();
5923 }
5924}