start implementing test

Cole Miller and cameron created

Co-authored-by: cameron <cameron.studdstreet@gmail.com>

Change summary

crates/editor/src/editor.rs                   |   1 
crates/editor/src/split.rs                    | 194 ++++++++++++++++++++
crates/multi_buffer/src/multi_buffer.rs       |   3 
crates/multi_buffer/src/multi_buffer_tests.rs |   5 
4 files changed, 191 insertions(+), 12 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -16300,7 +16300,6 @@ impl Editor {
         &mut self,
         lines: u32,
         direction: ExpandExcerptDirection,
-
         cx: &mut Context<Self>,
     ) {
         let selections = self.selections.disjoint_anchors_arc();

crates/editor/src/split.rs 🔗

@@ -1,12 +1,12 @@
-use std::{ops::Range, sync::Arc};
+use std::ops::Range;
 
 use buffer_diff::BufferDiff;
 use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
 use gpui::{
     Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
 };
-use language::{Buffer, Capability, LanguageRegistry};
-use multi_buffer::{Anchor, ExcerptRange, MultiBuffer, PathKey};
+use language::{Buffer, Capability};
+use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, PathKey};
 use project::Project;
 use rope::Point;
 use text::OffsetRangeExt as _;
@@ -272,6 +272,186 @@ impl SplittableEditor {
             })
         })
     }
+
+    fn expand_primary_excerpts(
+        &mut self,
+        excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
+        lines: u32,
+        direction: ExpandExcerptDirection,
+        cx: &mut Context<Self>,
+    ) {
+        self.update_primary_multibuffer(cx, |multibuffer, cx| {
+            multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
+        });
+        let paths: Vec<PathKey> = excerpt_ids
+            .flat_map(|excerpt_id| {
+                self.primary_multibuffer(cx)
+                    .path_for_excerpt(excerpt_id)
+                    .cloned()
+            })
+            .collect();
+
+        if let Some(secondary) = &self.secondary {
+            self.update_primary_multibuffer(cx, |multibuffer, cx| {
+                let snapshot = primary_multibuffer.snapshot(cx);
+                for path in paths {
+                    let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
+                    let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
+                    secondary.sync_path_excerpts(path, multibuffer, diff, cx);
+                }
+            })
+        }
+    }
+
+    fn primary_multibuffer<'a>(&'a self, cx: &'a Context<Self>) -> &'a MultiBuffer {
+        self.primary_editor.read(cx).buffer.read(cx)
+    }
+
+    fn update_primary_multibuffer<R>(
+        &mut self,
+        cx: &mut Context<Self>,
+        f: impl FnOnce(&mut MultiBuffer, &mut Context<MultiBuffer>) -> R,
+    ) -> R {
+        self.primary_editor
+            .update(cx, |editor, cx| editor.buffer().update(cx, f))
+    }
+
+    #[cfg(test)]
+    fn check_invariants(&self, cx: &App) {
+        todo!()
+    }
+
+    #[cfg(test)]
+    fn randomly_edit_excerpts(
+        &mut self,
+        rng: &mut impl rand::Rng,
+        mutation_count: usize,
+        cx: &mut Context<Self>,
+    ) {
+        use rand::prelude::*;
+        use std::env;
+        use util::RandomCharIter;
+
+        let max_excerpts = env::var("MAX_EXCERPTS")
+            .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
+            .unwrap_or(5);
+
+        let excerpt_ids = self.primary_multibuffer(cx).excerpt_ids();
+
+        let mut buffers = Vec::new();
+        for _ in 0..mutation_count {
+            if rng.random_bool(0.05) {
+                log::info!("Clearing multi-buffer");
+                self.update_primary_multibuffer(cx, |multibuffer, cx| {
+                    multibuffer.clear(cx);
+                });
+                continue;
+            } else if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
+                use collections::HashSet;
+
+                let mut excerpts = HashSet::default();
+                for _ in 0..rng.random_range(0..excerpt_ids.len()) {
+                    excerpts.extend(excerpt_ids.choose(rng).copied());
+                }
+
+                let line_count = rng.random_range(0..5);
+
+                log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
+
+                // FIXME we need an expand_excerpts API on the splittable editor that does a sync
+                self.expand_primary_excerpts(
+                    excerpts.iter().cloned(),
+                    line_count,
+                    ExpandExcerptDirection::UpAndDown,
+                    cx,
+                );
+                continue;
+            }
+
+            if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
+                let buffer_handle = if rng.random() || self.buffers.is_empty() {
+                    let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
+                    buffers.push(cx.new(|cx| Buffer::local(text, cx)));
+                    let buffer = buffers.last().unwrap().read(cx);
+                    log::info!(
+                        "Creating new buffer {} with text: {:?}",
+                        buffer.remote_id(),
+                        buffer.text()
+                    );
+                    buffers.last().unwrap().clone()
+                } else {
+                    self.buffers.values().choose(rng).unwrap().buffer.clone()
+                };
+
+                let buffer = buffer_handle.read(cx);
+                let buffer_text = buffer.text();
+                let ranges = (0..rng.random_range(0..5))
+                    .map(|_| {
+                        let end_ix =
+                            buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Right);
+                        let start_ix = buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left);
+                        ExcerptRange::new(start_ix..end_ix)
+                    })
+                    .collect::<Vec<_>>();
+                log::info!(
+                    "Inserting excerpts from buffer {} and ranges {:?}: {:?}",
+                    buffer_handle.read(cx).remote_id(),
+                    ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
+                    ranges
+                        .iter()
+                        .map(|r| &buffer_text[r.context.clone()])
+                        .collect::<Vec<_>>()
+                );
+
+                let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx);
+                log::info!("Inserted with ids: {:?}", excerpt_id);
+            } else {
+                let remove_count = rng.random_range(1..=excerpt_ids.len());
+                let mut excerpts_to_remove = excerpt_ids
+                    .choose_multiple(rng, remove_count)
+                    .cloned()
+                    .collect::<Vec<_>>();
+                let snapshot = self.snapshot.borrow();
+                excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot));
+                drop(snapshot);
+                log::info!("Removing excerpts {:?}", excerpts_to_remove);
+                self.remove_excerpts(excerpts_to_remove, cx);
+            }
+        }
+    }
+
+    #[cfg(test)]
+    fn randomly_mutate(
+        &mut self,
+        rng: &mut impl rand::Rng,
+        mutation_count: usize,
+        cx: &mut Context<Self>,
+    ) {
+        use rand::prelude::*;
+
+        if rng.random_bool(0.7) {
+            let buffers = self.primary_editor.read(cx).buffer().read(cx).all_buffers();
+            let buffer = buffers.iter().choose(rng);
+
+            if let Some(buffer) = buffer {
+                buffer.update(cx, |buffer, cx| {
+                    if rng.random() {
+                        buffer.randomly_edit(rng, mutation_count, cx);
+                    } else {
+                        buffer.randomly_undo_redo(rng, cx);
+                    }
+                });
+            } else {
+                self.update_primary_multibuffer(cx, |multibuffer, cx| {
+                    multibuffer.randomly_edit(rng, mutation_count, cx);
+                });
+            }
+        } else {
+            self.randomly_edit_excerpts(rng, mutation_count, cx);
+        }
+
+        self.check_invariants(cx);
+    }
 }
 
 impl EventEmitter<EditorEvent> for SplittableEditor {}
