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