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