1use std::ops::Range;
2
3use buffer_diff::BufferDiff;
4use collections::HashMap;
5use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
6use gpui::{
7 Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
8};
9use language::{Buffer, Capability};
10use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, PathKey};
11use project::Project;
12use rope::Point;
13use text::{Bias, OffsetRangeExt as _};
14use ui::{
15 App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
16 Styled as _, Window, div,
17};
18use workspace::{
19 ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
20};
21
22use crate::{Editor, EditorEvent};
23
24struct SplitDiffFeatureFlag;
25
26impl FeatureFlag for SplitDiffFeatureFlag {
27 const NAME: &'static str = "split-diff";
28
29 fn enabled_for_staff() -> bool {
30 true
31 }
32}
33
34#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
35#[action(namespace = editor)]
36struct SplitDiff;
37
38#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
39#[action(namespace = editor)]
40struct UnsplitDiff;
41
42pub struct SplittableEditor {
43 primary_multibuffer: Entity<MultiBuffer>,
44 primary_editor: Entity<Editor>,
45 secondary: Option<SecondaryEditor>,
46 panes: PaneGroup,
47 workspace: WeakEntity<Workspace>,
48 _subscriptions: Vec<Subscription>,
49}
50
51struct SecondaryEditor {
52 multibuffer: Entity<MultiBuffer>,
53 editor: Entity<Editor>,
54 pane: Entity<Pane>,
55 has_latest_selection: bool,
56 primary_to_secondary: HashMap<ExcerptId, ExcerptId>,
57 secondary_to_primary: HashMap<ExcerptId, ExcerptId>,
58 _subscriptions: Vec<Subscription>,
59}
60
61impl SplittableEditor {
62 pub fn primary_editor(&self) -> &Entity<Editor> {
63 &self.primary_editor
64 }
65
66 pub fn last_selected_editor(&self) -> &Entity<Editor> {
67 if let Some(secondary) = &self.secondary
68 && secondary.has_latest_selection
69 {
70 &secondary.editor
71 } else {
72 &self.primary_editor
73 }
74 }
75
76 pub fn new_unsplit(
77 primary_multibuffer: Entity<MultiBuffer>,
78 project: Entity<Project>,
79 workspace: Entity<Workspace>,
80 window: &mut Window,
81 cx: &mut Context<Self>,
82 ) -> Self {
83 let primary_editor = cx.new(|cx| {
84 let mut editor = Editor::for_multibuffer(
85 primary_multibuffer.clone(),
86 Some(project.clone()),
87 window,
88 cx,
89 );
90 editor.set_expand_all_diff_hunks(cx);
91 editor
92 });
93 let pane = cx.new(|cx| {
94 let mut pane = Pane::new(
95 workspace.downgrade(),
96 project,
97 Default::default(),
98 None,
99 NoAction.boxed_clone(),
100 true,
101 window,
102 cx,
103 );
104 pane.set_should_display_tab_bar(|_, _| false);
105 pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
106 pane
107 });
108 let panes = PaneGroup::new(pane);
109 // TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
110 let subscriptions = vec![cx.subscribe(
111 &primary_editor,
112 |this, _, event: &EditorEvent, cx| match event {
113 EditorEvent::ExpandExcerptsRequested {
114 excerpt_ids,
115 lines,
116 direction,
117 } => {
118 this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
119 }
120 EditorEvent::SelectionsChanged { .. } => {
121 if let Some(secondary) = &mut this.secondary {
122 secondary.has_latest_selection = false;
123 }
124 cx.emit(event.clone());
125 }
126 _ => cx.emit(event.clone()),
127 },
128 )];
129
130 window.defer(cx, {
131 let workspace = workspace.downgrade();
132 let primary_editor = primary_editor.downgrade();
133 move |window, cx| {
134 workspace
135 .update(cx, |workspace, cx| {
136 primary_editor.update(cx, |editor, cx| {
137 editor.added_to_workspace(workspace, window, cx);
138 })
139 })
140 .ok();
141 }
142 });
143 Self {
144 primary_editor,
145 primary_multibuffer,
146 secondary: None,
147 panes,
148 workspace: workspace.downgrade(),
149 _subscriptions: subscriptions,
150 }
151 }
152
153 fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
154 if !cx.has_flag::<SplitDiffFeatureFlag>() {
155 return;
156 }
157 if self.secondary.is_some() {
158 return;
159 }
160 let Some(workspace) = self.workspace.upgrade() else {
161 return;
162 };
163 let project = workspace.read(cx).project().clone();
164
165 let secondary_multibuffer = cx.new(|cx| {
166 let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
167 multibuffer.set_all_diff_hunks_expanded(cx);
168 multibuffer
169 });
170 let secondary_editor = cx.new(|cx| {
171 let mut editor = Editor::for_multibuffer(
172 secondary_multibuffer.clone(),
173 Some(project.clone()),
174 window,
175 cx,
176 );
177 editor.number_deleted_lines = true;
178 editor.set_delegate_expand_excerpts(true);
179 editor
180 });
181 let secondary_pane = cx.new(|cx| {
182 let mut pane = Pane::new(
183 workspace.downgrade(),
184 workspace.read(cx).project().clone(),
185 Default::default(),
186 None,
187 NoAction.boxed_clone(),
188 true,
189 window,
190 cx,
191 );
192 pane.set_should_display_tab_bar(|_, _| false);
193 pane.add_item(
194 ItemHandle::boxed_clone(&secondary_editor),
195 false,
196 false,
197 None,
198 window,
199 cx,
200 );
201 pane
202 });
203
204 let subscriptions = vec![cx.subscribe(
205 &secondary_editor,
206 |this, _, event: &EditorEvent, cx| match event {
207 EditorEvent::ExpandExcerptsRequested {
208 excerpt_ids,
209 lines,
210 direction,
211 } => {
212 if let Some(secondary) = &this.secondary {
213 let primary_ids: Vec<_> = excerpt_ids
214 .iter()
215 .filter_map(|id| secondary.secondary_to_primary.get(id).copied())
216 .collect();
217 this.expand_excerpts(primary_ids.into_iter(), *lines, *direction, cx);
218 }
219 }
220 EditorEvent::SelectionsChanged { .. } => {
221 if let Some(secondary) = &mut this.secondary {
222 secondary.has_latest_selection = true;
223 }
224 cx.emit(event.clone());
225 }
226 _ => cx.emit(event.clone()),
227 },
228 )];
229 let mut secondary = SecondaryEditor {
230 editor: secondary_editor,
231 multibuffer: secondary_multibuffer,
232 pane: secondary_pane.clone(),
233 has_latest_selection: false,
234 primary_to_secondary: HashMap::default(),
235 secondary_to_primary: HashMap::default(),
236 _subscriptions: subscriptions,
237 };
238 self.primary_editor.update(cx, |editor, cx| {
239 editor.set_delegate_expand_excerpts(true);
240 editor.buffer().update(cx, |primary_multibuffer, cx| {
241 primary_multibuffer.set_show_deleted_hunks(false, cx);
242 let paths = primary_multibuffer.paths().cloned().collect::<Vec<_>>();
243 for path in paths {
244 let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
245 else {
246 continue;
247 };
248 let snapshot = primary_multibuffer.snapshot(cx);
249 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
250 let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
251 secondary.sync_path_excerpts(path.clone(), primary_multibuffer, diff, cx);
252 }
253 })
254 });
255 self.secondary = Some(secondary);
256
257 let primary_pane = self.panes.first_pane();
258 self.panes
259 .split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
260 .unwrap();
261 cx.notify();
262 }
263
264 fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
265 let Some(secondary) = self.secondary.take() else {
266 return;
267 };
268 self.panes.remove(&secondary.pane, cx).unwrap();
269 self.primary_editor.update(cx, |primary, cx| {
270 primary.set_delegate_expand_excerpts(false);
271 primary.buffer().update(cx, |buffer, cx| {
272 buffer.set_show_deleted_hunks(true, cx);
273 });
274 });
275 cx.notify();
276 }
277
278 pub fn added_to_workspace(
279 &mut self,
280 workspace: &mut Workspace,
281 window: &mut Window,
282 cx: &mut Context<Self>,
283 ) {
284 self.workspace = workspace.weak_handle();
285 self.primary_editor.update(cx, |primary_editor, cx| {
286 primary_editor.added_to_workspace(workspace, window, cx);
287 });
288 if let Some(secondary) = &self.secondary {
289 secondary.editor.update(cx, |secondary_editor, cx| {
290 secondary_editor.added_to_workspace(workspace, window, cx);
291 });
292 }
293 }
294
295 pub fn set_excerpts_for_path(
296 &mut self,
297 path: PathKey,
298 buffer: Entity<Buffer>,
299 ranges: impl IntoIterator<Item = Range<Point>> + Clone,
300 context_line_count: u32,
301 diff: Entity<BufferDiff>,
302 cx: &mut Context<Self>,
303 ) -> (Vec<Range<Anchor>>, bool) {
304 self.primary_multibuffer
305 .update(cx, |primary_multibuffer, cx| {
306 let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
307 path.clone(),
308 buffer.clone(),
309 ranges,
310 context_line_count,
311 cx,
312 );
313 if !anchors.is_empty()
314 && primary_multibuffer
315 .diff_for(buffer.read(cx).remote_id())
316 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
317 {
318 primary_multibuffer.add_diff(diff.clone(), cx);
319 }
320 if let Some(secondary) = &mut self.secondary {
321 secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
322 }
323 (anchors, added_a_new_excerpt)
324 })
325 }
326
327 fn expand_excerpts(
328 &mut self,
329 excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
330 lines: u32,
331 direction: ExpandExcerptDirection,
332 cx: &mut Context<Self>,
333 ) {
334 let mut corresponding_paths = HashMap::default();
335 self.primary_multibuffer.update(cx, |multibuffer, cx| {
336 let snapshot = multibuffer.snapshot(cx);
337 if self.secondary.is_some() {
338 corresponding_paths = excerpt_ids
339 .clone()
340 .map(|excerpt_id| {
341 let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
342 let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
343 let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
344 (path, diff)
345 })
346 .collect::<HashMap<_, _>>();
347 }
348 multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
349 });
350
351 if let Some(secondary) = &mut self.secondary {
352 self.primary_multibuffer.update(cx, |multibuffer, cx| {
353 for (path, diff) in corresponding_paths {
354 secondary.sync_path_excerpts(path, multibuffer, diff, cx);
355 }
356 })
357 }
358 }
359
360 pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
361 self.primary_multibuffer.update(cx, |buffer, cx| {
362 buffer.remove_excerpts_for_path(path.clone(), cx)
363 });
364 if let Some(secondary) = &mut self.secondary {
365 secondary.remove_mappings_for_path(&path, cx);
366 secondary
367 .multibuffer
368 .update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
369 }
370 }
371}
372
373#[cfg(test)]
374impl SplittableEditor {
375 fn check_invariants(&self, quiesced: bool, cx: &App) {
376 use buffer_diff::DiffHunkStatusKind;
377 use collections::HashSet;
378 use multi_buffer::MultiBufferOffset;
379 use multi_buffer::MultiBufferRow;
380 use multi_buffer::MultiBufferSnapshot;
381
382 fn format_diff(snapshot: &MultiBufferSnapshot) -> String {
383 let text = snapshot.text();
384 let row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
385 let boundary_rows = snapshot
386 .excerpt_boundaries_in_range(MultiBufferOffset(0)..)
387 .map(|b| b.row)
388 .collect::<HashSet<_>>();
389
390 text.split('\n')
391 .enumerate()
392 .zip(row_infos)
393 .map(|((ix, line), info)| {
394 let marker = match info.diff_status.map(|status| status.kind) {
395 Some(DiffHunkStatusKind::Added) => "+ ",
396 Some(DiffHunkStatusKind::Deleted) => "- ",
397 Some(DiffHunkStatusKind::Modified) => unreachable!(),
398 None => {
399 if !line.is_empty() {
400 " "
401 } else {
402 ""
403 }
404 }
405 };
406 let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {
407 " ----------\n"
408 } else {
409 ""
410 };
411 let expand = info
412 .expand_info
413 .map(|expand_info| match expand_info.direction {
414 ExpandExcerptDirection::Up => " [↑]",
415 ExpandExcerptDirection::Down => " [↓]",
416 ExpandExcerptDirection::UpAndDown => " [↕]",
417 })
418 .unwrap_or_default();
419
420 format!("{boundary_row}{marker}{line}{expand}")
421 })
422 .collect::<Vec<_>>()
423 .join("\n")
424 }
425
426 let Some(secondary) = &self.secondary else {
427 return;
428 };
429
430 log::info!(
431 "primary:\n\n{}",
432 format_diff(&self.primary_multibuffer.read(cx).snapshot(cx))
433 );
434
435 log::info!(
436 "secondary:\n\n{}",
437 format_diff(&secondary.multibuffer.read(cx).snapshot(cx))
438 );
439
440 let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
441 let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
442 assert_eq!(primary_excerpts.len(), secondary_excerpts.len());
443
444 assert_eq!(
445 secondary.primary_to_secondary.len(),
446 primary_excerpts.len(),
447 "primary_to_secondary mapping count should match excerpt count"
448 );
449 assert_eq!(
450 secondary.secondary_to_primary.len(),
451 secondary_excerpts.len(),
452 "secondary_to_primary mapping count should match excerpt count"
453 );
454
455 for primary_id in &primary_excerpts {
456 assert!(
457 secondary.primary_to_secondary.contains_key(primary_id),
458 "primary excerpt {:?} should have a mapping to secondary",
459 primary_id
460 );
461 }
462 for secondary_id in &secondary_excerpts {
463 assert!(
464 secondary.secondary_to_primary.contains_key(secondary_id),
465 "secondary excerpt {:?} should have a mapping to primary",
466 secondary_id
467 );
468 }
469
470 for (primary_id, secondary_id) in &secondary.primary_to_secondary {
471 assert_eq!(
472 secondary.secondary_to_primary.get(secondary_id),
473 Some(primary_id),
474 "mappings should be bijective"
475 );
476 }
477
478 if quiesced {
479 let primary_snapshot = self.primary_multibuffer.read(cx).snapshot(cx);
480 let secondary_snapshot = secondary.multibuffer.read(cx).snapshot(cx);
481 let primary_diff_hunks = primary_snapshot
482 .diff_hunks()
483 .map(|hunk| hunk.diff_base_byte_range)
484 .collect::<Vec<_>>();
485 let secondary_diff_hunks = secondary_snapshot
486 .diff_hunks()
487 .map(|hunk| hunk.diff_base_byte_range)
488 .collect::<Vec<_>>();
489 pretty_assertions::assert_eq!(primary_diff_hunks, secondary_diff_hunks);
490
491 // Filtering out empty lines is a bit of a hack, to work around a case where
492 // the base text has a trailing newline but the current text doesn't, or vice versa.
493 // In this case, we get the additional newline on one side, but that line is not
494 // marked as added/deleted by rowinfos.
495 let primary_unmodified_rows = primary_snapshot
496 .text()
497 .split("\n")
498 .zip(primary_snapshot.row_infos(MultiBufferRow(0)))
499 .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
500 .map(|(line, _)| line.to_owned())
501 .collect::<Vec<_>>();
502 let secondary_unmodified_rows = secondary_snapshot
503 .text()
504 .split("\n")
505 .zip(secondary_snapshot.row_infos(MultiBufferRow(0)))
506 .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
507 .map(|(line, _)| line.to_owned())
508 .collect::<Vec<_>>();
509 pretty_assertions::assert_eq!(primary_unmodified_rows, secondary_unmodified_rows);
510 }
511 }
512
513 fn randomly_edit_excerpts(
514 &mut self,
515 rng: &mut impl rand::Rng,
516 mutation_count: usize,
517 cx: &mut Context<Self>,
518 ) {
519 use collections::HashSet;
520 use rand::prelude::*;
521 use std::env;
522 use util::RandomCharIter;
523
524 let max_excerpts = env::var("MAX_EXCERPTS")
525 .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
526 .unwrap_or(5);
527
528 for _ in 0..mutation_count {
529 let paths = self
530 .primary_multibuffer
531 .read(cx)
532 .paths()
533 .cloned()
534 .collect::<Vec<_>>();
535 let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
536
537 if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
538 let mut excerpts = HashSet::default();
539 for _ in 0..rng.random_range(0..excerpt_ids.len()) {
540 excerpts.extend(excerpt_ids.choose(rng).copied());
541 }
542
543 let line_count = rng.random_range(0..5);
544
545 log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
546
547 self.expand_excerpts(
548 excerpts.iter().cloned(),
549 line_count,
550 ExpandExcerptDirection::UpAndDown,
551 cx,
552 );
553 continue;
554 }
555
556 if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
557 let len = rng.random_range(100..500);
558 let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
559 let buffer = cx.new(|cx| Buffer::local(text, cx));
560 log::info!(
561 "Creating new buffer {} with text: {:?}",
562 buffer.read(cx).remote_id(),
563 buffer.read(cx).text()
564 );
565 let buffer_snapshot = buffer.read(cx).snapshot();
566 let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
567 // Create some initial diff hunks.
568 buffer.update(cx, |buffer, cx| {
569 buffer.randomly_edit(rng, 1, cx);
570 });
571 let buffer_snapshot = buffer.read(cx).text_snapshot();
572 let ranges = diff.update(cx, |diff, cx| {
573 diff.recalculate_diff_sync(&buffer_snapshot, cx);
574 diff.snapshot(cx)
575 .hunks(&buffer_snapshot)
576 .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
577 .collect::<Vec<_>>()
578 });
579 let path = PathKey::for_buffer(&buffer, cx);
580 self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
581 } else {
582 let remove_count = rng.random_range(1..=paths.len());
583 let paths_to_remove = paths
584 .choose_multiple(rng, remove_count)
585 .cloned()
586 .collect::<Vec<_>>();
587 for path in paths_to_remove {
588 self.remove_excerpts_for_path(path.clone(), cx);
589 }
590 }
591 }
592 }
593}
594
595impl EventEmitter<EditorEvent> for SplittableEditor {}
596impl Focusable for SplittableEditor {
597 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
598 self.primary_editor.read(cx).focus_handle(cx)
599 }
600}
601
602impl Render for SplittableEditor {
603 fn render(
604 &mut self,
605 window: &mut ui::Window,
606 cx: &mut ui::Context<Self>,
607 ) -> impl ui::IntoElement {
608 let inner = if self.secondary.is_none() {
609 self.primary_editor.clone().into_any_element()
610 } else if let Some(active) = self.panes.panes().into_iter().next() {
611 self.panes
612 .render(
613 None,
614 &ActivePaneDecorator::new(active, &self.workspace),
615 window,
616 cx,
617 )
618 .into_any_element()
619 } else {
620 div().into_any_element()
621 };
622 div()
623 .id("splittable-editor")
624 .on_action(cx.listener(Self::split))
625 .on_action(cx.listener(Self::unsplit))
626 .size_full()
627 .child(inner)
628 }
629}
630
631impl SecondaryEditor {
632 fn sync_path_excerpts(
633 &mut self,
634 path_key: PathKey,
635 primary_multibuffer: &mut MultiBuffer,
636 diff: Entity<BufferDiff>,
637 cx: &mut App,
638 ) {
639 let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path_key).next() else {
640 self.remove_mappings_for_path(&path_key, cx);
641 self.multibuffer.update(cx, |multibuffer, cx| {
642 multibuffer.remove_excerpts_for_path(path_key, cx);
643 });
644 return;
645 };
646
647 let primary_excerpt_ids: Vec<ExcerptId> =
648 primary_multibuffer.excerpts_for_path(&path_key).collect();
649
650 let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
651 let main_buffer = primary_multibuffer_snapshot
652 .buffer_for_excerpt(excerpt_id)
653 .unwrap();
654 let base_text_buffer = diff.read(cx).base_text_buffer();
655 let diff_snapshot = diff.read(cx).snapshot(cx);
656 let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
657 let new = primary_multibuffer
658 .excerpts_for_buffer(main_buffer.remote_id(), cx)
659 .into_iter()
660 .map(|(_, excerpt_range)| {
661 let point_range_to_base_text_point_range = |range: Range<Point>| {
662 let start_row = diff_snapshot.row_to_base_text_row(
663 range.start.row,
664 Bias::Left,
665 main_buffer,
666 );
667 let end_row =
668 diff_snapshot.row_to_base_text_row(range.end.row, Bias::Right, main_buffer);
669 let end_column = diff_snapshot.base_text().line_len(end_row);
670 Point::new(start_row, 0)..Point::new(end_row, end_column)
671 };
672 let primary = excerpt_range.primary.to_point(main_buffer);
673 let context = excerpt_range.context.to_point(main_buffer);
674 ExcerptRange {
675 primary: point_range_to_base_text_point_range(primary),
676 context: point_range_to_base_text_point_range(context),
677 }
678 })
679 .collect();
680
681 let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
682
683 self.remove_mappings_for_path(&path_key, cx);
684
685 self.editor.update(cx, |editor, cx| {
686 editor.buffer().update(cx, |buffer, cx| {
687 let (ids, _) = buffer.update_path_excerpts(
688 path_key.clone(),
689 base_text_buffer.clone(),
690 &base_text_buffer_snapshot,
691 new,
692 cx,
693 );
694 if !ids.is_empty()
695 && buffer
696 .diff_for(base_text_buffer.read(cx).remote_id())
697 .is_none_or(|old_diff| old_diff.entity_id() != diff.entity_id())
698 {
699 buffer.add_inverted_diff(diff, main_buffer, cx);
700 }
701 })
702 });
703
704 let secondary_excerpt_ids: Vec<ExcerptId> = self
705 .multibuffer
706 .read(cx)
707 .excerpts_for_path(&path_key)
708 .collect();
709
710 for (primary_id, secondary_id) in primary_excerpt_ids.into_iter().zip(secondary_excerpt_ids)
711 {
712 self.primary_to_secondary.insert(primary_id, secondary_id);
713 self.secondary_to_primary.insert(secondary_id, primary_id);
714 }
715 }
716
717 fn remove_mappings_for_path(&mut self, path_key: &PathKey, cx: &App) {
718 let secondary_excerpt_ids: Vec<ExcerptId> = self
719 .multibuffer
720 .read(cx)
721 .excerpts_for_path(path_key)
722 .collect();
723
724 for secondary_id in secondary_excerpt_ids {
725 if let Some(primary_id) = self.secondary_to_primary.remove(&secondary_id) {
726 self.primary_to_secondary.remove(&primary_id);
727 }
728 }
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use fs::FakeFs;
735 use gpui::AppContext as _;
736 use language::Capability;
737 use multi_buffer::{MultiBuffer, PathKey};
738 use project::Project;
739 use rand::rngs::StdRng;
740 use settings::SettingsStore;
741 use ui::VisualContext as _;
742 use workspace::Workspace;
743
744 use crate::SplittableEditor;
745
746 fn init_test(cx: &mut gpui::TestAppContext) {
747 cx.update(|cx| {
748 let store = SettingsStore::test(cx);
749 cx.set_global(store);
750 theme::init(theme::LoadThemes::JustBase, cx);
751 crate::init(cx);
752 });
753 }
754
755 #[ignore]
756 #[gpui::test(iterations = 100)]
757 async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
758 use rand::prelude::*;
759
760 init_test(cx);
761 let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
762 let (workspace, cx) =
763 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
764 let primary_multibuffer = cx.new(|cx| {
765 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
766 multibuffer.set_all_diff_hunks_expanded(cx);
767 multibuffer
768 });
769 let editor = cx.new_window_entity(|window, cx| {
770 let mut editor =
771 SplittableEditor::new_unsplit(primary_multibuffer, project, workspace, window, cx);
772 editor.split(&Default::default(), window, cx);
773 editor
774 });
775
776 let operations = std::env::var("OPERATIONS")
777 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
778 .unwrap_or(20);
779 let rng = &mut rng;
780 for _ in 0..operations {
781 editor.update(cx, |editor, cx| {
782 let buffers = editor
783 .primary_editor
784 .read(cx)
785 .buffer()
786 .read(cx)
787 .all_buffers();
788
789 if buffers.is_empty() {
790 editor.randomly_edit_excerpts(rng, 2, cx);
791 editor.check_invariants(true, cx);
792 return;
793 }
794
795 let quiesced = match rng.random_range(0..100) {
796 0..=69 if !buffers.is_empty() => {
797 let buffer = buffers.iter().choose(rng).unwrap();
798 buffer.update(cx, |buffer, cx| {
799 if rng.random() {
800 log::info!("randomly editing single buffer");
801 buffer.randomly_edit(rng, 5, cx);
802 } else {
803 log::info!("randomly undoing/redoing in single buffer");
804 buffer.randomly_undo_redo(rng, cx);
805 }
806 });
807 false
808 }
809 70..=79 => {
810 log::info!("mutating excerpts");
811 editor.randomly_edit_excerpts(rng, 2, cx);
812 false
813 }
814 80..=89 if !buffers.is_empty() => {
815 log::info!("recalculating buffer diff");
816 let buffer = buffers.iter().choose(rng).unwrap();
817 let diff = editor
818 .primary_multibuffer
819 .read(cx)
820 .diff_for(buffer.read(cx).remote_id())
821 .unwrap();
822 let buffer_snapshot = buffer.read(cx).text_snapshot();
823 diff.update(cx, |diff, cx| {
824 diff.recalculate_diff_sync(&buffer_snapshot, cx);
825 });
826 false
827 }
828 _ => {
829 log::info!("quiescing");
830 for buffer in buffers {
831 let buffer_snapshot = buffer.read(cx).text_snapshot();
832 let diff = editor
833 .primary_multibuffer
834 .read(cx)
835 .diff_for(buffer.read(cx).remote_id())
836 .unwrap();
837 diff.update(cx, |diff, cx| {
838 diff.recalculate_diff_sync(&buffer_snapshot, cx);
839 });
840 let diff_snapshot = diff.read(cx).snapshot(cx);
841 let ranges = diff_snapshot
842 .hunks(&buffer_snapshot)
843 .map(|hunk| hunk.range)
844 .collect::<Vec<_>>();
845 let path = PathKey::for_buffer(&buffer, cx);
846 editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
847 }
848 true
849 }
850 };
851
852 editor.check_invariants(quiesced, cx);
853 });
854 }
855 }
856}