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::{Bias, OffsetRangeExt as _};
 14use ui::{
 15    App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
 16    Styled as _, Window, div,
 17};
 18use workspace::{
 19    ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
 20};
 21
 22use crate::{Editor, EditorEvent};
 23
 24struct SplitDiffFeatureFlag;
 25
 26impl FeatureFlag for SplitDiffFeatureFlag {
 27    const NAME: &'static str = "split-diff";
 28
 29    fn enabled_for_staff() -> bool {
 30        true
 31    }
 32}
 33
 34#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 35#[action(namespace = editor)]
 36struct SplitDiff;
 37
 38#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
 39#[action(namespace = editor)]
 40struct UnsplitDiff;
 41
 42pub struct SplittableEditor {
 43    primary_multibuffer: Entity<MultiBuffer>,
 44    primary_editor: Entity<Editor>,
 45    secondary: Option<SecondaryEditor>,
 46    panes: PaneGroup,
 47    workspace: WeakEntity<Workspace>,
 48    _subscriptions: Vec<Subscription>,
 49}
 50
 51struct SecondaryEditor {
 52    multibuffer: Entity<MultiBuffer>,
 53    editor: Entity<Editor>,
 54    pane: Entity<Pane>,
 55    has_latest_selection: bool,
 56    _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().cloned().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.clone(), 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, cx)
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, cx).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).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, quiesced: bool, 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        if quiesced {
414            let primary_snapshot = self.primary_multibuffer.read(cx).snapshot(cx);
415            let secondary_snapshot = secondary.multibuffer.read(cx).snapshot(cx);
416            let primary_diff_hunks = primary_snapshot
417                .diff_hunks()
418                .map(|hunk| hunk.diff_base_byte_range)
419                .collect::<Vec<_>>();
420            let secondary_diff_hunks = secondary_snapshot
421                .diff_hunks()
422                .map(|hunk| hunk.diff_base_byte_range)
423                .collect::<Vec<_>>();
424            pretty_assertions::assert_eq!(primary_diff_hunks, secondary_diff_hunks);
425
426            // Filtering out empty lines is a bit of a hack, to work around a case where
427            // the base text has a trailing newline but the current text doesn't, or vice versa.
428            // In this case, we get the additional newline on one side, but that line is not
429            // marked as added/deleted by rowinfos.
430            let primary_unmodified_rows = primary_snapshot
431                .text()
432                .split("\n")
433                .zip(primary_snapshot.row_infos(MultiBufferRow(0)))
434                .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
435                .map(|(line, _)| line.to_owned())
436                .collect::<Vec<_>>();
437            let secondary_unmodified_rows = secondary_snapshot
438                .text()
439                .split("\n")
440                .zip(secondary_snapshot.row_infos(MultiBufferRow(0)))
441                .filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
442                .map(|(line, _)| line.to_owned())
443                .collect::<Vec<_>>();
444            pretty_assertions::assert_eq!(primary_unmodified_rows, secondary_unmodified_rows);
445        }
446    }
447
448    fn randomly_edit_excerpts(
449        &mut self,
450        rng: &mut impl rand::Rng,
451        mutation_count: usize,
452        cx: &mut Context<Self>,
453    ) {
454        use collections::HashSet;
455        use rand::prelude::*;
456        use std::env;
457        use util::RandomCharIter;
458
459        let max_excerpts = env::var("MAX_EXCERPTS")
460            .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
461            .unwrap_or(5);
462
463        for _ in 0..mutation_count {
464            let paths = self
465                .primary_multibuffer
466                .read(cx)
467                .paths()
468                .cloned()
469                .collect::<Vec<_>>();
470            let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
471
472            if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
473                let mut excerpts = HashSet::default();
474                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
475                    excerpts.extend(excerpt_ids.choose(rng).copied());
476                }
477
478                let line_count = rng.random_range(0..5);
479
480                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
481
482                self.expand_excerpts(
483                    excerpts.iter().cloned(),
484                    line_count,
485                    ExpandExcerptDirection::UpAndDown,
486                    cx,
487                );
488                continue;
489            }
490
491            if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
492                let len = rng.random_range(100..500);
493                let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
494                let buffer = cx.new(|cx| Buffer::local(text, cx));
495                log::info!(
496                    "Creating new buffer {} with text: {:?}",
497                    buffer.read(cx).remote_id(),
498                    buffer.read(cx).text()
499                );
500                let buffer_snapshot = buffer.read(cx).snapshot();
501                let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
502                // Create some initial diff hunks.
503                buffer.update(cx, |buffer, cx| {
504                    buffer.randomly_edit(rng, 1, cx);
505                });
506                let buffer_snapshot = buffer.read(cx).text_snapshot();
507                let ranges = diff.update(cx, |diff, cx| {
508                    diff.recalculate_diff_sync(&buffer_snapshot, cx);
509                    diff.snapshot(cx)
510                        .hunks(&buffer_snapshot)
511                        .map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
512                        .collect::<Vec<_>>()
513                });
514                let path = PathKey::for_buffer(&buffer, cx);
515                self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
516            } else {
517                let remove_count = rng.random_range(1..=paths.len());
518                let paths_to_remove = paths
519                    .choose_multiple(rng, remove_count)
520                    .cloned()
521                    .collect::<Vec<_>>();
522                for path in paths_to_remove {
523                    self.remove_excerpts_for_path(path.clone(), cx);
524                }
525            }
526        }
527    }
528}
529
530impl EventEmitter<EditorEvent> for SplittableEditor {}
531impl Focusable for SplittableEditor {
532    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
533        self.primary_editor.read(cx).focus_handle(cx)
534    }
535}
536
537impl Render for SplittableEditor {
538    fn render(
539        &mut self,
540        window: &mut ui::Window,
541        cx: &mut ui::Context<Self>,
542    ) -> impl ui::IntoElement {
543        let inner = if self.secondary.is_none() {
544            self.primary_editor.clone().into_any_element()
545        } else if let Some(active) = self.panes.panes().into_iter().next() {
546            self.panes
547                .render(
548                    None,
549                    &ActivePaneDecorator::new(active, &self.workspace),
550                    window,
551                    cx,
552                )
553                .into_any_element()
554        } else {
555            div().into_any_element()
556        };
557        div()
558            .id("splittable-editor")
559            .on_action(cx.listener(Self::split))
560            .on_action(cx.listener(Self::unsplit))
561            .size_full()
562            .child(inner)
563    }
564}
565
566impl SecondaryEditor {
567    fn sync_path_excerpts(
568        &mut self,
569        path_key: PathKey,
570        primary_multibuffer: &mut MultiBuffer,
571        diff: Entity<BufferDiff>,
572        cx: &mut App,
573    ) {
574        let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path_key).next() else {
575            self.multibuffer.update(cx, |multibuffer, cx| {
576                multibuffer.remove_excerpts_for_path(path_key, cx);
577            });
578            return;
579        };
580        let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
581        let main_buffer = primary_multibuffer_snapshot
582            .buffer_for_excerpt(excerpt_id)
583            .unwrap();
584        let base_text_buffer = diff.read(cx).base_text_buffer();
585        let diff_snapshot = diff.read(cx).snapshot(cx);
586        let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
587        let new = primary_multibuffer
588            .excerpts_for_buffer(main_buffer.remote_id(), cx)
589            .into_iter()
590            .map(|(_, excerpt_range)| {
591                let point_range_to_base_text_point_range = |range: Range<Point>| {
592                    let start_row = diff_snapshot.row_to_base_text_row(
593                        range.start.row,
594                        Bias::Left,
595                        main_buffer,
596                    );
597                    let end_row =
598                        diff_snapshot.row_to_base_text_row(range.end.row, Bias::Right, main_buffer);
599                    let end_column = diff_snapshot.base_text().line_len(end_row);
600                    Point::new(start_row, 0)..Point::new(end_row, end_column)
601                };
602                let primary = excerpt_range.primary.to_point(main_buffer);
603                let context = excerpt_range.context.to_point(main_buffer);
604                ExcerptRange {
605                    primary: point_range_to_base_text_point_range(primary),
606                    context: point_range_to_base_text_point_range(context),
607                }
608            })
609            .collect();
610
611        let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
612
613        self.editor.update(cx, |editor, cx| {
614            editor.buffer().update(cx, |buffer, cx| {
615                buffer.update_path_excerpts(
616                    path_key,
617                    base_text_buffer,
618                    &base_text_buffer_snapshot,
619                    new,
620                    cx,
621                );
622                buffer.add_inverted_diff(diff, main_buffer, cx);
623            })
624        });
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use fs::FakeFs;
631    use gpui::AppContext as _;
632    use language::Capability;
633    use multi_buffer::{MultiBuffer, PathKey};
634    use project::Project;
635    use rand::rngs::StdRng;
636    use settings::SettingsStore;
637    use ui::VisualContext as _;
638    use workspace::Workspace;
639
640    use crate::SplittableEditor;
641
642    fn init_test(cx: &mut gpui::TestAppContext) {
643        cx.update(|cx| {
644            let store = SettingsStore::test(cx);
645            cx.set_global(store);
646            theme::init(theme::LoadThemes::JustBase, cx);
647            crate::init(cx);
648        });
649    }
650
651    #[gpui::test(iterations = 100)]
652    async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
653        use rand::prelude::*;
654
655        init_test(cx);
656        let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
657        let (workspace, cx) =
658            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
659        let primary_multibuffer = cx.new(|cx| {
660            let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
661            multibuffer.set_all_diff_hunks_expanded(cx);
662            multibuffer
663        });
664        let editor = cx.new_window_entity(|window, cx| {
665            let mut editor =
666                SplittableEditor::new_unsplit(primary_multibuffer, project, workspace, window, cx);
667            editor.split(&Default::default(), window, cx);
668            editor
669        });
670
671        let operations = std::env::var("OPERATIONS")
672            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
673            .unwrap_or(20);
674        let rng = &mut rng;
675        for _ in 0..operations {
676            editor.update(cx, |editor, cx| {
677                let buffers = editor
678                    .primary_editor
679                    .read(cx)
680                    .buffer()
681                    .read(cx)
682                    .all_buffers();
683
684                if buffers.is_empty() {
685                    editor.randomly_edit_excerpts(rng, 2, cx);
686                    editor.check_invariants(true, cx);
687                    return;
688                }
689
690                let quiesced = match rng.random_range(0..100) {
691                    0..=69 if !buffers.is_empty() => {
692                        let buffer = buffers.iter().choose(rng).unwrap();
693                        buffer.update(cx, |buffer, cx| {
694                            if rng.random() {
695                                log::info!("randomly editing single buffer");
696                                buffer.randomly_edit(rng, 5, cx);
697                            } else {
698                                log::info!("randomly undoing/redoing in single buffer");
699                                buffer.randomly_undo_redo(rng, cx);
700                            }
701                        });
702                        false
703                    }
704                    70..=79 => {
705                        log::info!("mutating excerpts");
706                        editor.randomly_edit_excerpts(rng, 2, cx);
707                        false
708                    }
709                    80..=89 if !buffers.is_empty() => {
710                        log::info!("recalculating buffer diff");
711                        let buffer = buffers.iter().choose(rng).unwrap();
712                        let diff = editor
713                            .primary_multibuffer
714                            .read(cx)
715                            .diff_for(buffer.read(cx).remote_id())
716                            .unwrap();
717                        let buffer_snapshot = buffer.read(cx).text_snapshot();
718                        diff.update(cx, |diff, cx| {
719                            diff.recalculate_diff_sync(&buffer_snapshot, cx);
720                        });
721                        false
722                    }
723                    _ => {
724                        log::info!("quiescing");
725                        for buffer in buffers {
726                            let buffer_snapshot = buffer.read(cx).text_snapshot();
727                            let diff = editor
728                                .primary_multibuffer
729                                .read(cx)
730                                .diff_for(buffer.read(cx).remote_id())
731                                .unwrap();
732                            diff.update(cx, |diff, cx| {
733                                diff.recalculate_diff_sync(&buffer_snapshot, cx);
734                            });
735                            let diff_snapshot = diff.read(cx).snapshot(cx);
736                            let ranges = diff_snapshot
737                                .hunks(&buffer_snapshot)
738                                .map(|hunk| hunk.range)
739                                .collect::<Vec<_>>();
740                            let path = PathKey::for_buffer(&buffer, cx);
741                            editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
742                        }
743                        true
744                    }
745                };
746
747                editor.check_invariants(quiesced, cx);
748            });
749        }
750    }
751}