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