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