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