Start on maintaining edits in `FragmentList`

Antonio Scandurra created

Change summary

Cargo.lock                           |   2 
crates/language/Cargo.toml           |  10 
crates/language/src/fragment_list.rs | 220 +++++++++++++++++++++++++----
crates/text/src/patch.rs             |   4 
4 files changed, 200 insertions(+), 36 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2575,6 +2575,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "clock",
+ "collections",
  "futures",
  "gpui",
  "lazy_static",
@@ -2586,6 +2587,7 @@ dependencies = [
  "rpc",
  "serde",
  "similar",
+ "smallvec",
  "smol",
  "sum_tree",
  "text",

crates/language/Cargo.toml 🔗

@@ -9,19 +9,21 @@ path = "src/language.rs"
 [features]
 test-support = [
     "rand",
-    "text/test-support",
+    "collections/test-support",
     "lsp/test-support",
+    "text/test-support",
     "tree-sitter-rust",
     "util/test-support",
 ]
 
 [dependencies]
-text = { path = "../text" }
 clock = { path = "../clock" }
+collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 lsp = { path = "../lsp" }
 rpc = { path = "../rpc" }
 sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 anyhow = "1.0.38"
@@ -33,14 +35,16 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = { version = "0.8.3", optional = true }
 serde = { version = "1", features = ["derive"] }
 similar = "1.3"
+smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 tree-sitter = "0.20.0"
 tree-sitter-rust = { version = "0.20.0", optional = true }
 
 [dev-dependencies]
-text = { path = "../text", features = ["test-support"] }
+collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
+text = { path = "../text", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 rand = "0.8.3"
 tree-sitter-rust = "0.20.0"

crates/language/src/fragment_list.rs 🔗

@@ -1,14 +1,12 @@
-use std::{
-    cmp,
-    ops::{Deref, Range},
-};
+use crate::{buffer, Buffer, Chunk};
+use collections::HashMap;
+use gpui::{AppContext, Entity, ModelContext, ModelHandle};
+use parking_lot::Mutex;
+use smallvec::{smallvec, SmallVec};
+use std::{cmp, iter, mem, ops::Range};
 use sum_tree::{Bias, Cursor, SumTree};
 use text::TextSummary;
 use theme::SyntaxTheme;
-use util::post_inc;
-
-use crate::{buffer, Buffer, Chunk};
-use gpui::{Entity, ModelContext, ModelHandle};
 
 const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
 
@@ -16,12 +14,12 @@ pub trait ToOffset {
     fn to_offset<'a>(&self, content: &Snapshot) -> usize;
 }
 
-pub type FragmentId = usize;
+pub type FragmentId = Location;
 
 #[derive(Default)]
 pub struct FragmentList {
-    snapshot: Snapshot,
-    next_fragment_id: FragmentId,
+    snapshot: Mutex<Snapshot>,
+    buffers: HashMap<usize, (ModelHandle<Buffer>, text::Subscription, Vec<FragmentId>)>,
 }
 
 #[derive(Clone, Default)]
@@ -37,8 +35,8 @@ pub struct FragmentProperties<'a, T> {
 
 #[derive(Clone)]
 struct Entry {
+    id: FragmentId,
     buffer: buffer::Snapshot,
-    buffer_id: usize,
     buffer_range: Range<usize>,
     text_summary: TextSummary,
     header_height: u8,
@@ -46,11 +44,13 @@ struct Entry {
 
 #[derive(Clone, Debug, Default)]
 struct EntrySummary {
-    min_buffer_id: usize,
-    max_buffer_id: usize,
+    fragment_id: FragmentId,
     text: TextSummary,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Location(SmallVec<[usize; 4]>);
+
 pub struct Chunks<'a> {
     range: Range<usize>,
     cursor: Cursor<'a, Entry, usize>,
@@ -64,12 +64,20 @@ impl FragmentList {
         Self::default()
     }
 
-    pub fn push<'a, O: text::ToOffset>(
+    pub fn snapshot(&self, cx: &AppContext) -> Snapshot {
+        self.sync(cx);
+        self.snapshot.lock().clone()
+    }
+
+    pub fn push<O>(
         &mut self,
-        props: FragmentProperties<'a, O>,
+        props: FragmentProperties<O>,
         cx: &mut ModelContext<Self>,
-    ) -> FragmentId {
-        let id = post_inc(&mut self.next_fragment_id);
+    ) -> FragmentId
+    where
+        O: text::ToOffset,
+    {
+        self.sync(cx);
 
         let buffer = props.buffer.read(cx);
         let buffer_range = props.range.start.to_offset(buffer)..props.range.end.to_offset(buffer);
@@ -82,10 +90,13 @@ impl FragmentList {
             text_summary.bytes += props.header_height as usize;
         }
 
-        self.snapshot.entries.push(
+        let mut snapshot = self.snapshot.lock();
+        let prev_id = snapshot.entries.last().map(|e| &e.id);
+        let id = FragmentId::between(prev_id.unwrap_or(&FragmentId::min()), &FragmentId::max());
+        snapshot.entries.push(
             Entry {
+                id: id.clone(),
                 buffer: props.buffer.read(cx).snapshot(),
-                buffer_id: props.buffer.id(),
                 buffer_range,
                 text_summary,
                 header_height: props.header_height,
@@ -93,15 +104,89 @@ impl FragmentList {
             &(),
         );
 
+        self.buffers
+            .entry(props.buffer.id())
+            .or_insert_with(|| {
+                let subscription = props.buffer.update(cx, |buffer, _| buffer.subscribe());
+                (props.buffer.clone(), subscription, Default::default())
+            })
+            .2
+            .push(id.clone());
+
         id
     }
-}
 
-impl Deref for FragmentList {
-    type Target = Snapshot;
+    fn sync(&self, cx: &AppContext) {
+        let mut snapshot = self.snapshot.lock();
+        let mut patches = Vec::new();
+        let mut fragments_to_edit = Vec::new();
+        for (buffer, subscription, fragment_ids) in self.buffers.values() {
+            let patch = subscription.consume();
+            if !patch.is_empty() {
+                let patch_ix = patches.len();
+                patches.push(patch);
+                fragments_to_edit.extend(
+                    fragment_ids
+                        .iter()
+                        .map(|fragment_id| (buffer, fragment_id, patch_ix)),
+                )
+            }
+        }
+        fragments_to_edit.sort_unstable_by_key(|(_, fragment_id, _)| *fragment_id);
+
+        let old_fragments = mem::take(&mut snapshot.entries);
+        let mut cursor = old_fragments.cursor::<FragmentId>();
+        for (buffer, fragment_id, patch_ix) in fragments_to_edit {
+            snapshot
+                .entries
+                .push_tree(cursor.slice(fragment_id, Bias::Left, &()), &());
+
+            let fragment = cursor.item().unwrap();
+            let mut new_range = fragment.buffer_range.clone();
+            for edit in patches[patch_ix].edits() {
+                let edit_start = edit.new.start;
+                let edit_end = edit.new.start + edit.old_len();
+                if edit_end < new_range.start {
+                    let delta = edit.new_len() as isize - edit.old_len() as isize;
+                    new_range.start = (new_range.start as isize + delta) as usize;
+                    new_range.end = (new_range.end as isize + delta) as usize;
+                } else if edit_start >= new_range.end {
+                    break;
+                } else {
+                    let mut new_range_len = new_range.len();
+                    new_range_len -=
+                        cmp::min(new_range.end, edit_end) - cmp::max(new_range.start, edit_start);
+                    if edit_start > new_range.start {
+                        new_range_len += edit.new_len();
+                    }
+
+                    new_range.start = cmp::min(new_range.start, edit.new.end);
+                    new_range.end = new_range.start + new_range_len;
+                }
+            }
 
-    fn deref(&self) -> &Self::Target {
-        &self.snapshot
+            let buffer = buffer.read(cx);
+            let mut text_summary: TextSummary = buffer.text_summary_for_range(new_range.clone());
+            if fragment.header_height > 0 {
+                text_summary.first_line_chars = 0;
+                text_summary.lines.row += fragment.header_height as u32;
+                text_summary.lines_utf16.row += fragment.header_height as u32;
+                text_summary.bytes += fragment.header_height as usize;
+            }
+            snapshot.entries.push(
+                Entry {
+                    id: fragment.id.clone(),
+                    buffer: buffer.snapshot(),
+                    buffer_range: new_range,
+                    text_summary,
+                    header_height: fragment.header_height,
+                },
+                &(),
+            );
+
+            cursor.next(&());
+        }
+        snapshot.entries.push_tree(cursor.suffix(&()), &());
     }
 }
 
@@ -154,8 +239,7 @@ impl sum_tree::Item for Entry {
 
     fn summary(&self) -> Self::Summary {
         EntrySummary {
-            min_buffer_id: self.buffer_id,
-            max_buffer_id: self.buffer_id,
+            fragment_id: self.id.clone(),
             text: self.text_summary.clone(),
         }
     }
@@ -165,15 +249,22 @@ impl sum_tree::Summary for EntrySummary {
     type Context = ();
 
     fn add_summary(&mut self, summary: &Self, _: &()) {
-        self.min_buffer_id = cmp::min(self.min_buffer_id, summary.min_buffer_id);
-        self.max_buffer_id = cmp::max(self.max_buffer_id, summary.max_buffer_id);
+        debug_assert!(summary.fragment_id > self.fragment_id);
+        self.fragment_id = summary.fragment_id.clone();
         self.text.add_summary(&summary.text, &());
     }
 }
 
 impl<'a> sum_tree::Dimension<'a, EntrySummary> for usize {
     fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
-        *self += summary.text.bytes
+        *self += summary.text.bytes;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, EntrySummary> for FragmentId {
+    fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
+        debug_assert!(summary.fragment_id > *self);
+        *self = summary.fragment_id.clone();
     }
 }
 
@@ -228,11 +319,42 @@ impl ToOffset for usize {
     }
 }
 
+impl Default for Location {
+    fn default() -> Self {
+        Self::min()
+    }
+}
+
+impl Location {
+    pub fn min() -> Self {
+        Self(smallvec![usize::MIN])
+    }
+
+    pub fn max() -> Self {
+        Self(smallvec![usize::MAX])
+    }
+
+    pub fn between(lhs: &Self, rhs: &Self) -> Self {
+        let lhs = lhs.0.iter().copied().chain(iter::repeat(usize::MIN));
+        let rhs = rhs.0.iter().copied().chain(iter::repeat(usize::MAX));
+        let mut location = SmallVec::new();
+        for (lhs, rhs) in lhs.zip(rhs) {
+            let mid = lhs + (rhs.saturating_sub(lhs)) / 2;
+            location.push(mid);
+            if mid > lhs {
+                break;
+            }
+        }
+        Self(location)
+    }
+}
+
 #[cfg(test)]
 mod tests {
-    use super::{FragmentList, FragmentProperties};
+    use super::*;
     use crate::Buffer;
     use gpui::MutableAppContext;
+    use rand::prelude::*;
     use text::Point;
     use util::test::sample_text;
 
@@ -272,7 +394,7 @@ mod tests {
         });
 
         assert_eq!(
-            list.read(cx).text(),
+            list.read(cx).snapshot(cx).text(),
             concat!(
                 "\n",      // Preserve newlines
                 "\n",      //
@@ -300,7 +422,7 @@ mod tests {
         });
 
         assert_eq!(
-            list.read(cx).text(),
+            list.read(cx).snapshot(cx).text(),
             concat!(
                 "\n",     // Preserve newlines
                 "\n",     //
@@ -317,4 +439,36 @@ mod tests {
             )
         );
     }
+
+    #[gpui::test(iterations = 10000)]
+    fn test_location(mut rng: StdRng) {
+        let mut lhs = Default::default();
+        let mut rhs = Default::default();
+        while lhs == rhs {
+            lhs = Location(
+                (0..rng.gen_range(1..=5))
+                    .map(|_| rng.gen_range(0..=100))
+                    .collect(),
+            );
+            rhs = Location(
+                (0..rng.gen_range(1..=5))
+                    .map(|_| rng.gen_range(0..=100))
+                    .collect(),
+            );
+        }
+
+        if lhs > rhs {
+            mem::swap(&mut lhs, &mut rhs);
+        }
+
+        let middle = Location::between(&lhs, &rhs);
+        assert!(middle > lhs);
+        assert!(middle < rhs);
+        for ix in 0..middle.0.len() - 1 {
+            assert!(
+                middle.0[ix] == *lhs.0.get(ix).unwrap_or(&0)
+                    || middle.0[ix] == *rhs.0.get(ix).unwrap_or(&0)
+            );
+        }
+    }
 }

crates/text/src/patch.rs 🔗

@@ -33,6 +33,10 @@ where
         Self(edits)
     }
 
+    pub fn edits(&self) -> &[Edit<T>] {
+        &self.0
+    }
+
     pub fn into_inner(self) -> Vec<Edit<T>> {
         self.0
     }