split.rs

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