@@ -376,6 +556,7 @@ mod tests {
     use language::{Buffer, Capability};
     use multi_buffer::MultiBuffer;
     use project::Project;
+    use rand::rngs::StdRng;
     use settings::SettingsStore;
     use ui::VisualContext as _;
     use workspace::Workspace;
@@ -383,7 +564,7 @@ mod tests {
     use crate::SplittableEditor;
 
     #[gpui::test]
-    async fn test_basic_excerpts(cx: &mut gpui::TestAppContext) {
+    async fn test_basic_excerpts(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
         cx.update(|cx| {
             let store = SettingsStore::test(cx);
             cx.set_global(store);
@@ -408,6 +589,11 @@ mod tests {
             SplittableEditor::new_unsplit(multibuffer, project, workspace, window, cx)
         });
 
+        let mutation_count = rng.gen_range(0..100);
+        editor.update(cx, |editor, cx| {
+            editor.randomly_mutate(rng, mutation_count, cx);
+        })
+
         // for _ in 0..random() {
         //     editor.update(cx, |editor, cx| {
         //         randomly_mutate(primary_multibuffer);

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -3595,7 +3595,6 @@ impl MultiBuffer {
         let max_excerpts = env::var("MAX_EXCERPTS")
             .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
             .unwrap_or(5);
-        let use_path_excerpts = !self.excerpts_by_path.is_empty();
 
         let mut buffers = Vec::new();
         for _ in 0..mutation_count {
@@ -3709,7 +3708,7 @@ impl MultiBuffer {
         self.check_invariants(cx);
     }
 
-    fn check_invariants(&self, cx: &App) {
+    pub fn check_invariants(&self, cx: &App) {
         self.read(cx).check_invariants();
     }
 }

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -3715,11 +3715,6 @@ fn format_diff(
 //         .join("\n")
 // }
 
-// FIXME test:
-// - multibuffer with multiple excerpts having inverted diffs
-//   - via randomized test?
-// - diff hunk navigation when diff is inverted
-// - inverted diff language changing
 #[gpui::test]
 async fn test_inverted_diff(cx: &mut TestAppContext) {
     let text = indoc!(