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}