split.rs

  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}