1use std::{
2 ops::{Bound, Range, RangeInclusive},
3 sync::Arc,
4};
5
6use buffer_diff::{BufferDiff, BufferDiffSnapshot};
7use collections::HashMap;
8use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
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 text::{BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
19use ui::{
20 App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
21 Styled as _, Window, div,
22};
23
24use crate::{
25 display_map::CompanionExcerptPatch,
26 split_editor_view::{SplitEditorState, SplitEditorView},
27};
28use workspace::{
29 ActivatePaneLeft, ActivatePaneRight, Item, ToolbarItemLocation, Workspace,
30 item::{BreadcrumbText, ItemBufferKind, ItemEvent, SaveOptions, TabContentParams},
31 searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
32};
33
34use crate::{
35 Autoscroll, DisplayMap, Editor, EditorEvent, RenderDiffHunkControlsFn, ToggleCodeActions,
36 ToggleSoftWrap,
37 actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
38 display_map::Companion,
39};
40use zed_actions::assistant::InlineAssist;
41
42pub(crate) fn convert_lhs_rows_to_rhs(
43 lhs_excerpt_to_rhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
44 rhs_snapshot: &MultiBufferSnapshot,
45 lhs_snapshot: &MultiBufferSnapshot,
46 lhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
47) -> Vec<CompanionExcerptPatch> {
48 patches_for_range(
49 lhs_excerpt_to_rhs_excerpt,
50 lhs_snapshot,
51 rhs_snapshot,
52 lhs_bounds,
53 |diff, range, buffer| diff.patch_for_base_text_range(range, buffer),
54 )
55}
56
57pub(crate) fn convert_rhs_rows_to_lhs(
58 rhs_excerpt_to_lhs_excerpt: &HashMap<ExcerptId, ExcerptId>,
59 lhs_snapshot: &MultiBufferSnapshot,
60 rhs_snapshot: &MultiBufferSnapshot,
61 rhs_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
62) -> Vec<CompanionExcerptPatch> {
63 patches_for_range(
64 rhs_excerpt_to_lhs_excerpt,
65 rhs_snapshot,
66 lhs_snapshot,
67 rhs_bounds,
68 |diff, range, buffer| diff.patch_for_buffer_range(range, buffer),
69 )
70}
71
72fn translate_lhs_selections_to_rhs(
73 selections_by_buffer: &HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>,
74 splittable: &SplittableEditor,
75 cx: &App,
76) -> HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> {
77 let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx);
78 let Some(companion) = rhs_display_map.companion() else {
79 return HashMap::default();
80 };
81 let companion = companion.read(cx);
82
83 let mut translated: HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> =
84 HashMap::default();
85
86 for (lhs_buffer_id, (ranges, scroll_offset)) in selections_by_buffer {
87 let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(*lhs_buffer_id) else {
88 continue;
89 };
90
91 let Some(rhs_buffer) = splittable
92 .rhs_editor
93 .read(cx)
94 .buffer()
95 .read(cx)
96 .buffer(rhs_buffer_id)
97 else {
98 continue;
99 };
100
101 let Some(diff) = splittable
102 .rhs_editor
103 .read(cx)
104 .buffer()
105 .read(cx)
106 .diff_for(rhs_buffer_id)
107 else {
108 continue;
109 };
110
111 let diff_snapshot = diff.read(cx).snapshot(cx);
112 let rhs_buffer_snapshot = rhs_buffer.read(cx).snapshot();
113 let base_text_buffer = diff.read(cx).base_text_buffer();
114 let base_text_snapshot = base_text_buffer.read(cx).snapshot();
115
116 let translated_ranges: Vec<Range<BufferOffset>> = ranges
117 .iter()
118 .map(|range| {
119 let start_point = base_text_snapshot.offset_to_point(range.start.0);
120 let end_point = base_text_snapshot.offset_to_point(range.end.0);
121
122 let rhs_start = diff_snapshot
123 .base_text_point_to_buffer_point(start_point, &rhs_buffer_snapshot);
124 let rhs_end =
125 diff_snapshot.base_text_point_to_buffer_point(end_point, &rhs_buffer_snapshot);
126
127 BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_start))
128 ..BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_end))
129 })
130 .collect();
131
132 translated.insert(rhs_buffer, (translated_ranges, *scroll_offset));
133 }
134
135 translated
136}
137
138fn translate_lhs_hunks_to_rhs(
139 lhs_hunks: &[MultiBufferDiffHunk],
140 splittable: &SplittableEditor,
141 cx: &App,
142) -> Vec<MultiBufferDiffHunk> {
143 let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx);
144 let Some(companion) = rhs_display_map.companion() else {
145 return vec![];
146 };
147 let companion = companion.read(cx);
148 let rhs_snapshot = splittable.rhs_multibuffer.read(cx).snapshot(cx);
149 let rhs_hunks: Vec<MultiBufferDiffHunk> = rhs_snapshot.diff_hunks().collect();
150
151 let mut translated = Vec::new();
152 for lhs_hunk in lhs_hunks {
153 let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(lhs_hunk.buffer_id) else {
154 continue;
155 };
156 if let Some(rhs_hunk) = rhs_hunks.iter().find(|rhs_hunk| {
157 rhs_hunk.buffer_id == rhs_buffer_id
158 && rhs_hunk.diff_base_byte_range == lhs_hunk.diff_base_byte_range
159 }) {
160 translated.push(rhs_hunk.clone());
161 }
162 }
163 translated
164}
165
166fn patches_for_range<F>(
167 excerpt_map: &HashMap<ExcerptId, ExcerptId>,
168 source_snapshot: &MultiBufferSnapshot,
169 target_snapshot: &MultiBufferSnapshot,
170 source_bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
171 translate_fn: F,
172) -> Vec<CompanionExcerptPatch>
173where
174 F: Fn(&BufferDiffSnapshot, RangeInclusive<Point>, &text::BufferSnapshot) -> Patch<Point>,
175{
176 let mut result = Vec::new();
177 let mut patches = HashMap::default();
178
179 for (source_buffer, buffer_offset_range, source_excerpt_id) in
180 source_snapshot.range_to_buffer_ranges(source_bounds)
181 {
182 let target_excerpt_id = excerpt_map.get(&source_excerpt_id).copied().unwrap();
183 let target_buffer = target_snapshot
184 .buffer_for_excerpt(target_excerpt_id)
185 .unwrap();
186 let patch = patches.entry(source_buffer.remote_id()).or_insert_with(|| {
187 let diff = source_snapshot
188 .diff_for_buffer_id(source_buffer.remote_id())
189 .unwrap();
190 let rhs_buffer = if source_buffer.remote_id() == diff.base_text().remote_id() {
191 &target_buffer
192 } else {
193 source_buffer
194 };
195 // TODO(split-diff) pass only the union of the ranges for the affected excerpts
196 translate_fn(diff, Point::zero()..=source_buffer.max_point(), rhs_buffer)
197 });
198 let buffer_point_range = buffer_offset_range.to_point(source_buffer);
199
200 // TODO(split-diff) maybe narrow the patch to only the edited part of the excerpt
201 // (less useful for project diff, but important if we want to do singleton side-by-side diff)
202 result.push(patch_for_excerpt(
203 source_snapshot,
204 target_snapshot,
205 source_excerpt_id,
206 target_excerpt_id,
207 source_buffer,
208 target_buffer,
209 patch,
210 buffer_point_range,
211 ));
212 }
213
214 result
215}
216
217fn patch_for_excerpt(
218 source_snapshot: &MultiBufferSnapshot,
219 target_snapshot: &MultiBufferSnapshot,
220 source_excerpt_id: ExcerptId,
221 target_excerpt_id: ExcerptId,
222 source_buffer: &text::BufferSnapshot,
223 target_buffer: &text::BufferSnapshot,
224 patch: &Patch<Point>,
225 source_edited_range: Range<Point>,
226) -> CompanionExcerptPatch {
227 let source_multibuffer_range = source_snapshot
228 .range_for_excerpt(source_excerpt_id)
229 .unwrap();
230 let source_excerpt_start_in_multibuffer = source_multibuffer_range.start;
231 let source_context_range = source_snapshot
232 .context_range_for_excerpt(source_excerpt_id)
233 .unwrap();
234 let source_excerpt_start_in_buffer = source_context_range.start.to_point(&source_buffer);
235 let source_excerpt_end_in_buffer = source_context_range.end.to_point(&source_buffer);
236 let target_multibuffer_range = target_snapshot
237 .range_for_excerpt(target_excerpt_id)
238 .unwrap();
239 let target_excerpt_start_in_multibuffer = target_multibuffer_range.start;
240 let target_context_range = target_snapshot
241 .context_range_for_excerpt(target_excerpt_id)
242 .unwrap();
243 let target_excerpt_start_in_buffer = target_context_range.start.to_point(&target_buffer);
244 let target_excerpt_end_in_buffer = target_context_range.end.to_point(&target_buffer);
245
246 let edits = patch
247 .edits()
248 .iter()
249 .skip_while(|edit| edit.old.end < source_excerpt_start_in_buffer)
250 .take_while(|edit| edit.old.start <= source_excerpt_end_in_buffer)
251 .map(|edit| {
252 let clamped_source_start = edit
253 .old
254 .start
255 .max(source_excerpt_start_in_buffer)
256 .min(source_excerpt_end_in_buffer);
257 let clamped_source_end = edit
258 .old
259 .end
260 .max(source_excerpt_start_in_buffer)
261 .min(source_excerpt_end_in_buffer);
262 let source_multibuffer_start = source_excerpt_start_in_multibuffer
263 + (clamped_source_start - source_excerpt_start_in_buffer);
264 let source_multibuffer_end = source_excerpt_start_in_multibuffer
265 + (clamped_source_end - source_excerpt_start_in_buffer);
266 let clamped_target_start = edit
267 .new
268 .start
269 .max(target_excerpt_start_in_buffer)
270 .min(target_excerpt_end_in_buffer);
271 let clamped_target_end = edit
272 .new
273 .end
274 .max(target_excerpt_start_in_buffer)
275 .min(target_excerpt_end_in_buffer);
276 let target_multibuffer_start = target_excerpt_start_in_multibuffer
277 + (clamped_target_start - target_excerpt_start_in_buffer);
278 let target_multibuffer_end = target_excerpt_start_in_multibuffer
279 + (clamped_target_end - target_excerpt_start_in_buffer);
280 text::Edit {
281 old: source_multibuffer_start..source_multibuffer_end,
282 new: target_multibuffer_start..target_multibuffer_end,
283 }
284 });
285
286 let edits = [text::Edit {
287 old: source_excerpt_start_in_multibuffer..source_excerpt_start_in_multibuffer,
288 new: target_excerpt_start_in_multibuffer..target_excerpt_start_in_multibuffer,
289 }]
290 .into_iter()
291 .chain(edits);
292
293 let mut merged_edits: Vec<text::Edit<Point>> = Vec::new();
294 for edit in edits {
295 if let Some(last) = merged_edits.last_mut() {
296 if edit.new.start <= last.new.end {
297 last.old.end = last.old.end.max(edit.old.end);
298 last.new.end = last.new.end.max(edit.new.end);
299 continue;
300 }
301 }
302 merged_edits.push(edit);
303 }
304
305 let edited_range = source_excerpt_start_in_multibuffer
306 + (source_edited_range.start - source_excerpt_start_in_buffer)
307 ..source_excerpt_start_in_multibuffer
308 + (source_edited_range.end - source_excerpt_start_in_buffer);
309
310 let source_excerpt_end = source_excerpt_start_in_multibuffer
311 + (source_excerpt_end_in_buffer - source_excerpt_start_in_buffer);
312 let target_excerpt_end = target_excerpt_start_in_multibuffer
313 + (target_excerpt_end_in_buffer - target_excerpt_start_in_buffer);
314
315 CompanionExcerptPatch {
316 patch: Patch::new(merged_edits),
317 edited_range,
318 source_excerpt_range: source_excerpt_start_in_multibuffer..source_excerpt_end,
319 target_excerpt_range: target_excerpt_start_in_multibuffer..target_excerpt_end,
320 }
321}
322
323pub struct SplitDiffFeatureFlag;
324
325impl FeatureFlag for SplitDiffFeatureFlag {
326 const NAME: &'static str = "split-diff";
327
328 fn enabled_for_staff() -> bool {
329 true
330 }
331}
332
333#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
334#[action(namespace = editor)]
335struct SplitDiff;
336
337#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
338#[action(namespace = editor)]
339struct UnsplitDiff;
340
341#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
342#[action(namespace = editor)]
343pub struct ToggleSplitDiff;
344
345#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
346#[action(namespace = editor)]
347struct JumpToCorrespondingRow;
348
349/// When locked cursors mode is enabled, cursor movements in one editor will
350/// update the cursor position in the other editor to the corresponding row.
351#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
352#[action(namespace = editor)]
353pub struct ToggleLockedCursors;
354
355pub struct SplittableEditor {
356 rhs_multibuffer: Entity<MultiBuffer>,
357 rhs_editor: Entity<Editor>,
358 lhs: Option<LhsEditor>,
359 workspace: WeakEntity<Workspace>,
360 split_state: Entity<SplitEditorState>,
361 locked_cursors: bool,
362 _subscriptions: Vec<Subscription>,
363}
364
365struct LhsEditor {
366 multibuffer: Entity<MultiBuffer>,
367 editor: Entity<Editor>,
368 was_last_focused: bool,
369 _subscriptions: Vec<Subscription>,
370}
371
372impl SplittableEditor {
373 pub fn rhs_editor(&self) -> &Entity<Editor> {
374 &self.rhs_editor
375 }
376
377 pub fn lhs_editor(&self) -> Option<&Entity<Editor>> {
378 self.lhs.as_ref().map(|s| &s.editor)
379 }
380
381 pub fn is_split(&self) -> bool {
382 self.lhs.is_some()
383 }
384
385 pub fn set_render_diff_hunk_controls(
386 &self,
387 render_diff_hunk_controls: RenderDiffHunkControlsFn,
388 cx: &mut Context<Self>,
389 ) {
390 self.rhs_editor.update(cx, |editor, cx| {
391 editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
392 });
393
394 if let Some(lhs) = &self.lhs {
395 lhs.editor.update(cx, |editor, cx| {
396 editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
397 });
398 }
399 }
400
401 pub fn last_selected_editor(&self) -> &Entity<Editor> {
402 if let Some(lhs) = &self.lhs
403 && lhs.was_last_focused
404 {
405 &lhs.editor
406 } else {
407 &self.rhs_editor
408 }
409 }
410
411 pub fn new_unsplit(
412 rhs_multibuffer: Entity<MultiBuffer>,
413 project: Entity<Project>,
414 workspace: Entity<Workspace>,
415 window: &mut Window,
416 cx: &mut Context<Self>,
417 ) -> Self {
418 let rhs_editor = cx.new(|cx| {
419 let mut editor =
420 Editor::for_multibuffer(rhs_multibuffer.clone(), Some(project.clone()), window, cx);
421 editor.set_expand_all_diff_hunks(cx);
422 editor
423 });
424 // TODO(split-diff) we might want to tag editor events with whether they came from rhs/lhs
425 let subscriptions = vec![
426 cx.subscribe(
427 &rhs_editor,
428 |this, _, event: &EditorEvent, cx| match event {
429 EditorEvent::ExpandExcerptsRequested {
430 excerpt_ids,
431 lines,
432 direction,
433 } => {
434 this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
435 }
436 _ => cx.emit(event.clone()),
437 },
438 ),
439 cx.subscribe(&rhs_editor, |_, _, event: &SearchEvent, cx| {
440 cx.emit(event.clone());
441 }),
442 ];
443
444 window.defer(cx, {
445 let workspace = workspace.downgrade();
446 let rhs_editor = rhs_editor.downgrade();
447 move |window, cx| {
448 workspace
449 .update(cx, |workspace, cx| {
450 rhs_editor.update(cx, |editor, cx| {
451 editor.added_to_workspace(workspace, window, cx);
452 })
453 })
454 .ok();
455 }
456 });
457 let split_state = cx.new(|cx| SplitEditorState::new(cx));
458 Self {
459 rhs_editor,
460 rhs_multibuffer,
461 lhs: None,
462 workspace: workspace.downgrade(),
463 split_state,
464 locked_cursors: false,
465 _subscriptions: subscriptions,
466 }
467 }
468
469 fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
470 if !cx.has_flag::<SplitDiffFeatureFlag>() {
471 return;
472 }
473 if self.lhs.is_some() {
474 return;
475 }
476 let Some(workspace) = self.workspace.upgrade() else {
477 return;
478 };
479 let project = workspace.read(cx).project().clone();
480
481 let lhs_multibuffer = cx.new(|cx| {
482 let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
483 multibuffer.set_all_diff_hunks_expanded(cx);
484 multibuffer
485 });
486
487 let render_diff_hunk_controls = self.rhs_editor.read(cx).render_diff_hunk_controls.clone();
488 let lhs_editor = cx.new(|cx| {
489 let mut editor =
490 Editor::for_multibuffer(lhs_multibuffer.clone(), Some(project.clone()), window, cx);
491 editor.set_number_deleted_lines(true, cx);
492 editor.set_delegate_expand_excerpts(true);
493 editor.set_delegate_stage_and_restore(true);
494 editor.set_delegate_open_excerpts(true);
495 editor.set_show_vertical_scrollbar(false, cx);
496 editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
497 editor
498 });
499
500 lhs_editor.update(cx, |editor, cx| {
501 editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
502 });
503
504 let mut subscriptions = vec![cx.subscribe_in(
505 &lhs_editor,
506 window,
507 |this, _, event: &EditorEvent, window, cx| match event {
508 EditorEvent::ExpandExcerptsRequested {
509 excerpt_ids,
510 lines,
511 direction,
512 } => {
513 if this.lhs.is_some() {
514 let rhs_display_map = this.rhs_editor.read(cx).display_map.read(cx);
515 let rhs_ids: Vec<_> = excerpt_ids
516 .iter()
517 .filter_map(|id| {
518 rhs_display_map.companion_excerpt_to_my_excerpt(*id, cx)
519 })
520 .collect();
521 this.expand_excerpts(rhs_ids.into_iter(), *lines, *direction, cx);
522 }
523 }
524 EditorEvent::StageOrUnstageRequested { stage, hunks } => {
525 if this.lhs.is_some() {
526 let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
527 if !translated.is_empty() {
528 let stage = *stage;
529 this.rhs_editor.update(cx, |editor, cx| {
530 let chunk_by = translated.into_iter().chunk_by(|h| h.buffer_id);
531 for (buffer_id, hunks) in &chunk_by {
532 editor.do_stage_or_unstage(stage, buffer_id, hunks, cx);
533 }
534 });
535 }
536 }
537 }
538 EditorEvent::RestoreRequested { hunks } => {
539 if this.lhs.is_some() {
540 let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
541 if !translated.is_empty() {
542 this.rhs_editor.update(cx, |editor, cx| {
543 editor.restore_diff_hunks(translated, cx);
544 });
545 }
546 }
547 }
548 EditorEvent::OpenExcerptsRequested {
549 selections_by_buffer,
550 split,
551 } => {
552 if this.lhs.is_some() {
553 let translated =
554 translate_lhs_selections_to_rhs(selections_by_buffer, this, cx);
555 if !translated.is_empty() {
556 let workspace = this.workspace.clone();
557 let split = *split;
558 Editor::open_buffers_in_workspace(
559 workspace, translated, split, window, cx,
560 );
561 }
562 }
563 }
564 _ => cx.emit(event.clone()),
565 },
566 )];
567
568 let lhs_focus_handle = lhs_editor.read(cx).focus_handle(cx);
569 subscriptions.push(
570 cx.on_focus_in(&lhs_focus_handle, window, |this, _window, cx| {
571 if let Some(lhs) = &mut this.lhs {
572 if !lhs.was_last_focused {
573 lhs.was_last_focused = true;
574 cx.emit(SearchEvent::MatchesInvalidated);
575 cx.notify();
576 }
577 }
578 }),
579 );
580
581 let rhs_focus_handle = self.rhs_editor.read(cx).focus_handle(cx);
582 subscriptions.push(
583 cx.on_focus_in(&rhs_focus_handle, window, |this, _window, cx| {
584 if let Some(lhs) = &mut this.lhs {
585 if lhs.was_last_focused {
586 lhs.was_last_focused = false;
587 cx.emit(SearchEvent::MatchesInvalidated);
588 cx.notify();
589 }
590 }
591 }),
592 );
593
594 let mut lhs = LhsEditor {
595 editor: lhs_editor,
596 multibuffer: lhs_multibuffer,
597 was_last_focused: false,
598 _subscriptions: subscriptions,
599 };
600 let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
601 let lhs_display_map = lhs.editor.read(cx).display_map.clone();
602 let rhs_display_map_id = rhs_display_map.entity_id();
603
604 self.rhs_editor.update(cx, |editor, cx| {
605 editor.set_delegate_expand_excerpts(true);
606 editor.buffer().update(cx, |rhs_multibuffer, cx| {
607 rhs_multibuffer.set_show_deleted_hunks(false, cx);
608 rhs_multibuffer.set_use_extended_diff_range(true, cx);
609 })
610 });
611
612 let path_diffs: Vec<_> = {
613 let rhs_multibuffer = self.rhs_multibuffer.read(cx);
614 rhs_multibuffer
615 .paths()
616 .filter_map(|path| {
617 let excerpt_id = rhs_multibuffer.excerpts_for_path(path).next()?;
618 let snapshot = rhs_multibuffer.snapshot(cx);
619 let buffer = snapshot.buffer_for_excerpt(excerpt_id)?;
620 let diff = rhs_multibuffer.diff_for(buffer.remote_id())?;
621 Some((path.clone(), diff))
622 })
623 .collect()
624 };
625
626 let rhs_folded_buffers = rhs_display_map.read(cx).folded_buffers().clone();
627
628 let mut companion = Companion::new(
629 rhs_display_map_id,
630 rhs_folded_buffers,
631 convert_rhs_rows_to_lhs,
632 convert_lhs_rows_to_rhs,
633 );
634
635 // stream this
636 for (path, diff) in path_diffs {
637 for (lhs, rhs) in
638 lhs.update_path_excerpts_from_rhs(path, &self.rhs_multibuffer, diff.clone(), cx)
639 {
640 companion.add_excerpt_mapping(lhs, rhs);
641 }
642 companion.add_buffer_mapping(
643 diff.read(cx).base_text(cx).remote_id(),
644 diff.read(cx).buffer_id,
645 );
646 }
647
648 let companion = cx.new(|_| companion);
649
650 rhs_display_map.update(cx, |dm, cx| {
651 dm.set_companion(Some((lhs_display_map.downgrade(), companion.clone())), cx);
652 });
653 lhs_display_map.update(cx, |dm, cx| {
654 dm.set_companion(Some((rhs_display_map.downgrade(), companion)), cx);
655 });
656 rhs_display_map.update(cx, |dm, cx| {
657 dm.sync_custom_blocks_into_companion(cx);
658 });
659
660 let shared_scroll_anchor = self
661 .rhs_editor
662 .read(cx)
663 .scroll_manager
664 .scroll_anchor_entity();
665 lhs.editor.update(cx, |editor, _cx| {
666 editor
667 .scroll_manager
668 .set_shared_scroll_anchor(shared_scroll_anchor);
669 });
670
671 let this = cx.entity().downgrade();
672 self.rhs_editor.update(cx, |editor, _cx| {
673 let this = this.clone();
674 editor.set_on_local_selections_changed(Some(Box::new(
675 move |cursor_position, window, cx| {
676 let this = this.clone();
677 window.defer(cx, move |window, cx| {
678 this.update(cx, |this, cx| {
679 if this.locked_cursors {
680 this.sync_cursor_to_other_side(true, cursor_position, window, cx);
681 }
682 })
683 .ok();
684 })
685 },
686 )));
687 });
688 lhs.editor.update(cx, |editor, _cx| {
689 let this = this.clone();
690 editor.set_on_local_selections_changed(Some(Box::new(
691 move |cursor_position, window, cx| {
692 let this = this.clone();
693 window.defer(cx, move |window, cx| {
694 this.update(cx, |this, cx| {
695 if this.locked_cursors {
696 this.sync_cursor_to_other_side(false, cursor_position, window, cx);
697 }
698 })
699 .ok();
700 })
701 },
702 )));
703 });
704
705 // Copy soft wrap state from rhs (source of truth) to lhs
706 let rhs_soft_wrap_override = self.rhs_editor.read(cx).soft_wrap_mode_override;
707 lhs.editor.update(cx, |editor, cx| {
708 editor.soft_wrap_mode_override = rhs_soft_wrap_override;
709 cx.notify();
710 });
711
712 self.lhs = Some(lhs);
713
714 cx.notify();
715 }
716
717 fn activate_pane_left(
718 &mut self,
719 _: &ActivatePaneLeft,
720 window: &mut Window,
721 cx: &mut Context<Self>,
722 ) {
723 if let Some(lhs) = &self.lhs {
724 if !lhs.was_last_focused {
725 lhs.editor.read(cx).focus_handle(cx).focus(window, cx);
726 lhs.editor.update(cx, |editor, cx| {
727 editor.request_autoscroll(Autoscroll::fit(), cx);
728 });
729 } else {
730 cx.propagate();
731 }
732 } else {
733 cx.propagate();
734 }
735 }
736
737 fn activate_pane_right(
738 &mut self,
739 _: &ActivatePaneRight,
740 window: &mut Window,
741 cx: &mut Context<Self>,
742 ) {
743 if let Some(lhs) = &self.lhs {
744 if lhs.was_last_focused {
745 self.rhs_editor.read(cx).focus_handle(cx).focus(window, cx);
746 self.rhs_editor.update(cx, |editor, cx| {
747 editor.request_autoscroll(Autoscroll::fit(), cx);
748 });
749 } else {
750 cx.propagate();
751 }
752 } else {
753 cx.propagate();
754 }
755 }
756
757 fn toggle_locked_cursors(
758 &mut self,
759 _: &ToggleLockedCursors,
760 _window: &mut Window,
761 cx: &mut Context<Self>,
762 ) {
763 self.locked_cursors = !self.locked_cursors;
764 cx.notify();
765 }
766
767 pub fn locked_cursors(&self) -> bool {
768 self.locked_cursors
769 }
770
771 fn sync_cursor_to_other_side(
772 &mut self,
773 from_rhs: bool,
774 source_point: Point,
775 window: &mut Window,
776 cx: &mut Context<Self>,
777 ) {
778 let Some(lhs) = &self.lhs else {
779 return;
780 };
781
782 let target_editor = if from_rhs {
783 &lhs.editor
784 } else {
785 &self.rhs_editor
786 };
787
788 let (source_multibuffer, target_multibuffer) = if from_rhs {
789 (&self.rhs_multibuffer, &lhs.multibuffer)
790 } else {
791 (&lhs.multibuffer, &self.rhs_multibuffer)
792 };
793
794 let source_snapshot = source_multibuffer.read(cx).snapshot(cx);
795 let target_snapshot = target_multibuffer.read(cx).snapshot(cx);
796
797 let target_range = target_editor.update(cx, |target_editor, cx| {
798 target_editor.display_map.update(cx, |display_map, cx| {
799 let display_map_id = cx.entity_id();
800 display_map.companion().unwrap().update(cx, |companion, _| {
801 companion.convert_point_from_companion(
802 display_map_id,
803 &target_snapshot,
804 &source_snapshot,
805 source_point,
806 )
807 })
808 })
809 });
810
811 target_editor.update(cx, |editor, cx| {
812 editor.set_suppress_selection_callback(true);
813 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
814 s.select_ranges([target_range]);
815 });
816 editor.set_suppress_selection_callback(false);
817 });
818 }
819
820 fn toggle_split(&mut self, _: &ToggleSplitDiff, window: &mut Window, cx: &mut Context<Self>) {
821 if self.lhs.is_some() {
822 self.unsplit(&UnsplitDiff, window, cx);
823 } else {
824 self.split(&SplitDiff, window, cx);
825 }
826 }
827
828 fn intercept_toggle_code_actions(
829 &mut self,
830 _: &ToggleCodeActions,
831 _window: &mut Window,
832 cx: &mut Context<Self>,
833 ) {
834 if self.lhs.is_some() {
835 cx.stop_propagation();
836 } else {
837 cx.propagate();
838 }
839 }
840
841 fn intercept_toggle_breakpoint(
842 &mut self,
843 _: &ToggleBreakpoint,
844 _window: &mut Window,
845 cx: &mut Context<Self>,
846 ) {
847 // Only block breakpoint actions when the left (lhs) editor has focus
848 if let Some(lhs) = &self.lhs {
849 if lhs.was_last_focused {
850 cx.stop_propagation();
851 } else {
852 cx.propagate();
853 }
854 } else {
855 cx.propagate();
856 }
857 }
858
859 fn intercept_enable_breakpoint(
860 &mut self,
861 _: &EnableBreakpoint,
862 _window: &mut Window,
863 cx: &mut Context<Self>,
864 ) {
865 // Only block breakpoint actions when the left (lhs) editor has focus
866 if let Some(lhs) = &self.lhs {
867 if lhs.was_last_focused {
868 cx.stop_propagation();
869 } else {
870 cx.propagate();
871 }
872 } else {
873 cx.propagate();
874 }
875 }
876
877 fn intercept_disable_breakpoint(
878 &mut self,
879 _: &DisableBreakpoint,
880 _window: &mut Window,
881 cx: &mut Context<Self>,
882 ) {
883 // Only block breakpoint actions when the left (lhs) editor has focus
884 if let Some(lhs) = &self.lhs {
885 if lhs.was_last_focused {
886 cx.stop_propagation();
887 } else {
888 cx.propagate();
889 }
890 } else {
891 cx.propagate();
892 }
893 }
894
895 fn intercept_edit_log_breakpoint(
896 &mut self,
897 _: &EditLogBreakpoint,
898 _window: &mut Window,
899 cx: &mut Context<Self>,
900 ) {
901 // Only block breakpoint actions when the left (lhs) editor has focus
902 if let Some(lhs) = &self.lhs {
903 if lhs.was_last_focused {
904 cx.stop_propagation();
905 } else {
906 cx.propagate();
907 }
908 } else {
909 cx.propagate();
910 }
911 }
912
913 fn intercept_inline_assist(
914 &mut self,
915 _: &InlineAssist,
916 _window: &mut Window,
917 cx: &mut Context<Self>,
918 ) {
919 if self.lhs.is_some() {
920 cx.stop_propagation();
921 } else {
922 cx.propagate();
923 }
924 }
925
926 fn toggle_soft_wrap(
927 &mut self,
928 _: &ToggleSoftWrap,
929 window: &mut Window,
930 cx: &mut Context<Self>,
931 ) {
932 if let Some(lhs) = &self.lhs {
933 cx.stop_propagation();
934
935 let is_lhs_focused = lhs.was_last_focused;
936 let (focused_editor, other_editor) = if is_lhs_focused {
937 (&lhs.editor, &self.rhs_editor)
938 } else {
939 (&self.rhs_editor, &lhs.editor)
940 };
941
942 // Toggle the focused editor
943 focused_editor.update(cx, |editor, cx| {
944 editor.toggle_soft_wrap(&ToggleSoftWrap, window, cx);
945 });
946
947 // Copy the soft wrap state from the focused editor to the other editor
948 let soft_wrap_override = focused_editor.read(cx).soft_wrap_mode_override;
949 other_editor.update(cx, |editor, cx| {
950 editor.soft_wrap_mode_override = soft_wrap_override;
951 cx.notify();
952 });
953 } else {
954 cx.propagate();
955 }
956 }
957
958 fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
959 let Some(lhs) = self.lhs.take() else {
960 return;
961 };
962 self.rhs_editor.update(cx, |rhs, cx| {
963 let rhs_snapshot = rhs.display_map.update(cx, |dm, cx| dm.snapshot(cx));
964 let native_anchor = rhs.scroll_manager.native_anchor(&rhs_snapshot, cx);
965 let rhs_display_map_id = rhs_snapshot.display_map_id;
966 rhs.scroll_manager
967 .scroll_anchor_entity()
968 .update(cx, |shared, _| {
969 shared.scroll_anchor = native_anchor;
970 shared.display_map_id = Some(rhs_display_map_id);
971 });
972
973 rhs.set_on_local_selections_changed(None);
974 rhs.set_delegate_expand_excerpts(false);
975 rhs.buffer().update(cx, |buffer, cx| {
976 buffer.set_show_deleted_hunks(true, cx);
977 buffer.set_use_extended_diff_range(false, cx);
978 });
979 rhs.display_map.update(cx, |dm, cx| {
980 dm.set_companion(None, cx);
981 });
982 });
983 lhs.editor.update(cx, |editor, _cx| {
984 editor.set_on_local_selections_changed(None);
985 });
986 cx.notify();
987 }
988
989 pub fn set_excerpts_for_path(
990 &mut self,
991 path: PathKey,
992 buffer: Entity<Buffer>,
993 ranges: impl IntoIterator<Item = Range<Point>> + Clone,
994 context_line_count: u32,
995 diff: Entity<BufferDiff>,
996 cx: &mut Context<Self>,
997 ) -> (Vec<Range<Anchor>>, bool) {
998 let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
999 let lhs_display_map = self
1000 .lhs
1001 .as_ref()
1002 .map(|s| s.editor.read(cx).display_map.clone());
1003
1004 let (anchors, added_a_new_excerpt) =
1005 self.rhs_multibuffer.update(cx, |rhs_multibuffer, cx| {
1006 let (anchors, added_a_new_excerpt) = rhs_multibuffer.set_excerpts_for_path(
1007 path.clone(),
1008 buffer.clone(),
1009 ranges,
1010 context_line_count,
1011 cx,
1012 );
1013 if !anchors.is_empty()
1014 && rhs_multibuffer
1015 .diff_for(buffer.read(cx).remote_id())
1016 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1017 {
1018 rhs_multibuffer.add_diff(diff.clone(), cx);
1019 }
1020 (anchors, added_a_new_excerpt)
1021 });
1022
1023 if let Some(lhs) = &mut self.lhs {
1024 if let Some(lhs_display_map) = &lhs_display_map {
1025 lhs.sync_path_excerpts(
1026 path,
1027 &self.rhs_multibuffer,
1028 diff,
1029 &rhs_display_map,
1030 lhs_display_map,
1031 cx,
1032 );
1033 }
1034 }
1035
1036 (anchors, added_a_new_excerpt)
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 mut corresponding_paths = HashMap::default();
1047 self.rhs_multibuffer.update(cx, |multibuffer, cx| {
1048 let snapshot = multibuffer.snapshot(cx);
1049 if self.lhs.is_some() {
1050 corresponding_paths = excerpt_ids
1051 .clone()
1052 .map(|excerpt_id| {
1053 let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
1054 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
1055 let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
1056 (path, diff)
1057 })
1058 .collect::<HashMap<_, _>>();
1059 }
1060 multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
1061 });
1062
1063 if let Some(lhs) = &mut self.lhs {
1064 let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
1065 let lhs_display_map = lhs.editor.read(cx).display_map.clone();
1066 for (path, diff) in corresponding_paths {
1067 lhs.sync_path_excerpts(
1068 path,
1069 &self.rhs_multibuffer,
1070 diff,
1071 &rhs_display_map,
1072 &lhs_display_map,
1073 cx,
1074 );
1075 }
1076 }
1077 }
1078
1079 pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
1080 self.rhs_multibuffer.update(cx, |buffer, cx| {
1081 buffer.remove_excerpts_for_path(path.clone(), cx)
1082 });
1083 if let Some(lhs) = &self.lhs {
1084 let rhs_display_map = self.rhs_editor.read(cx).display_map.clone();
1085 let lhs_display_map = lhs.editor.read(cx).display_map.clone();
1086 lhs.remove_mappings_for_path(
1087 &path,
1088 &self.rhs_multibuffer,
1089 &rhs_display_map,
1090 &lhs_display_map,
1091 cx,
1092 );
1093 lhs.multibuffer
1094 .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
1095 }
1096 }
1097}
1098
1099#[cfg(test)]
1100impl SplittableEditor {
1101 fn check_invariants(&self, quiesced: bool, cx: &mut App) {
1102 use multi_buffer::MultiBufferRow;
1103 use text::Bias;
1104
1105 use crate::display_map::Block;
1106 use crate::display_map::DisplayRow;
1107
1108 self.debug_print(cx);
1109
1110 let lhs = self.lhs.as_ref().unwrap();
1111 let rhs_excerpts = self.rhs_multibuffer.read(cx).excerpt_ids();
1112 let lhs_excerpts = lhs.multibuffer.read(cx).excerpt_ids();
1113 assert_eq!(
1114 lhs_excerpts.len(),
1115 rhs_excerpts.len(),
1116 "mismatch in excerpt count"
1117 );
1118
1119 if quiesced {
1120 let rhs_snapshot = lhs
1121 .editor
1122 .update(cx, |editor, cx| editor.display_snapshot(cx));
1123 let lhs_snapshot = self
1124 .rhs_editor
1125 .update(cx, |editor, cx| editor.display_snapshot(cx));
1126
1127 let lhs_max_row = lhs_snapshot.max_point().row();
1128 let rhs_max_row = rhs_snapshot.max_point().row();
1129 assert_eq!(lhs_max_row, rhs_max_row, "mismatch in display row count");
1130
1131 let lhs_excerpt_block_rows = lhs_snapshot
1132 .blocks_in_range(DisplayRow(0)..lhs_max_row + 1)
1133 .filter(|(_, block)| {
1134 matches!(
1135 block,
1136 Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1137 )
1138 })
1139 .map(|(row, _)| row)
1140 .collect::<Vec<_>>();
1141 let rhs_excerpt_block_rows = rhs_snapshot
1142 .blocks_in_range(DisplayRow(0)..rhs_max_row + 1)
1143 .filter(|(_, block)| {
1144 matches!(
1145 block,
1146 Block::BufferHeader { .. } | Block::ExcerptBoundary { .. }
1147 )
1148 })
1149 .map(|(row, _)| row)
1150 .collect::<Vec<_>>();
1151 assert_eq!(lhs_excerpt_block_rows, rhs_excerpt_block_rows);
1152
1153 for (lhs_hunk, rhs_hunk) in lhs_snapshot.diff_hunks().zip(rhs_snapshot.diff_hunks()) {
1154 assert_eq!(
1155 lhs_hunk.diff_base_byte_range, rhs_hunk.diff_base_byte_range,
1156 "mismatch in hunks"
1157 );
1158 assert_eq!(
1159 lhs_hunk.status, rhs_hunk.status,
1160 "mismatch in hunk statuses"
1161 );
1162
1163 let (lhs_point, rhs_point) =
1164 if lhs_hunk.row_range.is_empty() || rhs_hunk.row_range.is_empty() {
1165 (
1166 Point::new(lhs_hunk.row_range.end.0, 0),
1167 Point::new(rhs_hunk.row_range.end.0, 0),
1168 )
1169 } else {
1170 (
1171 Point::new(lhs_hunk.row_range.start.0, 0),
1172 Point::new(rhs_hunk.row_range.start.0, 0),
1173 )
1174 };
1175 let lhs_point = lhs_snapshot.point_to_display_point(lhs_point, Bias::Left);
1176 let rhs_point = rhs_snapshot.point_to_display_point(rhs_point, Bias::Left);
1177 assert_eq!(
1178 lhs_point.row(),
1179 rhs_point.row(),
1180 "mismatch in hunk position"
1181 );
1182 }
1183
1184 // Filtering out empty lines is a bit of a hack, to work around a case where
1185 // the base text has a trailing newline but the current text doesn't, or vice versa.
1186 // In this case, we get the additional newline on one side, but that line is not
1187 // marked as added/deleted by rowinfos.
1188 self.check_sides_match(cx, |snapshot| {
1189 snapshot
1190 .buffer_snapshot()
1191 .text()
1192 .split("\n")
1193 .zip(snapshot.buffer_snapshot().row_infos(MultiBufferRow(0)))
1194 .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
1195 .map(|(line, _)| line.to_owned())
1196 .collect::<Vec<_>>()
1197 });
1198 }
1199 }
1200
1201 #[track_caller]
1202 fn check_sides_match<T: std::fmt::Debug + PartialEq>(
1203 &self,
1204 cx: &mut App,
1205 mut extract: impl FnMut(&crate::DisplaySnapshot) -> T,
1206 ) {
1207 let lhs = self.lhs.as_ref().expect("requires split");
1208 let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1209 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1210 });
1211 let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1212 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1213 });
1214
1215 let rhs_t = extract(&rhs_snapshot);
1216 let lhs_t = extract(&lhs_snapshot);
1217
1218 if rhs_t != lhs_t {
1219 self.debug_print(cx);
1220 pretty_assertions::assert_eq!(rhs_t, lhs_t);
1221 }
1222 }
1223
1224 fn debug_print(&self, cx: &mut App) {
1225 use crate::DisplayRow;
1226 use crate::display_map::Block;
1227 use buffer_diff::DiffHunkStatusKind;
1228
1229 assert!(
1230 self.lhs.is_some(),
1231 "debug_print is only useful when lhs editor exists"
1232 );
1233
1234 let lhs = self.lhs.as_ref().unwrap();
1235
1236 // Get terminal width, default to 80 if unavailable
1237 let terminal_width = std::env::var("COLUMNS")
1238 .ok()
1239 .and_then(|s| s.parse::<usize>().ok())
1240 .unwrap_or(80);
1241
1242 // Each side gets half the terminal width minus the separator
1243 let separator = " │ ";
1244 let side_width = (terminal_width - separator.len()) / 2;
1245
1246 // Get display snapshots for both editors
1247 let lhs_snapshot = lhs.editor.update(cx, |editor, cx| {
1248 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1249 });
1250 let rhs_snapshot = self.rhs_editor.update(cx, |editor, cx| {
1251 editor.display_map.update(cx, |map, cx| map.snapshot(cx))
1252 });
1253
1254 let lhs_max_row = lhs_snapshot.max_point().row().0;
1255 let rhs_max_row = rhs_snapshot.max_point().row().0;
1256 let max_row = lhs_max_row.max(rhs_max_row);
1257
1258 // Build a map from display row -> block type string
1259 // Each row of a multi-row block gets an entry with the same block type
1260 // For spacers, the ID is included in brackets
1261 fn build_block_map(
1262 snapshot: &crate::DisplaySnapshot,
1263 max_row: u32,
1264 ) -> std::collections::HashMap<u32, String> {
1265 let mut block_map = std::collections::HashMap::new();
1266 for (start_row, block) in
1267 snapshot.blocks_in_range(DisplayRow(0)..DisplayRow(max_row + 1))
1268 {
1269 let (block_type, height) = match block {
1270 Block::Spacer {
1271 id,
1272 height,
1273 is_below: _,
1274 } => (format!("SPACER[{}]", id.0), *height),
1275 Block::ExcerptBoundary { height, .. } => {
1276 ("EXCERPT_BOUNDARY".to_string(), *height)
1277 }
1278 Block::BufferHeader { height, .. } => ("BUFFER_HEADER".to_string(), *height),
1279 Block::FoldedBuffer { height, .. } => ("FOLDED_BUFFER".to_string(), *height),
1280 Block::Custom(custom) => {
1281 ("CUSTOM_BLOCK".to_string(), custom.height.unwrap_or(1))
1282 }
1283 };
1284 for offset in 0..height {
1285 block_map.insert(start_row.0 + offset, block_type.clone());
1286 }
1287 }
1288 block_map
1289 }
1290
1291 let lhs_blocks = build_block_map(&lhs_snapshot, lhs_max_row);
1292 let rhs_blocks = build_block_map(&rhs_snapshot, rhs_max_row);
1293
1294 fn display_width(s: &str) -> usize {
1295 unicode_width::UnicodeWidthStr::width(s)
1296 }
1297
1298 fn truncate_line(line: &str, max_width: usize) -> String {
1299 let line_width = display_width(line);
1300 if line_width <= max_width {
1301 return line.to_string();
1302 }
1303 if max_width < 9 {
1304 let mut result = String::new();
1305 let mut width = 0;
1306 for c in line.chars() {
1307 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1308 if width + c_width > max_width {
1309 break;
1310 }
1311 result.push(c);
1312 width += c_width;
1313 }
1314 return result;
1315 }
1316 let ellipsis = "...";
1317 let target_prefix_width = 3;
1318 let target_suffix_width = 3;
1319
1320 let mut prefix = String::new();
1321 let mut prefix_width = 0;
1322 for c in line.chars() {
1323 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1324 if prefix_width + c_width > target_prefix_width {
1325 break;
1326 }
1327 prefix.push(c);
1328 prefix_width += c_width;
1329 }
1330
1331 let mut suffix_chars: Vec<char> = Vec::new();
1332 let mut suffix_width = 0;
1333 for c in line.chars().rev() {
1334 let c_width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
1335 if suffix_width + c_width > target_suffix_width {
1336 break;
1337 }
1338 suffix_chars.push(c);
1339 suffix_width += c_width;
1340 }
1341 suffix_chars.reverse();
1342 let suffix: String = suffix_chars.into_iter().collect();
1343
1344 format!("{}{}{}", prefix, ellipsis, suffix)
1345 }
1346
1347 fn pad_to_width(s: &str, target_width: usize) -> String {
1348 let current_width = display_width(s);
1349 if current_width >= target_width {
1350 s.to_string()
1351 } else {
1352 format!("{}{}", s, " ".repeat(target_width - current_width))
1353 }
1354 }
1355
1356 // Helper to format a single row for one side
1357 // Format: "ln# diff bytes(cumul) text" or block info
1358 // Line numbers come from buffer_row in RowInfo (1-indexed for display)
1359 fn format_row(
1360 row: u32,
1361 max_row: u32,
1362 snapshot: &crate::DisplaySnapshot,
1363 blocks: &std::collections::HashMap<u32, String>,
1364 row_infos: &[multi_buffer::RowInfo],
1365 cumulative_bytes: &[usize],
1366 side_width: usize,
1367 ) -> String {
1368 // Get row info if available
1369 let row_info = row_infos.get(row as usize);
1370
1371 // Line number prefix (3 chars + space)
1372 // Use buffer_row from RowInfo, which is None for block rows
1373 let line_prefix = if row > max_row {
1374 " ".to_string()
1375 } else if let Some(buffer_row) = row_info.and_then(|info| info.buffer_row) {
1376 format!("{:>3} ", buffer_row + 1) // 1-indexed for display
1377 } else {
1378 " ".to_string() // block rows have no line number
1379 };
1380 let content_width = side_width.saturating_sub(line_prefix.len());
1381
1382 if row > max_row {
1383 return format!("{}{}", line_prefix, " ".repeat(content_width));
1384 }
1385
1386 // Check if this row is a block row
1387 if let Some(block_type) = blocks.get(&row) {
1388 let block_str = format!("~~~[{}]~~~", block_type);
1389 let formatted = format!("{:^width$}", block_str, width = content_width);
1390 return format!(
1391 "{}{}",
1392 line_prefix,
1393 truncate_line(&formatted, content_width)
1394 );
1395 }
1396
1397 // Get line text
1398 let line_text = snapshot.line(DisplayRow(row));
1399 let line_bytes = line_text.len();
1400
1401 // Diff status marker
1402 let diff_marker = match row_info.and_then(|info| info.diff_status.as_ref()) {
1403 Some(status) => match status.kind {
1404 DiffHunkStatusKind::Added => "+",
1405 DiffHunkStatusKind::Deleted => "-",
1406 DiffHunkStatusKind::Modified => "~",
1407 },
1408 None => " ",
1409 };
1410
1411 // Cumulative bytes
1412 let cumulative = cumulative_bytes.get(row as usize).copied().unwrap_or(0);
1413
1414 // Format: "diff bytes(cumul) text" - use 3 digits for bytes, 4 for cumulative
1415 let info_prefix = format!("{}{:>3}({:>4}) ", diff_marker, line_bytes, cumulative);
1416 let text_width = content_width.saturating_sub(info_prefix.len());
1417 let truncated_text = truncate_line(&line_text, text_width);
1418
1419 let text_part = pad_to_width(&truncated_text, text_width);
1420 format!("{}{}{}", line_prefix, info_prefix, text_part)
1421 }
1422
1423 // Collect row infos for both sides
1424 let lhs_row_infos: Vec<_> = lhs_snapshot
1425 .row_infos(DisplayRow(0))
1426 .take((lhs_max_row + 1) as usize)
1427 .collect();
1428 let rhs_row_infos: Vec<_> = rhs_snapshot
1429 .row_infos(DisplayRow(0))
1430 .take((rhs_max_row + 1) as usize)
1431 .collect();
1432
1433 // Calculate cumulative bytes for each side (only counting non-block rows)
1434 let mut lhs_cumulative = Vec::with_capacity((lhs_max_row + 1) as usize);
1435 let mut cumulative = 0usize;
1436 for row in 0..=lhs_max_row {
1437 if !lhs_blocks.contains_key(&row) {
1438 cumulative += lhs_snapshot.line(DisplayRow(row)).len() + 1; // +1 for newline
1439 }
1440 lhs_cumulative.push(cumulative);
1441 }
1442
1443 let mut rhs_cumulative = Vec::with_capacity((rhs_max_row + 1) as usize);
1444 cumulative = 0;
1445 for row in 0..=rhs_max_row {
1446 if !rhs_blocks.contains_key(&row) {
1447 cumulative += rhs_snapshot.line(DisplayRow(row)).len() + 1;
1448 }
1449 rhs_cumulative.push(cumulative);
1450 }
1451
1452 // Print header
1453 eprintln!();
1454 eprintln!("{}", "═".repeat(terminal_width));
1455 let header_left = format!("{:^width$}", "(LHS)", width = side_width);
1456 let header_right = format!("{:^width$}", "(RHS)", width = side_width);
1457 eprintln!("{}{}{}", header_left, separator, header_right);
1458 eprintln!(
1459 "{:^width$}{}{:^width$}",
1460 "ln# diff len(cum) text",
1461 separator,
1462 "ln# diff len(cum) text",
1463 width = side_width
1464 );
1465 eprintln!("{}", "─".repeat(terminal_width));
1466
1467 // Print each row
1468 for row in 0..=max_row {
1469 let left = format_row(
1470 row,
1471 lhs_max_row,
1472 &lhs_snapshot,
1473 &lhs_blocks,
1474 &lhs_row_infos,
1475 &lhs_cumulative,
1476 side_width,
1477 );
1478 let right = format_row(
1479 row,
1480 rhs_max_row,
1481 &rhs_snapshot,
1482 &rhs_blocks,
1483 &rhs_row_infos,
1484 &rhs_cumulative,
1485 side_width,
1486 );
1487 eprintln!("{}{}{}", left, separator, right);
1488 }
1489
1490 eprintln!("{}", "═".repeat(terminal_width));
1491 eprintln!("Legend: + added, - deleted, ~ modified, ~~~ block/spacer row");
1492 eprintln!();
1493 }
1494
1495 fn randomly_edit_excerpts(
1496 &mut self,
1497 rng: &mut impl rand::Rng,
1498 mutation_count: usize,
1499 cx: &mut Context<Self>,
1500 ) {
1501 use collections::HashSet;
1502 use rand::prelude::*;
1503 use std::env;
1504 use util::RandomCharIter;
1505
1506 let max_buffers = env::var("MAX_BUFFERS")
1507 .map(|i| i.parse().expect("invalid `MAX_BUFFERS` variable"))
1508 .unwrap_or(4);
1509
1510 for _ in 0..mutation_count {
1511 let paths = self
1512 .rhs_multibuffer
1513 .read(cx)
1514 .paths()
1515 .cloned()
1516 .collect::<Vec<_>>();
1517 let excerpt_ids = self.rhs_multibuffer.read(cx).excerpt_ids();
1518
1519 if rng.random_bool(0.2) && !excerpt_ids.is_empty() {
1520 let mut excerpts = HashSet::default();
1521 for _ in 0..rng.random_range(0..excerpt_ids.len()) {
1522 excerpts.extend(excerpt_ids.choose(rng).copied());
1523 }
1524
1525 let line_count = rng.random_range(1..5);
1526
1527 log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
1528
1529 self.expand_excerpts(
1530 excerpts.iter().cloned(),
1531 line_count,
1532 ExpandExcerptDirection::UpAndDown,
1533 cx,
1534 );
1535 continue;
1536 }
1537
1538 if excerpt_ids.is_empty() || (rng.random_bool(0.8) && paths.len() < max_buffers) {
1539 let len = rng.random_range(100..500);
1540 let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
1541 let buffer = cx.new(|cx| Buffer::local(text, cx));
1542 log::info!(
1543 "Creating new buffer {} with text: {:?}",
1544 buffer.read(cx).remote_id(),
1545 buffer.read(cx).text()
1546 );
1547 let buffer_snapshot = buffer.read(cx).snapshot();
1548 let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
1549 // Create some initial diff hunks.
1550 buffer.update(cx, |buffer, cx| {
1551 buffer.randomly_edit(rng, 1, cx);
1552 });
1553 let buffer_snapshot = buffer.read(cx).text_snapshot();
1554 diff.update(cx, |diff, cx| {
1555 diff.recalculate_diff_sync(&buffer_snapshot, cx);
1556 });
1557 let path = PathKey::for_buffer(&buffer, cx);
1558 let ranges = diff.update(cx, |diff, cx| {
1559 diff.snapshot(cx)
1560 .hunks(&buffer_snapshot)
1561 .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
1562 .collect::<Vec<_>>()
1563 });
1564 self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
1565 } else {
1566 log::info!("removing excerpts");
1567 let remove_count = rng.random_range(1..=paths.len());
1568 let paths_to_remove = paths
1569 .choose_multiple(rng, remove_count)
1570 .cloned()
1571 .collect::<Vec<_>>();
1572 for path in paths_to_remove {
1573 self.remove_excerpts_for_path(path.clone(), cx);
1574 }
1575 }
1576 }
1577 }
1578}
1579
1580impl Item for SplittableEditor {
1581 type Event = EditorEvent;
1582
1583 fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1584 self.rhs_editor.read(cx).tab_content_text(detail, cx)
1585 }
1586
1587 fn tab_tooltip_text(&self, cx: &App) -> Option<ui::SharedString> {
1588 self.rhs_editor.read(cx).tab_tooltip_text(cx)
1589 }
1590
1591 fn tab_icon(&self, window: &Window, cx: &App) -> Option<ui::Icon> {
1592 self.rhs_editor.read(cx).tab_icon(window, cx)
1593 }
1594
1595 fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> gpui::AnyElement {
1596 self.rhs_editor.read(cx).tab_content(params, window, cx)
1597 }
1598
1599 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
1600 Editor::to_item_events(event, f)
1601 }
1602
1603 fn for_each_project_item(
1604 &self,
1605 cx: &App,
1606 f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
1607 ) {
1608 self.rhs_editor.read(cx).for_each_project_item(cx, f)
1609 }
1610
1611 fn buffer_kind(&self, cx: &App) -> ItemBufferKind {
1612 self.rhs_editor.read(cx).buffer_kind(cx)
1613 }
1614
1615 fn is_dirty(&self, cx: &App) -> bool {
1616 self.rhs_editor.read(cx).is_dirty(cx)
1617 }
1618
1619 fn has_conflict(&self, cx: &App) -> bool {
1620 self.rhs_editor.read(cx).has_conflict(cx)
1621 }
1622
1623 fn has_deleted_file(&self, cx: &App) -> bool {
1624 self.rhs_editor.read(cx).has_deleted_file(cx)
1625 }
1626
1627 fn capability(&self, cx: &App) -> language::Capability {
1628 self.rhs_editor.read(cx).capability(cx)
1629 }
1630
1631 fn can_save(&self, cx: &App) -> bool {
1632 self.rhs_editor.read(cx).can_save(cx)
1633 }
1634
1635 fn can_save_as(&self, cx: &App) -> bool {
1636 self.rhs_editor.read(cx).can_save_as(cx)
1637 }
1638
1639 fn save(
1640 &mut self,
1641 options: SaveOptions,
1642 project: Entity<Project>,
1643 window: &mut Window,
1644 cx: &mut Context<Self>,
1645 ) -> gpui::Task<anyhow::Result<()>> {
1646 self.rhs_editor
1647 .update(cx, |editor, cx| editor.save(options, project, window, cx))
1648 }
1649
1650 fn save_as(
1651 &mut self,
1652 project: Entity<Project>,
1653 path: project::ProjectPath,
1654 window: &mut Window,
1655 cx: &mut Context<Self>,
1656 ) -> gpui::Task<anyhow::Result<()>> {
1657 self.rhs_editor
1658 .update(cx, |editor, cx| editor.save_as(project, path, window, cx))
1659 }
1660
1661 fn reload(
1662 &mut self,
1663 project: Entity<Project>,
1664 window: &mut Window,
1665 cx: &mut Context<Self>,
1666 ) -> gpui::Task<anyhow::Result<()>> {
1667 self.rhs_editor
1668 .update(cx, |editor, cx| editor.reload(project, window, cx))
1669 }
1670
1671 fn navigate(
1672 &mut self,
1673 data: Arc<dyn std::any::Any + Send>,
1674 window: &mut Window,
1675 cx: &mut Context<Self>,
1676 ) -> bool {
1677 self.last_selected_editor()
1678 .update(cx, |editor, cx| editor.navigate(data, window, cx))
1679 }
1680
1681 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1682 self.last_selected_editor().update(cx, |editor, cx| {
1683 editor.deactivated(window, cx);
1684 });
1685 }
1686
1687 fn added_to_workspace(
1688 &mut self,
1689 workspace: &mut Workspace,
1690 window: &mut Window,
1691 cx: &mut Context<Self>,
1692 ) {
1693 self.workspace = workspace.weak_handle();
1694 self.rhs_editor.update(cx, |rhs_editor, cx| {
1695 rhs_editor.added_to_workspace(workspace, window, cx);
1696 });
1697 if let Some(lhs) = &self.lhs {
1698 lhs.editor.update(cx, |lhs_editor, cx| {
1699 lhs_editor.added_to_workspace(workspace, window, cx);
1700 });
1701 }
1702 }
1703
1704 fn as_searchable(
1705 &self,
1706 handle: &Entity<Self>,
1707 _: &App,
1708 ) -> Option<Box<dyn SearchableItemHandle>> {
1709 Some(Box::new(handle.clone()))
1710 }
1711
1712 fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1713 self.rhs_editor.read(cx).breadcrumb_location(cx)
1714 }
1715
1716 fn breadcrumbs(&self, cx: &App) -> Option<Vec<BreadcrumbText>> {
1717 self.rhs_editor.read(cx).breadcrumbs(cx)
1718 }
1719
1720 fn pixel_position_of_cursor(&self, cx: &App) -> Option<gpui::Point<gpui::Pixels>> {
1721 self.last_selected_editor()
1722 .read(cx)
1723 .pixel_position_of_cursor(cx)
1724 }
1725}
1726
1727impl SearchableItem for SplittableEditor {
1728 type Match = Range<Anchor>;
1729
1730 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1731 self.last_selected_editor().update(cx, |editor, cx| {
1732 editor.clear_matches(window, cx);
1733 });
1734 }
1735
1736 fn update_matches(
1737 &mut self,
1738 matches: &[Self::Match],
1739 active_match_index: Option<usize>,
1740 window: &mut Window,
1741 cx: &mut Context<Self>,
1742 ) {
1743 self.last_selected_editor().update(cx, |editor, cx| {
1744 editor.update_matches(matches, active_match_index, window, cx);
1745 });
1746 }
1747
1748 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1749 self.last_selected_editor()
1750 .update(cx, |editor, cx| editor.query_suggestion(window, cx))
1751 }
1752
1753 fn activate_match(
1754 &mut self,
1755 index: usize,
1756 matches: &[Self::Match],
1757 window: &mut Window,
1758 cx: &mut Context<Self>,
1759 ) {
1760 self.last_selected_editor().update(cx, |editor, cx| {
1761 editor.activate_match(index, matches, window, cx);
1762 });
1763 }
1764
1765 fn select_matches(
1766 &mut self,
1767 matches: &[Self::Match],
1768 window: &mut Window,
1769 cx: &mut Context<Self>,
1770 ) {
1771 self.last_selected_editor().update(cx, |editor, cx| {
1772 editor.select_matches(matches, window, cx);
1773 });
1774 }
1775
1776 fn replace(
1777 &mut self,
1778 identifier: &Self::Match,
1779 query: &project::search::SearchQuery,
1780 window: &mut Window,
1781 cx: &mut Context<Self>,
1782 ) {
1783 self.last_selected_editor().update(cx, |editor, cx| {
1784 editor.replace(identifier, query, window, cx);
1785 });
1786 }
1787
1788 fn find_matches(
1789 &mut self,
1790 query: Arc<project::search::SearchQuery>,
1791 window: &mut Window,
1792 cx: &mut Context<Self>,
1793 ) -> gpui::Task<Vec<Self::Match>> {
1794 self.last_selected_editor()
1795 .update(cx, |editor, cx| editor.find_matches(query, window, cx))
1796 }
1797
1798 fn active_match_index(
1799 &mut self,
1800 direction: workspace::searchable::Direction,
1801 matches: &[Self::Match],
1802 window: &mut Window,
1803 cx: &mut Context<Self>,
1804 ) -> Option<usize> {
1805 self.last_selected_editor().update(cx, |editor, cx| {
1806 editor.active_match_index(direction, matches, window, cx)
1807 })
1808 }
1809}
1810
1811impl EventEmitter<EditorEvent> for SplittableEditor {}
1812impl EventEmitter<SearchEvent> for SplittableEditor {}
1813impl Focusable for SplittableEditor {
1814 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1815 self.last_selected_editor().read(cx).focus_handle(cx)
1816 }
1817}
1818
1819// impl Item for SplittableEditor {
1820// type Event = EditorEvent;
1821
1822// fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
1823// self.rhs_editor().tab_content_text(detail, cx)
1824// }
1825
1826// fn as_searchable(&self, _this: &Entity<Self>, cx: &App) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
1827// Some(Box::new(self.last_selected_editor().clone()))
1828// }
1829// }
1830
1831impl Render for SplittableEditor {
1832 fn render(
1833 &mut self,
1834 _window: &mut ui::Window,
1835 cx: &mut ui::Context<Self>,
1836 ) -> impl ui::IntoElement {
1837 let inner = if self.lhs.is_some() {
1838 let style = self.rhs_editor.read(cx).create_style(cx);
1839 SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
1840 } else {
1841 self.rhs_editor.clone().into_any_element()
1842 };
1843 div()
1844 .id("splittable-editor")
1845 .on_action(cx.listener(Self::split))
1846 .on_action(cx.listener(Self::unsplit))
1847 .on_action(cx.listener(Self::toggle_split))
1848 .on_action(cx.listener(Self::activate_pane_left))
1849 .on_action(cx.listener(Self::activate_pane_right))
1850 .on_action(cx.listener(Self::toggle_locked_cursors))
1851 .on_action(cx.listener(Self::intercept_toggle_code_actions))
1852 .on_action(cx.listener(Self::intercept_toggle_breakpoint))
1853 .on_action(cx.listener(Self::intercept_enable_breakpoint))
1854 .on_action(cx.listener(Self::intercept_disable_breakpoint))
1855 .on_action(cx.listener(Self::intercept_edit_log_breakpoint))
1856 .on_action(cx.listener(Self::intercept_inline_assist))
1857 .capture_action(cx.listener(Self::toggle_soft_wrap))
1858 .size_full()
1859 .child(inner)
1860 }
1861}
1862
1863impl LhsEditor {
1864 fn update_path_excerpts_from_rhs(
1865 &mut self,
1866 path_key: PathKey,
1867 rhs_multibuffer: &Entity<MultiBuffer>,
1868 diff: Entity<BufferDiff>,
1869 cx: &mut App,
1870 ) -> Vec<(ExcerptId, ExcerptId)> {
1871 let rhs_multibuffer_ref = rhs_multibuffer.read(cx);
1872 let rhs_excerpt_ids: Vec<ExcerptId> =
1873 rhs_multibuffer_ref.excerpts_for_path(&path_key).collect();
1874
1875 let Some(excerpt_id) = rhs_multibuffer_ref.excerpts_for_path(&path_key).next() else {
1876 self.multibuffer.update(cx, |multibuffer, cx| {
1877 multibuffer.remove_excerpts_for_path(path_key, cx);
1878 });
1879 return Vec::new();
1880 };
1881
1882 let rhs_multibuffer_snapshot = rhs_multibuffer_ref.snapshot(cx);
1883 let main_buffer = rhs_multibuffer_snapshot
1884 .buffer_for_excerpt(excerpt_id)
1885 .unwrap();
1886 let base_text_buffer = diff.read(cx).base_text_buffer();
1887 let diff_snapshot = diff.read(cx).snapshot(cx);
1888 let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
1889 let new = rhs_multibuffer_ref
1890 .excerpts_for_buffer(main_buffer.remote_id(), cx)
1891 .into_iter()
1892 .map(|(_, excerpt_range)| {
1893 let point_range_to_base_text_point_range = |range: Range<Point>| {
1894 let start = diff_snapshot
1895 .buffer_point_to_base_text_range(
1896 Point::new(range.start.row, 0),
1897 main_buffer,
1898 )
1899 .start;
1900 let end = diff_snapshot
1901 .buffer_point_to_base_text_range(Point::new(range.end.row, 0), main_buffer)
1902 .end;
1903 let end_column = diff_snapshot.base_text().line_len(end.row);
1904 Point::new(start.row, 0)..Point::new(end.row, end_column)
1905 };
1906 let rhs = excerpt_range.primary.to_point(main_buffer);
1907 let context = excerpt_range.context.to_point(main_buffer);
1908 ExcerptRange {
1909 primary: point_range_to_base_text_point_range(rhs),
1910 context: point_range_to_base_text_point_range(context),
1911 }
1912 })
1913 .collect();
1914
1915 self.editor.update(cx, |editor, cx| {
1916 editor.buffer().update(cx, |buffer, cx| {
1917 let (ids, _) = buffer.update_path_excerpts(
1918 path_key.clone(),
1919 base_text_buffer.clone(),
1920 &base_text_buffer_snapshot,
1921 new,
1922 cx,
1923 );
1924 if !ids.is_empty()
1925 && buffer
1926 .diff_for(base_text_buffer.read(cx).remote_id())
1927 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
1928 {
1929 buffer.add_inverted_diff(diff, cx);
1930 }
1931 })
1932 });
1933
1934 let lhs_excerpt_ids: Vec<ExcerptId> = self
1935 .multibuffer
1936 .read(cx)
1937 .excerpts_for_path(&path_key)
1938 .collect();
1939
1940 debug_assert_eq!(rhs_excerpt_ids.len(), lhs_excerpt_ids.len());
1941
1942 lhs_excerpt_ids.into_iter().zip(rhs_excerpt_ids).collect()
1943 }
1944
1945 fn sync_path_excerpts(
1946 &mut self,
1947 path_key: PathKey,
1948 rhs_multibuffer: &Entity<MultiBuffer>,
1949 diff: Entity<BufferDiff>,
1950 rhs_display_map: &Entity<DisplayMap>,
1951 lhs_display_map: &Entity<DisplayMap>,
1952 cx: &mut App,
1953 ) {
1954 self.remove_mappings_for_path(
1955 &path_key,
1956 rhs_multibuffer,
1957 rhs_display_map,
1958 lhs_display_map,
1959 cx,
1960 );
1961
1962 let mappings =
1963 self.update_path_excerpts_from_rhs(path_key, rhs_multibuffer, diff.clone(), cx);
1964
1965 let lhs_buffer_id = diff.read(cx).base_text(cx).remote_id();
1966 let rhs_buffer_id = diff.read(cx).buffer_id;
1967
1968 if let Some(companion) = rhs_display_map.read(cx).companion().cloned() {
1969 companion.update(cx, |c, _| {
1970 for (lhs, rhs) in mappings {
1971 c.add_excerpt_mapping(lhs, rhs);
1972 }
1973 c.add_buffer_mapping(lhs_buffer_id, rhs_buffer_id);
1974 });
1975 }
1976 }
1977
1978 fn remove_mappings_for_path(
1979 &self,
1980 path_key: &PathKey,
1981 rhs_multibuffer: &Entity<MultiBuffer>,
1982 rhs_display_map: &Entity<DisplayMap>,
1983 _lhs_display_map: &Entity<DisplayMap>,
1984 cx: &mut App,
1985 ) {
1986 let rhs_excerpt_ids: Vec<ExcerptId> = rhs_multibuffer
1987 .read(cx)
1988 .excerpts_for_path(path_key)
1989 .collect();
1990 let lhs_excerpt_ids: Vec<ExcerptId> = self
1991 .multibuffer
1992 .read(cx)
1993 .excerpts_for_path(path_key)
1994 .collect();
1995
1996 if let Some(companion) = rhs_display_map.read(cx).companion().cloned() {
1997 companion.update(cx, |c, _| {
1998 c.remove_excerpt_mappings(lhs_excerpt_ids, rhs_excerpt_ids);
1999 });
2000 }
2001 }
2002}
2003
2004#[cfg(test)]
2005mod tests {
2006 use buffer_diff::BufferDiff;
2007 use collections::HashSet;
2008 use fs::FakeFs;
2009 use gpui::Element as _;
2010 use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
2011 use language::language_settings::SoftWrap;
2012 use language::{Buffer, Capability};
2013 use multi_buffer::{MultiBuffer, PathKey};
2014 use pretty_assertions::assert_eq;
2015 use project::Project;
2016 use rand::rngs::StdRng;
2017 use settings::SettingsStore;
2018 use std::sync::Arc;
2019 use ui::{VisualContext as _, div, px};
2020 use workspace::Workspace;
2021
2022 use crate::SplittableEditor;
2023 use crate::display_map::{BlockPlacement, BlockProperties, BlockStyle};
2024 use crate::split::{SplitDiff, UnsplitDiff};
2025 use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
2026
2027 async fn init_test(
2028 cx: &mut gpui::TestAppContext,
2029 soft_wrap: SoftWrap,
2030 ) -> (Entity<SplittableEditor>, &mut VisualTestContext) {
2031 cx.update(|cx| {
2032 let store = SettingsStore::test(cx);
2033 cx.set_global(store);
2034 theme::init(theme::LoadThemes::JustBase, cx);
2035 crate::init(cx);
2036 });
2037 let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
2038 let (workspace, cx) =
2039 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2040 let rhs_multibuffer = cx.new(|cx| {
2041 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
2042 multibuffer.set_all_diff_hunks_expanded(cx);
2043 multibuffer
2044 });
2045 let editor = cx.new_window_entity(|window, cx| {
2046 let mut editor = SplittableEditor::new_unsplit(
2047 rhs_multibuffer.clone(),
2048 project.clone(),
2049 workspace,
2050 window,
2051 cx,
2052 );
2053 editor.split(&Default::default(), window, cx);
2054 editor.rhs_editor.update(cx, |editor, cx| {
2055 editor.set_soft_wrap_mode(soft_wrap, cx);
2056 });
2057 editor
2058 .lhs
2059 .as_ref()
2060 .unwrap()
2061 .editor
2062 .update(cx, |editor, cx| {
2063 editor.set_soft_wrap_mode(soft_wrap, cx);
2064 });
2065 editor
2066 });
2067 (editor, cx)
2068 }
2069
2070 fn buffer_with_diff(
2071 base_text: &str,
2072 current_text: &str,
2073 cx: &mut VisualTestContext,
2074 ) -> (Entity<Buffer>, Entity<BufferDiff>) {
2075 let buffer = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
2076 let diff = cx.new(|cx| {
2077 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
2078 });
2079 (buffer, diff)
2080 }
2081
2082 #[track_caller]
2083 fn assert_split_content(
2084 editor: &Entity<SplittableEditor>,
2085 expected_rhs: String,
2086 expected_lhs: String,
2087 cx: &mut VisualTestContext,
2088 ) {
2089 assert_split_content_with_widths(
2090 editor,
2091 px(3000.0),
2092 px(3000.0),
2093 expected_rhs,
2094 expected_lhs,
2095 cx,
2096 );
2097 }
2098
2099 #[track_caller]
2100 fn assert_split_content_with_widths(
2101 editor: &Entity<SplittableEditor>,
2102 rhs_width: Pixels,
2103 lhs_width: Pixels,
2104 expected_rhs: String,
2105 expected_lhs: String,
2106 cx: &mut VisualTestContext,
2107 ) {
2108 let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
2109 let lhs = editor.lhs.as_ref().expect("should have lhs editor");
2110 (editor.rhs_editor.clone(), lhs.editor.clone())
2111 });
2112
2113 // Make sure both sides learn if the other has soft-wrapped
2114 let _ = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2115 cx.run_until_parked();
2116 let _ = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2117 cx.run_until_parked();
2118
2119 let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, rhs_width, cx);
2120 let lhs_content = editor_content_with_blocks_and_width(&lhs_editor, lhs_width, cx);
2121
2122 if rhs_content != expected_rhs || lhs_content != expected_lhs {
2123 editor.update(cx, |editor, cx| editor.debug_print(cx));
2124 }
2125
2126 assert_eq!(rhs_content, expected_rhs, "rhs");
2127 assert_eq!(lhs_content, expected_lhs, "lhs");
2128 }
2129
2130 #[gpui::test(iterations = 100)]
2131 async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
2132 use rand::prelude::*;
2133
2134 let (editor, cx) = init_test(cx, SoftWrap::EditorWidth).await;
2135 let operations = std::env::var("OPERATIONS")
2136 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
2137 .unwrap_or(10);
2138 let rng = &mut rng;
2139 for _ in 0..operations {
2140 let buffers = editor.update(cx, |editor, cx| {
2141 editor.rhs_editor.read(cx).buffer().read(cx).all_buffers()
2142 });
2143
2144 if buffers.is_empty() {
2145 log::info!("adding excerpts to empty multibuffer");
2146 editor.update(cx, |editor, cx| {
2147 editor.randomly_edit_excerpts(rng, 2, cx);
2148 editor.check_invariants(true, cx);
2149 });
2150 continue;
2151 }
2152
2153 let mut quiesced = false;
2154
2155 match rng.random_range(0..100) {
2156 0..=44 => {
2157 log::info!("randomly editing multibuffer");
2158 editor.update(cx, |editor, cx| {
2159 editor.rhs_multibuffer.update(cx, |multibuffer, cx| {
2160 multibuffer.randomly_edit(rng, 5, cx);
2161 })
2162 })
2163 }
2164 45..=64 => {
2165 log::info!("randomly undoing/redoing in single buffer");
2166 let buffer = buffers.iter().choose(rng).unwrap();
2167 buffer.update(cx, |buffer, cx| {
2168 buffer.randomly_undo_redo(rng, cx);
2169 });
2170 }
2171 65..=79 => {
2172 log::info!("mutating excerpts");
2173 editor.update(cx, |editor, cx| {
2174 editor.randomly_edit_excerpts(rng, 2, cx);
2175 });
2176 }
2177 _ => {
2178 log::info!("quiescing");
2179 for buffer in buffers {
2180 let buffer_snapshot =
2181 buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2182 let diff = editor.update(cx, |editor, cx| {
2183 editor
2184 .rhs_multibuffer
2185 .read(cx)
2186 .diff_for(buffer.read(cx).remote_id())
2187 .unwrap()
2188 });
2189 diff.update(cx, |diff, cx| {
2190 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2191 });
2192 cx.run_until_parked();
2193 let diff_snapshot = diff.read_with(cx, |diff, cx| diff.snapshot(cx));
2194 let ranges = diff_snapshot
2195 .hunks(&buffer_snapshot)
2196 .map(|hunk| hunk.range)
2197 .collect::<Vec<_>>();
2198 editor.update(cx, |editor, cx| {
2199 let path = PathKey::for_buffer(&buffer, cx);
2200 editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
2201 });
2202 }
2203 quiesced = true;
2204 }
2205 }
2206
2207 editor.update(cx, |editor, cx| {
2208 editor.check_invariants(quiesced, cx);
2209 });
2210 }
2211 }
2212
2213 #[gpui::test]
2214 async fn test_basic_alignment(cx: &mut gpui::TestAppContext) {
2215 use rope::Point;
2216 use unindent::Unindent as _;
2217
2218 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2219
2220 let base_text = "
2221 aaa
2222 bbb
2223 ccc
2224 ddd
2225 eee
2226 fff
2227 "
2228 .unindent();
2229 let current_text = "
2230 aaa
2231 ddd
2232 eee
2233 fff
2234 "
2235 .unindent();
2236
2237 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2238
2239 editor.update(cx, |editor, cx| {
2240 let path = PathKey::for_buffer(&buffer, cx);
2241 editor.set_excerpts_for_path(
2242 path,
2243 buffer.clone(),
2244 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2245 0,
2246 diff.clone(),
2247 cx,
2248 );
2249 });
2250
2251 cx.run_until_parked();
2252
2253 assert_split_content(
2254 &editor,
2255 "
2256 § <no file>
2257 § -----
2258 aaa
2259 § spacer
2260 § spacer
2261 ddd
2262 eee
2263 fff"
2264 .unindent(),
2265 "
2266 § <no file>
2267 § -----
2268 aaa
2269 bbb
2270 ccc
2271 ddd
2272 eee
2273 fff"
2274 .unindent(),
2275 &mut cx,
2276 );
2277
2278 buffer.update(cx, |buffer, cx| {
2279 buffer.edit([(Point::new(3, 0)..Point::new(3, 3), "FFF")], None, cx);
2280 });
2281
2282 cx.run_until_parked();
2283
2284 assert_split_content(
2285 &editor,
2286 "
2287 § <no file>
2288 § -----
2289 aaa
2290 § spacer
2291 § spacer
2292 ddd
2293 eee
2294 FFF"
2295 .unindent(),
2296 "
2297 § <no file>
2298 § -----
2299 aaa
2300 bbb
2301 ccc
2302 ddd
2303 eee
2304 fff"
2305 .unindent(),
2306 &mut cx,
2307 );
2308
2309 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2310 diff.update(cx, |diff, cx| {
2311 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2312 });
2313
2314 cx.run_until_parked();
2315
2316 assert_split_content(
2317 &editor,
2318 "
2319 § <no file>
2320 § -----
2321 aaa
2322 § spacer
2323 § spacer
2324 ddd
2325 eee
2326 FFF"
2327 .unindent(),
2328 "
2329 § <no file>
2330 § -----
2331 aaa
2332 bbb
2333 ccc
2334 ddd
2335 eee
2336 fff"
2337 .unindent(),
2338 &mut cx,
2339 );
2340 }
2341
2342 #[gpui::test]
2343 async fn test_deleting_unmodified_lines(cx: &mut gpui::TestAppContext) {
2344 use rope::Point;
2345 use unindent::Unindent as _;
2346
2347 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2348
2349 let base_text1 = "
2350 aaa
2351 bbb
2352 ccc
2353 ddd
2354 eee"
2355 .unindent();
2356
2357 let base_text2 = "
2358 fff
2359 ggg
2360 hhh
2361 iii
2362 jjj"
2363 .unindent();
2364
2365 let (buffer1, diff1) = buffer_with_diff(&base_text1, &base_text1, &mut cx);
2366 let (buffer2, diff2) = buffer_with_diff(&base_text2, &base_text2, &mut cx);
2367
2368 editor.update(cx, |editor, cx| {
2369 let path1 = PathKey::for_buffer(&buffer1, cx);
2370 editor.set_excerpts_for_path(
2371 path1,
2372 buffer1.clone(),
2373 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
2374 0,
2375 diff1.clone(),
2376 cx,
2377 );
2378 let path2 = PathKey::for_buffer(&buffer2, cx);
2379 editor.set_excerpts_for_path(
2380 path2,
2381 buffer2.clone(),
2382 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
2383 1,
2384 diff2.clone(),
2385 cx,
2386 );
2387 });
2388
2389 cx.run_until_parked();
2390
2391 buffer1.update(cx, |buffer, cx| {
2392 buffer.edit(
2393 [
2394 (Point::new(0, 0)..Point::new(1, 0), ""),
2395 (Point::new(3, 0)..Point::new(4, 0), ""),
2396 ],
2397 None,
2398 cx,
2399 );
2400 });
2401 buffer2.update(cx, |buffer, cx| {
2402 buffer.edit(
2403 [
2404 (Point::new(0, 0)..Point::new(1, 0), ""),
2405 (Point::new(3, 0)..Point::new(4, 0), ""),
2406 ],
2407 None,
2408 cx,
2409 );
2410 });
2411
2412 cx.run_until_parked();
2413
2414 assert_split_content(
2415 &editor,
2416 "
2417 § <no file>
2418 § -----
2419 § spacer
2420 bbb
2421 ccc
2422 § spacer
2423 eee
2424 § <no file>
2425 § -----
2426 § spacer
2427 ggg
2428 hhh
2429 § spacer
2430 jjj"
2431 .unindent(),
2432 "
2433 § <no file>
2434 § -----
2435 aaa
2436 bbb
2437 ccc
2438 ddd
2439 eee
2440 § <no file>
2441 § -----
2442 fff
2443 ggg
2444 hhh
2445 iii
2446 jjj"
2447 .unindent(),
2448 &mut cx,
2449 );
2450
2451 let buffer1_snapshot = buffer1.read_with(cx, |buffer, _| buffer.text_snapshot());
2452 diff1.update(cx, |diff, cx| {
2453 diff.recalculate_diff_sync(&buffer1_snapshot, cx);
2454 });
2455 let buffer2_snapshot = buffer2.read_with(cx, |buffer, _| buffer.text_snapshot());
2456 diff2.update(cx, |diff, cx| {
2457 diff.recalculate_diff_sync(&buffer2_snapshot, cx);
2458 });
2459
2460 cx.run_until_parked();
2461
2462 assert_split_content(
2463 &editor,
2464 "
2465 § <no file>
2466 § -----
2467 § spacer
2468 bbb
2469 ccc
2470 § spacer
2471 eee
2472 § <no file>
2473 § -----
2474 § spacer
2475 ggg
2476 hhh
2477 § spacer
2478 jjj"
2479 .unindent(),
2480 "
2481 § <no file>
2482 § -----
2483 aaa
2484 bbb
2485 ccc
2486 ddd
2487 eee
2488 § <no file>
2489 § -----
2490 fff
2491 ggg
2492 hhh
2493 iii
2494 jjj"
2495 .unindent(),
2496 &mut cx,
2497 );
2498 }
2499
2500 #[gpui::test]
2501 async fn test_deleting_added_line(cx: &mut gpui::TestAppContext) {
2502 use rope::Point;
2503 use unindent::Unindent as _;
2504
2505 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2506
2507 let base_text = "
2508 aaa
2509 bbb
2510 ccc
2511 ddd
2512 "
2513 .unindent();
2514
2515 let current_text = "
2516 aaa
2517 NEW1
2518 NEW2
2519 ccc
2520 ddd
2521 "
2522 .unindent();
2523
2524 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2525
2526 editor.update(cx, |editor, cx| {
2527 let path = PathKey::for_buffer(&buffer, cx);
2528 editor.set_excerpts_for_path(
2529 path,
2530 buffer.clone(),
2531 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2532 0,
2533 diff.clone(),
2534 cx,
2535 );
2536 });
2537
2538 cx.run_until_parked();
2539
2540 assert_split_content(
2541 &editor,
2542 "
2543 § <no file>
2544 § -----
2545 aaa
2546 NEW1
2547 NEW2
2548 ccc
2549 ddd"
2550 .unindent(),
2551 "
2552 § <no file>
2553 § -----
2554 aaa
2555 bbb
2556 § spacer
2557 ccc
2558 ddd"
2559 .unindent(),
2560 &mut cx,
2561 );
2562
2563 buffer.update(cx, |buffer, cx| {
2564 buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
2565 });
2566
2567 cx.run_until_parked();
2568
2569 assert_split_content(
2570 &editor,
2571 "
2572 § <no file>
2573 § -----
2574 aaa
2575 NEW1
2576 ccc
2577 ddd"
2578 .unindent(),
2579 "
2580 § <no file>
2581 § -----
2582 aaa
2583 bbb
2584 ccc
2585 ddd"
2586 .unindent(),
2587 &mut cx,
2588 );
2589
2590 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2591 diff.update(cx, |diff, cx| {
2592 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2593 });
2594
2595 cx.run_until_parked();
2596
2597 assert_split_content(
2598 &editor,
2599 "
2600 § <no file>
2601 § -----
2602 aaa
2603 NEW1
2604 ccc
2605 ddd"
2606 .unindent(),
2607 "
2608 § <no file>
2609 § -----
2610 aaa
2611 bbb
2612 ccc
2613 ddd"
2614 .unindent(),
2615 &mut cx,
2616 );
2617 }
2618
2619 #[gpui::test]
2620 async fn test_inserting_consecutive_blank_line(cx: &mut gpui::TestAppContext) {
2621 use rope::Point;
2622 use unindent::Unindent as _;
2623
2624 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2625
2626 let base_text = "
2627 aaa
2628 bbb
2629
2630
2631
2632
2633
2634 ccc
2635 ddd
2636 "
2637 .unindent();
2638 let current_text = "
2639 aaa
2640 bbb
2641
2642
2643
2644
2645
2646 CCC
2647 ddd
2648 "
2649 .unindent();
2650
2651 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2652
2653 editor.update(cx, |editor, cx| {
2654 let path = PathKey::for_buffer(&buffer, cx);
2655 editor.set_excerpts_for_path(
2656 path,
2657 buffer.clone(),
2658 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2659 0,
2660 diff.clone(),
2661 cx,
2662 );
2663 });
2664
2665 cx.run_until_parked();
2666
2667 buffer.update(cx, |buffer, cx| {
2668 buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "\n")], None, cx);
2669 });
2670
2671 cx.run_until_parked();
2672
2673 assert_split_content(
2674 &editor,
2675 "
2676 § <no file>
2677 § -----
2678 aaa
2679 bbb
2680
2681
2682
2683
2684
2685
2686 CCC
2687 ddd"
2688 .unindent(),
2689 "
2690 § <no file>
2691 § -----
2692 aaa
2693 bbb
2694 § spacer
2695
2696
2697
2698
2699
2700 ccc
2701 ddd"
2702 .unindent(),
2703 &mut cx,
2704 );
2705
2706 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2707 diff.update(cx, |diff, cx| {
2708 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2709 });
2710
2711 cx.run_until_parked();
2712
2713 assert_split_content(
2714 &editor,
2715 "
2716 § <no file>
2717 § -----
2718 aaa
2719 bbb
2720
2721
2722
2723
2724
2725
2726 CCC
2727 ddd"
2728 .unindent(),
2729 "
2730 § <no file>
2731 § -----
2732 aaa
2733 bbb
2734
2735
2736
2737
2738
2739 ccc
2740 § spacer
2741 ddd"
2742 .unindent(),
2743 &mut cx,
2744 );
2745 }
2746
2747 #[gpui::test]
2748 async fn test_reverting_deletion_hunk(cx: &mut gpui::TestAppContext) {
2749 use git::Restore;
2750 use rope::Point;
2751 use unindent::Unindent as _;
2752
2753 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2754
2755 let base_text = "
2756 aaa
2757 bbb
2758 ccc
2759 ddd
2760 eee
2761 "
2762 .unindent();
2763 let current_text = "
2764 aaa
2765 ddd
2766 eee
2767 "
2768 .unindent();
2769
2770 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2771
2772 editor.update(cx, |editor, cx| {
2773 let path = PathKey::for_buffer(&buffer, cx);
2774 editor.set_excerpts_for_path(
2775 path,
2776 buffer.clone(),
2777 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2778 0,
2779 diff.clone(),
2780 cx,
2781 );
2782 });
2783
2784 cx.run_until_parked();
2785
2786 assert_split_content(
2787 &editor,
2788 "
2789 § <no file>
2790 § -----
2791 aaa
2792 § spacer
2793 § spacer
2794 ddd
2795 eee"
2796 .unindent(),
2797 "
2798 § <no file>
2799 § -----
2800 aaa
2801 bbb
2802 ccc
2803 ddd
2804 eee"
2805 .unindent(),
2806 &mut cx,
2807 );
2808
2809 let rhs_editor = editor.update(cx, |editor, _cx| editor.rhs_editor.clone());
2810 cx.update_window_entity(&rhs_editor, |editor, window, cx| {
2811 editor.change_selections(crate::SelectionEffects::no_scroll(), window, cx, |s| {
2812 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]);
2813 });
2814 editor.git_restore(&Restore, window, cx);
2815 });
2816
2817 cx.run_until_parked();
2818
2819 assert_split_content(
2820 &editor,
2821 "
2822 § <no file>
2823 § -----
2824 aaa
2825 bbb
2826 ccc
2827 ddd
2828 eee"
2829 .unindent(),
2830 "
2831 § <no file>
2832 § -----
2833 aaa
2834 bbb
2835 ccc
2836 ddd
2837 eee"
2838 .unindent(),
2839 &mut cx,
2840 );
2841
2842 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2843 diff.update(cx, |diff, cx| {
2844 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2845 });
2846
2847 cx.run_until_parked();
2848
2849 assert_split_content(
2850 &editor,
2851 "
2852 § <no file>
2853 § -----
2854 aaa
2855 bbb
2856 ccc
2857 ddd
2858 eee"
2859 .unindent(),
2860 "
2861 § <no file>
2862 § -----
2863 aaa
2864 bbb
2865 ccc
2866 ddd
2867 eee"
2868 .unindent(),
2869 &mut cx,
2870 );
2871 }
2872
2873 #[gpui::test]
2874 async fn test_deleting_added_lines(cx: &mut gpui::TestAppContext) {
2875 use rope::Point;
2876 use unindent::Unindent as _;
2877
2878 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2879
2880 let base_text = "
2881 aaa
2882 old1
2883 old2
2884 old3
2885 old4
2886 zzz
2887 "
2888 .unindent();
2889
2890 let current_text = "
2891 aaa
2892 new1
2893 new2
2894 new3
2895 new4
2896 zzz
2897 "
2898 .unindent();
2899
2900 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
2901
2902 editor.update(cx, |editor, cx| {
2903 let path = PathKey::for_buffer(&buffer, cx);
2904 editor.set_excerpts_for_path(
2905 path,
2906 buffer.clone(),
2907 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
2908 0,
2909 diff.clone(),
2910 cx,
2911 );
2912 });
2913
2914 cx.run_until_parked();
2915
2916 buffer.update(cx, |buffer, cx| {
2917 buffer.edit(
2918 [
2919 (Point::new(2, 0)..Point::new(3, 0), ""),
2920 (Point::new(4, 0)..Point::new(5, 0), ""),
2921 ],
2922 None,
2923 cx,
2924 );
2925 });
2926 cx.run_until_parked();
2927
2928 assert_split_content(
2929 &editor,
2930 "
2931 § <no file>
2932 § -----
2933 aaa
2934 new1
2935 new3
2936 § spacer
2937 § spacer
2938 zzz"
2939 .unindent(),
2940 "
2941 § <no file>
2942 § -----
2943 aaa
2944 old1
2945 old2
2946 old3
2947 old4
2948 zzz"
2949 .unindent(),
2950 &mut cx,
2951 );
2952
2953 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
2954 diff.update(cx, |diff, cx| {
2955 diff.recalculate_diff_sync(&buffer_snapshot, cx);
2956 });
2957
2958 cx.run_until_parked();
2959
2960 assert_split_content(
2961 &editor,
2962 "
2963 § <no file>
2964 § -----
2965 aaa
2966 new1
2967 new3
2968 § spacer
2969 § spacer
2970 zzz"
2971 .unindent(),
2972 "
2973 § <no file>
2974 § -----
2975 aaa
2976 old1
2977 old2
2978 old3
2979 old4
2980 zzz"
2981 .unindent(),
2982 &mut cx,
2983 );
2984 }
2985
2986 #[gpui::test]
2987 async fn test_soft_wrap_at_end_of_excerpt(cx: &mut gpui::TestAppContext) {
2988 use rope::Point;
2989 use unindent::Unindent as _;
2990
2991 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
2992
2993 let text = "aaaa bbbb cccc dddd eeee ffff";
2994
2995 let (buffer1, diff1) = buffer_with_diff(text, text, &mut cx);
2996 let (buffer2, diff2) = buffer_with_diff(text, text, &mut cx);
2997
2998 editor.update(cx, |editor, cx| {
2999 let end = Point::new(0, text.len() as u32);
3000 let path1 = PathKey::for_buffer(&buffer1, cx);
3001 editor.set_excerpts_for_path(
3002 path1,
3003 buffer1.clone(),
3004 vec![Point::new(0, 0)..end],
3005 0,
3006 diff1.clone(),
3007 cx,
3008 );
3009 let path2 = PathKey::for_buffer(&buffer2, cx);
3010 editor.set_excerpts_for_path(
3011 path2,
3012 buffer2.clone(),
3013 vec![Point::new(0, 0)..end],
3014 0,
3015 diff2.clone(),
3016 cx,
3017 );
3018 });
3019
3020 cx.run_until_parked();
3021
3022 assert_split_content_with_widths(
3023 &editor,
3024 px(200.0),
3025 px(400.0),
3026 "
3027 § <no file>
3028 § -----
3029 aaaa bbbb\x20
3030 cccc dddd\x20
3031 eeee ffff
3032 § <no file>
3033 § -----
3034 aaaa bbbb\x20
3035 cccc dddd\x20
3036 eeee ffff"
3037 .unindent(),
3038 "
3039 § <no file>
3040 § -----
3041 aaaa bbbb cccc dddd eeee ffff
3042 § spacer
3043 § spacer
3044 § <no file>
3045 § -----
3046 aaaa bbbb cccc dddd eeee ffff
3047 § spacer
3048 § spacer"
3049 .unindent(),
3050 &mut cx,
3051 );
3052 }
3053
3054 #[gpui::test]
3055 async fn test_soft_wrap_before_modification_hunk(cx: &mut gpui::TestAppContext) {
3056 use rope::Point;
3057 use unindent::Unindent as _;
3058
3059 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3060
3061 let base_text = "
3062 aaaa bbbb cccc dddd eeee ffff
3063 old line one
3064 old line two
3065 "
3066 .unindent();
3067
3068 let current_text = "
3069 aaaa bbbb cccc dddd eeee ffff
3070 new line
3071 "
3072 .unindent();
3073
3074 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3075
3076 editor.update(cx, |editor, cx| {
3077 let path = PathKey::for_buffer(&buffer, cx);
3078 editor.set_excerpts_for_path(
3079 path,
3080 buffer.clone(),
3081 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3082 0,
3083 diff.clone(),
3084 cx,
3085 );
3086 });
3087
3088 cx.run_until_parked();
3089
3090 assert_split_content_with_widths(
3091 &editor,
3092 px(200.0),
3093 px(400.0),
3094 "
3095 § <no file>
3096 § -----
3097 aaaa bbbb\x20
3098 cccc dddd\x20
3099 eeee ffff
3100 new line
3101 § spacer"
3102 .unindent(),
3103 "
3104 § <no file>
3105 § -----
3106 aaaa bbbb cccc dddd eeee ffff
3107 § spacer
3108 § spacer
3109 old line one
3110 old line two"
3111 .unindent(),
3112 &mut cx,
3113 );
3114 }
3115
3116 #[gpui::test]
3117 async fn test_soft_wrap_before_deletion_hunk(cx: &mut gpui::TestAppContext) {
3118 use rope::Point;
3119 use unindent::Unindent as _;
3120
3121 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3122
3123 let base_text = "
3124 aaaa bbbb cccc dddd eeee ffff
3125 deleted line one
3126 deleted line two
3127 after
3128 "
3129 .unindent();
3130
3131 let current_text = "
3132 aaaa bbbb cccc dddd eeee ffff
3133 after
3134 "
3135 .unindent();
3136
3137 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3138
3139 editor.update(cx, |editor, cx| {
3140 let path = PathKey::for_buffer(&buffer, cx);
3141 editor.set_excerpts_for_path(
3142 path,
3143 buffer.clone(),
3144 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3145 0,
3146 diff.clone(),
3147 cx,
3148 );
3149 });
3150
3151 cx.run_until_parked();
3152
3153 assert_split_content_with_widths(
3154 &editor,
3155 px(400.0),
3156 px(200.0),
3157 "
3158 § <no file>
3159 § -----
3160 aaaa bbbb cccc dddd eeee ffff
3161 § spacer
3162 § spacer
3163 § spacer
3164 § spacer
3165 § spacer
3166 § spacer
3167 after"
3168 .unindent(),
3169 "
3170 § <no file>
3171 § -----
3172 aaaa bbbb\x20
3173 cccc dddd\x20
3174 eeee ffff
3175 deleted line\x20
3176 one
3177 deleted line\x20
3178 two
3179 after"
3180 .unindent(),
3181 &mut cx,
3182 );
3183 }
3184
3185 #[gpui::test]
3186 async fn test_soft_wrap_spacer_after_editing_second_line(cx: &mut gpui::TestAppContext) {
3187 use rope::Point;
3188 use unindent::Unindent as _;
3189
3190 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3191
3192 let text = "
3193 aaaa bbbb cccc dddd eeee ffff
3194 short
3195 "
3196 .unindent();
3197
3198 let (buffer, diff) = buffer_with_diff(&text, &text, &mut cx);
3199
3200 editor.update(cx, |editor, cx| {
3201 let path = PathKey::for_buffer(&buffer, cx);
3202 editor.set_excerpts_for_path(
3203 path,
3204 buffer.clone(),
3205 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3206 0,
3207 diff.clone(),
3208 cx,
3209 );
3210 });
3211
3212 cx.run_until_parked();
3213
3214 assert_split_content_with_widths(
3215 &editor,
3216 px(400.0),
3217 px(200.0),
3218 "
3219 § <no file>
3220 § -----
3221 aaaa bbbb cccc dddd eeee ffff
3222 § spacer
3223 § spacer
3224 short"
3225 .unindent(),
3226 "
3227 § <no file>
3228 § -----
3229 aaaa bbbb\x20
3230 cccc dddd\x20
3231 eeee ffff
3232 short"
3233 .unindent(),
3234 &mut cx,
3235 );
3236
3237 buffer.update(cx, |buffer, cx| {
3238 buffer.edit([(Point::new(1, 0)..Point::new(1, 5), "modified")], None, cx);
3239 });
3240
3241 cx.run_until_parked();
3242
3243 assert_split_content_with_widths(
3244 &editor,
3245 px(400.0),
3246 px(200.0),
3247 "
3248 § <no file>
3249 § -----
3250 aaaa bbbb cccc dddd eeee ffff
3251 § spacer
3252 § spacer
3253 modified"
3254 .unindent(),
3255 "
3256 § <no file>
3257 § -----
3258 aaaa bbbb\x20
3259 cccc dddd\x20
3260 eeee ffff
3261 short"
3262 .unindent(),
3263 &mut cx,
3264 );
3265
3266 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3267 diff.update(cx, |diff, cx| {
3268 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3269 });
3270
3271 cx.run_until_parked();
3272
3273 assert_split_content_with_widths(
3274 &editor,
3275 px(400.0),
3276 px(200.0),
3277 "
3278 § <no file>
3279 § -----
3280 aaaa bbbb cccc dddd eeee ffff
3281 § spacer
3282 § spacer
3283 modified"
3284 .unindent(),
3285 "
3286 § <no file>
3287 § -----
3288 aaaa bbbb\x20
3289 cccc dddd\x20
3290 eeee ffff
3291 short"
3292 .unindent(),
3293 &mut cx,
3294 );
3295 }
3296
3297 #[gpui::test]
3298 async fn test_no_base_text(cx: &mut gpui::TestAppContext) {
3299 use rope::Point;
3300 use unindent::Unindent as _;
3301
3302 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3303
3304 let (buffer1, diff1) = buffer_with_diff("xxx\nyyy", "xxx\nyyy", &mut cx);
3305
3306 let current_text = "
3307 aaa
3308 bbb
3309 ccc
3310 "
3311 .unindent();
3312
3313 let buffer2 = cx.new(|cx| Buffer::local(current_text.to_string(), cx));
3314 let diff2 = cx.new(|cx| BufferDiff::new(&buffer2.read(cx).text_snapshot(), cx));
3315
3316 editor.update(cx, |editor, cx| {
3317 let path1 = PathKey::for_buffer(&buffer1, cx);
3318 editor.set_excerpts_for_path(
3319 path1,
3320 buffer1.clone(),
3321 vec![Point::new(0, 0)..buffer1.read(cx).max_point()],
3322 0,
3323 diff1.clone(),
3324 cx,
3325 );
3326
3327 let path2 = PathKey::for_buffer(&buffer2, cx);
3328 editor.set_excerpts_for_path(
3329 path2,
3330 buffer2.clone(),
3331 vec![Point::new(0, 0)..buffer2.read(cx).max_point()],
3332 1,
3333 diff2.clone(),
3334 cx,
3335 );
3336 });
3337
3338 cx.run_until_parked();
3339
3340 assert_split_content(
3341 &editor,
3342 "
3343 § <no file>
3344 § -----
3345 xxx
3346 yyy
3347 § <no file>
3348 § -----
3349 aaa
3350 bbb
3351 ccc"
3352 .unindent(),
3353 "
3354 § <no file>
3355 § -----
3356 xxx
3357 yyy
3358 § <no file>
3359 § -----
3360 § spacer
3361 § spacer
3362 § spacer"
3363 .unindent(),
3364 &mut cx,
3365 );
3366
3367 buffer1.update(cx, |buffer, cx| {
3368 buffer.edit([(Point::new(0, 3)..Point::new(0, 3), "z")], None, cx);
3369 });
3370
3371 cx.run_until_parked();
3372
3373 assert_split_content(
3374 &editor,
3375 "
3376 § <no file>
3377 § -----
3378 xxxz
3379 yyy
3380 § <no file>
3381 § -----
3382 aaa
3383 bbb
3384 ccc"
3385 .unindent(),
3386 "
3387 § <no file>
3388 § -----
3389 xxx
3390 yyy
3391 § <no file>
3392 § -----
3393 § spacer
3394 § spacer
3395 § spacer"
3396 .unindent(),
3397 &mut cx,
3398 );
3399 }
3400
3401 #[gpui::test]
3402 async fn test_deleting_char_in_added_line(cx: &mut gpui::TestAppContext) {
3403 use rope::Point;
3404 use unindent::Unindent as _;
3405
3406 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3407
3408 let base_text = "
3409 aaa
3410 bbb
3411 ccc
3412 "
3413 .unindent();
3414
3415 let current_text = "
3416 NEW1
3417 NEW2
3418 ccc
3419 "
3420 .unindent();
3421
3422 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3423
3424 editor.update(cx, |editor, cx| {
3425 let path = PathKey::for_buffer(&buffer, cx);
3426 editor.set_excerpts_for_path(
3427 path,
3428 buffer.clone(),
3429 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3430 0,
3431 diff.clone(),
3432 cx,
3433 );
3434 });
3435
3436 cx.run_until_parked();
3437
3438 assert_split_content(
3439 &editor,
3440 "
3441 § <no file>
3442 § -----
3443 NEW1
3444 NEW2
3445 ccc"
3446 .unindent(),
3447 "
3448 § <no file>
3449 § -----
3450 aaa
3451 bbb
3452 ccc"
3453 .unindent(),
3454 &mut cx,
3455 );
3456
3457 buffer.update(cx, |buffer, cx| {
3458 buffer.edit([(Point::new(1, 3)..Point::new(1, 4), "")], None, cx);
3459 });
3460
3461 cx.run_until_parked();
3462
3463 assert_split_content(
3464 &editor,
3465 "
3466 § <no file>
3467 § -----
3468 NEW1
3469 NEW
3470 ccc"
3471 .unindent(),
3472 "
3473 § <no file>
3474 § -----
3475 aaa
3476 bbb
3477 ccc"
3478 .unindent(),
3479 &mut cx,
3480 );
3481 }
3482
3483 #[gpui::test]
3484 async fn test_soft_wrap_spacer_before_added_line(cx: &mut gpui::TestAppContext) {
3485 use rope::Point;
3486 use unindent::Unindent as _;
3487
3488 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3489
3490 let base_text = "aaaa bbbb cccc dddd eeee ffff\n";
3491
3492 let current_text = "
3493 aaaa bbbb cccc dddd eeee ffff
3494 added line
3495 "
3496 .unindent();
3497
3498 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3499
3500 editor.update(cx, |editor, cx| {
3501 let path = PathKey::for_buffer(&buffer, cx);
3502 editor.set_excerpts_for_path(
3503 path,
3504 buffer.clone(),
3505 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3506 0,
3507 diff.clone(),
3508 cx,
3509 );
3510 });
3511
3512 cx.run_until_parked();
3513
3514 assert_split_content_with_widths(
3515 &editor,
3516 px(400.0),
3517 px(200.0),
3518 "
3519 § <no file>
3520 § -----
3521 aaaa bbbb cccc dddd eeee ffff
3522 § spacer
3523 § spacer
3524 added line"
3525 .unindent(),
3526 "
3527 § <no file>
3528 § -----
3529 aaaa bbbb\x20
3530 cccc dddd\x20
3531 eeee ffff
3532 § spacer"
3533 .unindent(),
3534 &mut cx,
3535 );
3536
3537 assert_split_content_with_widths(
3538 &editor,
3539 px(200.0),
3540 px(400.0),
3541 "
3542 § <no file>
3543 § -----
3544 aaaa bbbb\x20
3545 cccc dddd\x20
3546 eeee ffff
3547 added line"
3548 .unindent(),
3549 "
3550 § <no file>
3551 § -----
3552 aaaa bbbb cccc dddd eeee ffff
3553 § spacer
3554 § spacer
3555 § spacer"
3556 .unindent(),
3557 &mut cx,
3558 );
3559 }
3560
3561 #[gpui::test]
3562 #[ignore]
3563 async fn test_joining_added_line_with_unmodified_line(cx: &mut gpui::TestAppContext) {
3564 use rope::Point;
3565 use unindent::Unindent as _;
3566
3567 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3568
3569 let base_text = "
3570 aaa
3571 bbb
3572 ccc
3573 ddd
3574 eee
3575 "
3576 .unindent();
3577
3578 let current_text = "
3579 aaa
3580 NEW
3581 eee
3582 "
3583 .unindent();
3584
3585 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3586
3587 editor.update(cx, |editor, cx| {
3588 let path = PathKey::for_buffer(&buffer, cx);
3589 editor.set_excerpts_for_path(
3590 path,
3591 buffer.clone(),
3592 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3593 0,
3594 diff.clone(),
3595 cx,
3596 );
3597 });
3598
3599 cx.run_until_parked();
3600
3601 assert_split_content(
3602 &editor,
3603 "
3604 § <no file>
3605 § -----
3606 aaa
3607 NEW
3608 § spacer
3609 § spacer
3610 eee"
3611 .unindent(),
3612 "
3613 § <no file>
3614 § -----
3615 aaa
3616 bbb
3617 ccc
3618 ddd
3619 eee"
3620 .unindent(),
3621 &mut cx,
3622 );
3623
3624 buffer.update(cx, |buffer, cx| {
3625 buffer.edit([(Point::new(1, 3)..Point::new(2, 0), "")], None, cx);
3626 });
3627
3628 cx.run_until_parked();
3629
3630 assert_split_content(
3631 &editor,
3632 "
3633 § <no file>
3634 § -----
3635 aaa
3636 § spacer
3637 § spacer
3638 § spacer
3639 NEWeee"
3640 .unindent(),
3641 "
3642 § <no file>
3643 § -----
3644 aaa
3645 bbb
3646 ccc
3647 ddd
3648 eee"
3649 .unindent(),
3650 &mut cx,
3651 );
3652
3653 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3654 diff.update(cx, |diff, cx| {
3655 diff.recalculate_diff_sync(&buffer_snapshot, cx);
3656 });
3657
3658 cx.run_until_parked();
3659
3660 assert_split_content(
3661 &editor,
3662 "
3663 § <no file>
3664 § -----
3665 aaa
3666 NEWeee
3667 § spacer
3668 § spacer
3669 § spacer"
3670 .unindent(),
3671 "
3672 § <no file>
3673 § -----
3674 aaa
3675 bbb
3676 ccc
3677 ddd
3678 eee"
3679 .unindent(),
3680 &mut cx,
3681 );
3682 }
3683
3684 #[gpui::test]
3685 async fn test_added_file_at_end(cx: &mut gpui::TestAppContext) {
3686 use rope::Point;
3687 use unindent::Unindent as _;
3688
3689 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3690
3691 let base_text = "";
3692 let current_text = "
3693 aaaa bbbb cccc dddd eeee ffff
3694 bbb
3695 ccc
3696 "
3697 .unindent();
3698
3699 let (buffer, diff) = buffer_with_diff(base_text, ¤t_text, &mut cx);
3700
3701 editor.update(cx, |editor, cx| {
3702 let path = PathKey::for_buffer(&buffer, cx);
3703 editor.set_excerpts_for_path(
3704 path,
3705 buffer.clone(),
3706 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3707 0,
3708 diff.clone(),
3709 cx,
3710 );
3711 });
3712
3713 cx.run_until_parked();
3714
3715 assert_split_content(
3716 &editor,
3717 "
3718 § <no file>
3719 § -----
3720 aaaa bbbb cccc dddd eeee ffff
3721 bbb
3722 ccc"
3723 .unindent(),
3724 "
3725 § <no file>
3726 § -----
3727 § spacer
3728 § spacer
3729 § spacer"
3730 .unindent(),
3731 &mut cx,
3732 );
3733
3734 assert_split_content_with_widths(
3735 &editor,
3736 px(200.0),
3737 px(200.0),
3738 "
3739 § <no file>
3740 § -----
3741 aaaa bbbb\x20
3742 cccc dddd\x20
3743 eeee ffff
3744 bbb
3745 ccc"
3746 .unindent(),
3747 "
3748 § <no file>
3749 § -----
3750 § spacer
3751 § spacer
3752 § spacer
3753 § spacer
3754 § spacer"
3755 .unindent(),
3756 &mut cx,
3757 );
3758 }
3759
3760 #[gpui::test]
3761 async fn test_adding_line_to_addition_hunk(cx: &mut gpui::TestAppContext) {
3762 use rope::Point;
3763 use unindent::Unindent as _;
3764
3765 let (editor, mut cx) = init_test(cx, SoftWrap::EditorWidth).await;
3766
3767 let base_text = "
3768 aaa
3769 bbb
3770 ccc
3771 "
3772 .unindent();
3773
3774 let current_text = "
3775 aaa
3776 bbb
3777 xxx
3778 yyy
3779 ccc
3780 "
3781 .unindent();
3782
3783 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3784
3785 editor.update(cx, |editor, cx| {
3786 let path = PathKey::for_buffer(&buffer, cx);
3787 editor.set_excerpts_for_path(
3788 path,
3789 buffer.clone(),
3790 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3791 0,
3792 diff.clone(),
3793 cx,
3794 );
3795 });
3796
3797 cx.run_until_parked();
3798
3799 assert_split_content(
3800 &editor,
3801 "
3802 § <no file>
3803 § -----
3804 aaa
3805 bbb
3806 xxx
3807 yyy
3808 ccc"
3809 .unindent(),
3810 "
3811 § <no file>
3812 § -----
3813 aaa
3814 bbb
3815 § spacer
3816 § spacer
3817 ccc"
3818 .unindent(),
3819 &mut cx,
3820 );
3821
3822 buffer.update(cx, |buffer, cx| {
3823 buffer.edit([(Point::new(3, 3)..Point::new(3, 3), "\nzzz")], None, cx);
3824 });
3825
3826 cx.run_until_parked();
3827
3828 assert_split_content(
3829 &editor,
3830 "
3831 § <no file>
3832 § -----
3833 aaa
3834 bbb
3835 xxx
3836 yyy
3837 zzz
3838 ccc"
3839 .unindent(),
3840 "
3841 § <no file>
3842 § -----
3843 aaa
3844 bbb
3845 § spacer
3846 § spacer
3847 § spacer
3848 ccc"
3849 .unindent(),
3850 &mut cx,
3851 );
3852 }
3853
3854 #[gpui::test]
3855 async fn test_scrolling(cx: &mut gpui::TestAppContext) {
3856 use crate::test::editor_content_with_blocks_and_size;
3857 use gpui::size;
3858 use rope::Point;
3859
3860 let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
3861
3862 let long_line = "x".repeat(200);
3863 let mut lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
3864 lines[25] = long_line;
3865 let content = lines.join("\n");
3866
3867 let (buffer, diff) = buffer_with_diff(&content, &content, &mut cx);
3868
3869 editor.update(cx, |editor, cx| {
3870 let path = PathKey::for_buffer(&buffer, cx);
3871 editor.set_excerpts_for_path(
3872 path,
3873 buffer.clone(),
3874 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3875 0,
3876 diff.clone(),
3877 cx,
3878 );
3879 });
3880
3881 cx.run_until_parked();
3882
3883 let (rhs_editor, lhs_editor) = editor.update(cx, |editor, _cx| {
3884 let lhs = editor.lhs.as_ref().expect("should have lhs editor");
3885 (editor.rhs_editor.clone(), lhs.editor.clone())
3886 });
3887
3888 rhs_editor.update_in(cx, |e, window, cx| {
3889 e.set_scroll_position(gpui::Point::new(0., 10.), window, cx);
3890 });
3891
3892 let rhs_pos =
3893 rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3894 let lhs_pos =
3895 lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3896 assert_eq!(rhs_pos.y, 10., "RHS should be scrolled to row 10");
3897 assert_eq!(
3898 lhs_pos.y, rhs_pos.y,
3899 "LHS should have same scroll position as RHS after set_scroll_position"
3900 );
3901
3902 let draw_size = size(px(300.), px(300.));
3903
3904 rhs_editor.update_in(cx, |e, window, cx| {
3905 e.change_selections(Some(crate::Autoscroll::fit()).into(), window, cx, |s| {
3906 s.select_ranges([Point::new(25, 150)..Point::new(25, 150)]);
3907 });
3908 });
3909
3910 let _ = editor_content_with_blocks_and_size(&rhs_editor, draw_size, &mut cx);
3911 cx.run_until_parked();
3912 let _ = editor_content_with_blocks_and_size(&lhs_editor, draw_size, &mut cx);
3913 cx.run_until_parked();
3914
3915 let rhs_pos =
3916 rhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3917 let lhs_pos =
3918 lhs_editor.update_in(cx, |e, window, cx| e.snapshot(window, cx).scroll_position());
3919
3920 assert!(
3921 rhs_pos.y > 0.,
3922 "RHS should have scrolled vertically to show cursor at row 25"
3923 );
3924 assert!(
3925 rhs_pos.x > 0.,
3926 "RHS should have scrolled horizontally to show cursor at column 150"
3927 );
3928 assert_eq!(
3929 lhs_pos.y, rhs_pos.y,
3930 "LHS should have same vertical scroll position as RHS after autoscroll"
3931 );
3932 assert_eq!(
3933 lhs_pos.x, rhs_pos.x,
3934 "LHS should have same horizontal scroll position as RHS after autoscroll"
3935 );
3936 }
3937
3938 #[gpui::test]
3939 async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
3940 use rope::Point;
3941 use unindent::Unindent as _;
3942
3943 let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
3944
3945 let base_text = "
3946 bbb
3947 ccc
3948 "
3949 .unindent();
3950 let current_text = "
3951 aaa
3952 bbb
3953 ccc
3954 "
3955 .unindent();
3956
3957 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
3958
3959 editor.update(cx, |editor, cx| {
3960 let path = PathKey::for_buffer(&buffer, cx);
3961 editor.set_excerpts_for_path(
3962 path,
3963 buffer.clone(),
3964 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
3965 0,
3966 diff.clone(),
3967 cx,
3968 );
3969 });
3970
3971 cx.run_until_parked();
3972
3973 assert_split_content(
3974 &editor,
3975 "
3976 § <no file>
3977 § -----
3978 aaa
3979 bbb
3980 ccc"
3981 .unindent(),
3982 "
3983 § <no file>
3984 § -----
3985 § spacer
3986 bbb
3987 ccc"
3988 .unindent(),
3989 &mut cx,
3990 );
3991
3992 let block_ids = editor.update(cx, |splittable_editor, cx| {
3993 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
3994 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
3995 let anchor = snapshot.anchor_before(Point::new(2, 0));
3996 rhs_editor.insert_blocks(
3997 [BlockProperties {
3998 placement: BlockPlacement::Above(anchor),
3999 height: Some(1),
4000 style: BlockStyle::Fixed,
4001 render: Arc::new(|_| div().into_any()),
4002 priority: 0,
4003 }],
4004 None,
4005 cx,
4006 )
4007 })
4008 });
4009
4010 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4011 let lhs_editor =
4012 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4013
4014 cx.update(|_, cx| {
4015 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4016 "custom block".to_string()
4017 });
4018 });
4019
4020 let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
4021 let display_map = lhs_editor.display_map.read(cx);
4022 let companion = display_map.companion().unwrap().read(cx);
4023 let mapping = companion.companion_custom_block_to_custom_block(
4024 rhs_editor.read(cx).display_map.entity_id(),
4025 );
4026 *mapping.get(&block_ids[0]).unwrap()
4027 });
4028
4029 cx.update(|_, cx| {
4030 set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
4031 "custom block".to_string()
4032 });
4033 });
4034
4035 cx.run_until_parked();
4036
4037 assert_split_content(
4038 &editor,
4039 "
4040 § <no file>
4041 § -----
4042 aaa
4043 bbb
4044 § custom block
4045 ccc"
4046 .unindent(),
4047 "
4048 § <no file>
4049 § -----
4050 § spacer
4051 bbb
4052 § custom block
4053 ccc"
4054 .unindent(),
4055 &mut cx,
4056 );
4057
4058 editor.update(cx, |splittable_editor, cx| {
4059 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4060 rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
4061 });
4062 });
4063
4064 cx.run_until_parked();
4065
4066 assert_split_content(
4067 &editor,
4068 "
4069 § <no file>
4070 § -----
4071 aaa
4072 bbb
4073 ccc"
4074 .unindent(),
4075 "
4076 § <no file>
4077 § -----
4078 § spacer
4079 bbb
4080 ccc"
4081 .unindent(),
4082 &mut cx,
4083 );
4084 }
4085
4086 #[gpui::test]
4087 async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
4088 use rope::Point;
4089 use unindent::Unindent as _;
4090
4091 let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
4092
4093 let base_text = "
4094 bbb
4095 ccc
4096 "
4097 .unindent();
4098 let current_text = "
4099 aaa
4100 bbb
4101 ccc
4102 "
4103 .unindent();
4104
4105 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4106
4107 editor.update(cx, |editor, cx| {
4108 let path = PathKey::for_buffer(&buffer, cx);
4109 editor.set_excerpts_for_path(
4110 path,
4111 buffer.clone(),
4112 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4113 0,
4114 diff.clone(),
4115 cx,
4116 );
4117 });
4118
4119 cx.run_until_parked();
4120
4121 assert_split_content(
4122 &editor,
4123 "
4124 § <no file>
4125 § -----
4126 aaa
4127 bbb
4128 ccc"
4129 .unindent(),
4130 "
4131 § <no file>
4132 § -----
4133 § spacer
4134 bbb
4135 ccc"
4136 .unindent(),
4137 &mut cx,
4138 );
4139
4140 let block_ids = editor.update(cx, |splittable_editor, cx| {
4141 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4142 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4143 let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4144 let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4145 rhs_editor.insert_blocks(
4146 [
4147 BlockProperties {
4148 placement: BlockPlacement::Above(anchor1),
4149 height: Some(1),
4150 style: BlockStyle::Fixed,
4151 render: Arc::new(|_| div().into_any()),
4152 priority: 0,
4153 },
4154 BlockProperties {
4155 placement: BlockPlacement::Above(anchor2),
4156 height: Some(1),
4157 style: BlockStyle::Fixed,
4158 render: Arc::new(|_| div().into_any()),
4159 priority: 0,
4160 },
4161 ],
4162 None,
4163 cx,
4164 )
4165 })
4166 });
4167
4168 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4169 let lhs_editor =
4170 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4171
4172 cx.update(|_, cx| {
4173 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4174 "custom block 1".to_string()
4175 });
4176 set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4177 "custom block 2".to_string()
4178 });
4179 });
4180
4181 let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4182 let display_map = lhs_editor.display_map.read(cx);
4183 let companion = display_map.companion().unwrap().read(cx);
4184 let mapping = companion.companion_custom_block_to_custom_block(
4185 rhs_editor.read(cx).display_map.entity_id(),
4186 );
4187 (
4188 *mapping.get(&block_ids[0]).unwrap(),
4189 *mapping.get(&block_ids[1]).unwrap(),
4190 )
4191 });
4192
4193 cx.update(|_, cx| {
4194 set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4195 "custom block 1".to_string()
4196 });
4197 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4198 "custom block 2".to_string()
4199 });
4200 });
4201
4202 cx.run_until_parked();
4203
4204 assert_split_content(
4205 &editor,
4206 "
4207 § <no file>
4208 § -----
4209 aaa
4210 bbb
4211 § custom block 1
4212 ccc
4213 § custom block 2"
4214 .unindent(),
4215 "
4216 § <no file>
4217 § -----
4218 § spacer
4219 bbb
4220 § custom block 1
4221 ccc
4222 § custom block 2"
4223 .unindent(),
4224 &mut cx,
4225 );
4226
4227 editor.update(cx, |splittable_editor, cx| {
4228 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4229 rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
4230 });
4231 });
4232
4233 cx.run_until_parked();
4234
4235 assert_split_content(
4236 &editor,
4237 "
4238 § <no file>
4239 § -----
4240 aaa
4241 bbb
4242 ccc
4243 § custom block 2"
4244 .unindent(),
4245 "
4246 § <no file>
4247 § -----
4248 § spacer
4249 bbb
4250 ccc
4251 § custom block 2"
4252 .unindent(),
4253 &mut cx,
4254 );
4255
4256 editor.update_in(cx, |splittable_editor, window, cx| {
4257 splittable_editor.unsplit(&UnsplitDiff, window, cx);
4258 });
4259
4260 cx.run_until_parked();
4261
4262 editor.update_in(cx, |splittable_editor, window, cx| {
4263 splittable_editor.split(&SplitDiff, window, cx);
4264 });
4265
4266 cx.run_until_parked();
4267
4268 let lhs_editor =
4269 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4270
4271 let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4272 let display_map = lhs_editor.display_map.read(cx);
4273 let companion = display_map.companion().unwrap().read(cx);
4274 let mapping = companion.companion_custom_block_to_custom_block(
4275 rhs_editor.read(cx).display_map.entity_id(),
4276 );
4277 *mapping.get(&block_ids[1]).unwrap()
4278 });
4279
4280 cx.update(|_, cx| {
4281 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4282 "custom block 2".to_string()
4283 });
4284 });
4285
4286 cx.run_until_parked();
4287
4288 assert_split_content(
4289 &editor,
4290 "
4291 § <no file>
4292 § -----
4293 aaa
4294 bbb
4295 ccc
4296 § custom block 2"
4297 .unindent(),
4298 "
4299 § <no file>
4300 § -----
4301 § spacer
4302 bbb
4303 ccc
4304 § custom block 2"
4305 .unindent(),
4306 &mut cx,
4307 );
4308 }
4309
4310 #[gpui::test]
4311 async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
4312 use rope::Point;
4313 use unindent::Unindent as _;
4314
4315 let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
4316
4317 let base_text = "
4318 bbb
4319 ccc
4320 "
4321 .unindent();
4322 let current_text = "
4323 aaa
4324 bbb
4325 ccc
4326 "
4327 .unindent();
4328
4329 let (buffer, diff) = buffer_with_diff(&base_text, ¤t_text, &mut cx);
4330
4331 editor.update(cx, |editor, cx| {
4332 let path = PathKey::for_buffer(&buffer, cx);
4333 editor.set_excerpts_for_path(
4334 path,
4335 buffer.clone(),
4336 vec![Point::new(0, 0)..buffer.read(cx).max_point()],
4337 0,
4338 diff.clone(),
4339 cx,
4340 );
4341 });
4342
4343 cx.run_until_parked();
4344
4345 editor.update_in(cx, |splittable_editor, window, cx| {
4346 splittable_editor.unsplit(&UnsplitDiff, window, cx);
4347 });
4348
4349 cx.run_until_parked();
4350
4351 let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
4352
4353 let block_ids = editor.update(cx, |splittable_editor, cx| {
4354 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4355 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4356 let anchor1 = snapshot.anchor_before(Point::new(2, 0));
4357 let anchor2 = snapshot.anchor_before(Point::new(3, 0));
4358 rhs_editor.insert_blocks(
4359 [
4360 BlockProperties {
4361 placement: BlockPlacement::Above(anchor1),
4362 height: Some(1),
4363 style: BlockStyle::Fixed,
4364 render: Arc::new(|_| div().into_any()),
4365 priority: 0,
4366 },
4367 BlockProperties {
4368 placement: BlockPlacement::Above(anchor2),
4369 height: Some(1),
4370 style: BlockStyle::Fixed,
4371 render: Arc::new(|_| div().into_any()),
4372 priority: 0,
4373 },
4374 ],
4375 None,
4376 cx,
4377 )
4378 })
4379 });
4380
4381 cx.update(|_, cx| {
4382 set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
4383 "custom block 1".to_string()
4384 });
4385 set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
4386 "custom block 2".to_string()
4387 });
4388 });
4389
4390 cx.run_until_parked();
4391
4392 let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
4393 assert_eq!(
4394 rhs_content,
4395 "
4396 § <no file>
4397 § -----
4398 aaa
4399 bbb
4400 § custom block 1
4401 ccc
4402 § custom block 2"
4403 .unindent(),
4404 "rhs content before split"
4405 );
4406
4407 editor.update_in(cx, |splittable_editor, window, cx| {
4408 splittable_editor.split(&SplitDiff, window, cx);
4409 });
4410
4411 cx.run_until_parked();
4412
4413 let lhs_editor =
4414 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4415
4416 let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
4417 let display_map = lhs_editor.display_map.read(cx);
4418 let companion = display_map.companion().unwrap().read(cx);
4419 let mapping = companion.companion_custom_block_to_custom_block(
4420 rhs_editor.read(cx).display_map.entity_id(),
4421 );
4422 (
4423 *mapping.get(&block_ids[0]).unwrap(),
4424 *mapping.get(&block_ids[1]).unwrap(),
4425 )
4426 });
4427
4428 cx.update(|_, cx| {
4429 set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
4430 "custom block 1".to_string()
4431 });
4432 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4433 "custom block 2".to_string()
4434 });
4435 });
4436
4437 cx.run_until_parked();
4438
4439 assert_split_content(
4440 &editor,
4441 "
4442 § <no file>
4443 § -----
4444 aaa
4445 bbb
4446 § custom block 1
4447 ccc
4448 § custom block 2"
4449 .unindent(),
4450 "
4451 § <no file>
4452 § -----
4453 § spacer
4454 bbb
4455 § custom block 1
4456 ccc
4457 § custom block 2"
4458 .unindent(),
4459 &mut cx,
4460 );
4461
4462 editor.update(cx, |splittable_editor, cx| {
4463 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4464 rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
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 ccc
4478 § custom block 2"
4479 .unindent(),
4480 "
4481 § <no file>
4482 § -----
4483 § spacer
4484 bbb
4485 ccc
4486 § custom block 2"
4487 .unindent(),
4488 &mut cx,
4489 );
4490
4491 editor.update_in(cx, |splittable_editor, window, cx| {
4492 splittable_editor.unsplit(&UnsplitDiff, window, cx);
4493 });
4494
4495 cx.run_until_parked();
4496
4497 editor.update_in(cx, |splittable_editor, window, cx| {
4498 splittable_editor.split(&SplitDiff, window, cx);
4499 });
4500
4501 cx.run_until_parked();
4502
4503 let lhs_editor =
4504 editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
4505
4506 let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4507 let display_map = lhs_editor.display_map.read(cx);
4508 let companion = display_map.companion().unwrap().read(cx);
4509 let mapping = companion.companion_custom_block_to_custom_block(
4510 rhs_editor.read(cx).display_map.entity_id(),
4511 );
4512 *mapping.get(&block_ids[1]).unwrap()
4513 });
4514
4515 cx.update(|_, cx| {
4516 set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
4517 "custom block 2".to_string()
4518 });
4519 });
4520
4521 cx.run_until_parked();
4522
4523 assert_split_content(
4524 &editor,
4525 "
4526 § <no file>
4527 § -----
4528 aaa
4529 bbb
4530 ccc
4531 § custom block 2"
4532 .unindent(),
4533 "
4534 § <no file>
4535 § -----
4536 § spacer
4537 bbb
4538 ccc
4539 § custom block 2"
4540 .unindent(),
4541 &mut cx,
4542 );
4543
4544 let new_block_ids = editor.update(cx, |splittable_editor, cx| {
4545 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4546 let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
4547 let anchor = snapshot.anchor_before(Point::new(2, 0));
4548 rhs_editor.insert_blocks(
4549 [BlockProperties {
4550 placement: BlockPlacement::Above(anchor),
4551 height: Some(1),
4552 style: BlockStyle::Fixed,
4553 render: Arc::new(|_| div().into_any()),
4554 priority: 0,
4555 }],
4556 None,
4557 cx,
4558 )
4559 })
4560 });
4561
4562 cx.update(|_, cx| {
4563 set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
4564 "custom block 3".to_string()
4565 });
4566 });
4567
4568 let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
4569 let display_map = lhs_editor.display_map.read(cx);
4570 let companion = display_map.companion().unwrap().read(cx);
4571 let mapping = companion.companion_custom_block_to_custom_block(
4572 rhs_editor.read(cx).display_map.entity_id(),
4573 );
4574 *mapping.get(&new_block_ids[0]).unwrap()
4575 });
4576
4577 cx.update(|_, cx| {
4578 set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
4579 "custom block 3".to_string()
4580 });
4581 });
4582
4583 cx.run_until_parked();
4584
4585 assert_split_content(
4586 &editor,
4587 "
4588 § <no file>
4589 § -----
4590 aaa
4591 bbb
4592 § custom block 3
4593 ccc
4594 § custom block 2"
4595 .unindent(),
4596 "
4597 § <no file>
4598 § -----
4599 § spacer
4600 bbb
4601 § custom block 3
4602 ccc
4603 § custom block 2"
4604 .unindent(),
4605 &mut cx,
4606 );
4607
4608 editor.update(cx, |splittable_editor, cx| {
4609 splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
4610 rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
4611 });
4612 });
4613
4614 cx.run_until_parked();
4615
4616 assert_split_content(
4617 &editor,
4618 "
4619 § <no file>
4620 § -----
4621 aaa
4622 bbb
4623 ccc
4624 § custom block 2"
4625 .unindent(),
4626 "
4627 § <no file>
4628 § -----
4629 § spacer
4630 bbb
4631 ccc
4632 § custom block 2"
4633 .unindent(),
4634 &mut cx,
4635 );
4636 }
4637}