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