Implement Anchor, AnchorRangeMap, SelectionSet in multi_buffer

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/language/src/multi_buffer.rs           | 140 ++++-----
crates/language/src/multi_buffer/anchor.rs    | 280 +++++++++++++++++++++
crates/language/src/multi_buffer/location.rs  |  76 +++++
crates/language/src/multi_buffer/selection.rs |  91 ++++++
crates/text/src/anchor.rs                     |   7 
crates/text/src/rope.rs                       |   9 
6 files changed, 525 insertions(+), 78 deletions(-)

Detailed changes

crates/language/src/multi_buffer.rs 🔗

@@ -1,21 +1,26 @@
-use crate::buffer::{self, Buffer, Chunk, ToOffset as _, ToPoint as _};
+mod anchor;
+mod location;
+mod selection;
+
+use self::location::*;
+use crate::{
+    buffer::{self, Buffer, Chunk, ToOffset as _, ToPoint as _},
+    BufferSnapshot,
+};
 use collections::HashMap;
 use gpui::{AppContext, Entity, ModelContext, ModelHandle};
 use parking_lot::Mutex;
-use smallvec::{smallvec, SmallVec};
-use std::{cmp, iter, ops::Range};
+use std::{cmp, ops::Range};
 use sum_tree::{Bias, Cursor, SumTree};
 use text::{
     rope::TextDimension,
     subscription::{Subscription, Topic},
-    Anchor, AnchorRangeExt, Edit, Point, PointUtf16, TextSummary,
+    AnchorRangeExt, Edit, Point, PointUtf16, TextSummary,
 };
 use theme::SyntaxTheme;
 
 const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
 
-pub type ExcerptId = Location;
-
 #[derive(Default)]
 pub struct MultiBuffer {
     snapshot: Mutex<MultiBufferSnapshot>,
@@ -53,7 +58,7 @@ pub struct ExcerptProperties<'a, T> {
 struct Excerpt {
     id: ExcerptId,
     buffer: buffer::BufferSnapshot,
-    range: Range<Anchor>,
+    range: Range<text::Anchor>,
     text_summary: TextSummary,
     header_height: u8,
 }
@@ -64,9 +69,6 @@ struct ExcerptSummary {
     text: TextSummary,
 }
 
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Location(SmallVec<[u8; 4]>);
-
 pub struct Chunks<'a> {
     range: Range<usize>,
     cursor: Cursor<'a, Excerpt, usize>,
@@ -531,13 +533,41 @@ impl MultiBufferSnapshot {
 
         summary
     }
+
+    fn resolve_excerpt<'a, D: TextDimension>(
+        &'a self,
+        excerpt_id: &ExcerptId,
+    ) -> Option<(D, &'a BufferSnapshot)> {
+        let mut cursor = self.excerpts.cursor::<(ExcerptId, TextSummary)>();
+        cursor.seek(excerpt_id, Bias::Left, &());
+        if let Some(excerpt) = cursor.item() {
+            if cursor.start().0 == *excerpt_id {
+                return Some((D::from_text_summary(&cursor.start().1), &excerpt.buffer));
+            }
+        }
+        None
+    }
+
+    fn buffer_snapshot_for_excerpt<'a>(
+        &'a self,
+        excerpt_id: &ExcerptId,
+    ) -> Option<&'a BufferSnapshot> {
+        let mut cursor = self.excerpts.cursor::<ExcerptId>();
+        cursor.seek(excerpt_id, Bias::Left, &());
+        if let Some(excerpt) = cursor.item() {
+            if cursor.start() == excerpt_id {
+                return Some(&excerpt.buffer);
+            }
+        }
+        None
+    }
 }
 
 impl Excerpt {
     fn new(
         id: ExcerptId,
         buffer: buffer::BufferSnapshot,
-        range: Range<Anchor>,
+        range: Range<text::Anchor>,
         header_height: u8,
     ) -> Self {
         let mut text_summary =
@@ -564,6 +594,18 @@ impl Excerpt {
             header_height,
         }
     }
+
+    fn header_summary(&self) -> TextSummary {
+        TextSummary {
+            bytes: self.header_height as usize,
+            lines: Point::new(self.header_height as u32, 0),
+            lines_utf16: PointUtf16::new(self.header_height as u32, 0),
+            first_line_chars: 0,
+            last_line_chars: 0,
+            longest_row: 0,
+            longest_row_chars: 0,
+        }
+    }
 }
 
 impl sum_tree::Item for Excerpt {
@@ -599,6 +641,18 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for usize {
     }
 }
 
+impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
+    fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering {
+        Ord::cmp(self, &cursor_location.text.bytes)
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Location {
+    fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering {
+        Ord::cmp(self, &cursor_location.excerpt_id)
+    }
+}
+
 impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point {
     fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
         *self += summary.text.lines;
@@ -703,43 +757,13 @@ impl ToPoint for Point {
     }
 }
 
-impl Default for Location {
-    fn default() -> Self {
-        Self::min()
-    }
-}
-
-impl Location {
-    pub fn min() -> Self {
-        Self(smallvec![u8::MIN])
-    }
-
-    pub fn max() -> Self {
-        Self(smallvec![u8::MAX])
-    }
-
-    pub fn between(lhs: &Self, rhs: &Self) -> Self {
-        let lhs = lhs.0.iter().copied().chain(iter::repeat(u8::MIN));
-        let rhs = rhs.0.iter().copied().chain(iter::repeat(u8::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::*;
     use crate::buffer::Buffer;
     use gpui::MutableAppContext;
     use rand::prelude::*;
-    use std::{env, mem};
+    use std::env;
     use text::{Point, RandomCharIter};
     use util::test::sample_text;
 
@@ -1094,36 +1118,4 @@ mod tests {
             assert_eq!(text.to_string(), snapshot.text());
         }
     }
-
-    #[gpui::test(iterations = 100)]
-    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/language/src/multi_buffer/anchor.rs 🔗

@@ -0,0 +1,280 @@
+use super::{location::*, ExcerptSummary, MultiBufferSnapshot, ToOffset};
+use anyhow::{anyhow, Result};
+use smallvec::SmallVec;
+use std::{cmp::Ordering, ops::Range};
+use sum_tree::Bias;
+use text::{rope::TextDimension, AnchorRangeExt, ToOffset as _};
+
+#[derive(Clone, Eq, PartialEq, Debug, Hash)]
+pub struct Anchor {
+    excerpt_id: ExcerptId,
+    text_anchor: text::Anchor,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct AnchorRangeMap<T> {
+    entries: SmallVec<[(ExcerptId, text::AnchorRangeMap<T>); 1]>,
+}
+
+impl Anchor {
+    pub fn min() -> Self {
+        Self {
+            excerpt_id: ExcerptId::min(),
+            text_anchor: text::Anchor::min(),
+        }
+    }
+
+    pub fn max() -> Self {
+        Self {
+            excerpt_id: ExcerptId::max(),
+            text_anchor: text::Anchor::max(),
+        }
+    }
+
+    pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Result<Ordering> {
+        let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
+        if excerpt_id_cmp.is_eq() {
+            self.text_anchor.cmp(
+                &other.text_anchor,
+                snapshot
+                    .buffer_snapshot_for_excerpt(&self.excerpt_id)
+                    .ok_or_else(|| anyhow!("excerpt {:?} not found", self.excerpt_id))?,
+            )
+        } else {
+            return Ok(excerpt_id_cmp);
+        }
+    }
+
+    pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
+        if self.text_anchor.bias != Bias::Left {
+            if let Some(buffer_snapshot) = snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) {
+                return Self {
+                    excerpt_id: self.excerpt_id.clone(),
+                    text_anchor: self.text_anchor.bias_left(buffer_snapshot),
+                };
+            }
+        }
+        self.clone()
+    }
+
+    pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
+        if self.text_anchor.bias != Bias::Right {
+            if let Some(buffer_snapshot) = snapshot.buffer_snapshot_for_excerpt(&self.excerpt_id) {
+                return Self {
+                    excerpt_id: self.excerpt_id.clone(),
+                    text_anchor: self.text_anchor.bias_right(buffer_snapshot),
+                };
+            }
+        }
+        self.clone()
+    }
+}
+
+impl<T> AnchorRangeMap<T> {
+    pub fn len(&self) -> usize {
+        self.entries
+            .iter()
+            .map(|(_, text_map)| text_map.len())
+            .sum()
+    }
+
+    pub fn ranges<'a, D>(
+        &'a self,
+        snapshot: &'a MultiBufferSnapshot,
+    ) -> impl Iterator<Item = (Range<D>, &'a T)> + 'a
+    where
+        D: TextDimension + Clone,
+    {
+        let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>();
+        self.entries
+            .iter()
+            .filter_map(move |(excerpt_id, text_map)| {
+                cursor.seek_forward(excerpt_id, Bias::Left, &());
+                if let Some(excerpt) = cursor.item() {
+                    if excerpt.id == *excerpt_id {
+                        let mut excerpt_start = D::from_text_summary(&cursor.start().text);
+                        excerpt_start.add_summary(&excerpt.header_summary(), &());
+                        return Some(text_map.ranges::<D>(&excerpt.buffer).map(
+                            move |(range, value)| {
+                                let mut full_range = excerpt_start.clone()..excerpt_start.clone();
+                                full_range.start.add_assign(&range.start);
+                                full_range.end.add_assign(&range.end);
+                                (full_range, value)
+                            },
+                        ));
+                    }
+                }
+                None
+            })
+            .flatten()
+    }
+
+    pub fn intersecting_ranges<'a, D, I>(
+        &'a self,
+        range: Range<(I, Bias)>,
+        snapshot: &'a MultiBufferSnapshot,
+    ) -> impl Iterator<Item = (Range<D>, &'a T)> + 'a
+    where
+        D: TextDimension,
+        I: ToOffset,
+    {
+        let start_bias = range.start.1;
+        let end_bias = range.end.1;
+        let start_offset = range.start.0.to_offset(snapshot);
+        let end_offset = range.end.0.to_offset(snapshot);
+
+        let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>();
+        cursor.seek(&start_offset, start_bias, &());
+        let start_excerpt_id = &cursor.start().excerpt_id;
+        let start_ix = match self
+            .entries
+            .binary_search_by_key(&start_excerpt_id, |e| &e.0)
+        {
+            Ok(ix) | Err(ix) => ix,
+        };
+
+        let mut entry_ranges = None;
+        let mut entries = self.entries[start_ix..].iter();
+        std::iter::from_fn(move || loop {
+            match &mut entry_ranges {
+                None => {
+                    let (excerpt_id, text_map) = entries.next()?;
+                    cursor.seek(excerpt_id, Bias::Left, &());
+                    if cursor.start().text.bytes >= end_offset {
+                        return None;
+                    }
+
+                    if let Some(excerpt) = cursor.item() {
+                        if excerpt.id == *excerpt_id {
+                            let mut excerpt_start = D::from_text_summary(&cursor.start().text);
+                            excerpt_start.add_summary(&excerpt.header_summary(), &());
+
+                            let excerpt_start_offset = cursor.start().text.bytes;
+                            let excerpt_end_offset = cursor.end(&()).text.bytes;
+                            let excerpt_buffer_range = excerpt.range.to_offset(&excerpt.buffer);
+
+                            let start;
+                            if start_offset >= excerpt_start_offset {
+                                start = (
+                                    excerpt_buffer_range.start + start_offset
+                                        - excerpt_start_offset,
+                                    start_bias,
+                                );
+                            } else {
+                                start = (excerpt_buffer_range.start, Bias::Left);
+                            }
+
+                            let end;
+                            if end_offset <= excerpt_end_offset {
+                                end = (
+                                    excerpt_buffer_range.start + end_offset - excerpt_start_offset,
+                                    end_bias,
+                                );
+                            } else {
+                                end = (excerpt_buffer_range.end, Bias::Right);
+                            }
+
+                            entry_ranges = Some(
+                                text_map
+                                    .intersecting_ranges(start..end, &excerpt.buffer)
+                                    .map(move |(range, value)| {
+                                        let mut full_range =
+                                            excerpt_start.clone()..excerpt_start.clone();
+                                        full_range.start.add_assign(&range.start);
+                                        full_range.end.add_assign(&range.end);
+                                        (full_range, value)
+                                    }),
+                            );
+                        }
+                    }
+                }
+                Some(ranges) => {
+                    if let Some(item) = ranges.next() {
+                        return Some(item);
+                    } else {
+                        entry_ranges.take();
+                    }
+                }
+            }
+        })
+    }
+
+    pub fn min_by_key<'a, D, F, K>(
+        &self,
+        snapshot: &'a MultiBufferSnapshot,
+        extract_key: F,
+    ) -> Option<(Range<D>, &T)>
+    where
+        D: TextDimension,
+        F: FnMut(&T) -> K,
+        K: Ord,
+    {
+        self.min_or_max_by_key(snapshot, Ordering::Less, extract_key)
+    }
+
+    pub fn max_by_key<'a, D, F, K>(
+        &self,
+        snapshot: &'a MultiBufferSnapshot,
+        extract_key: F,
+    ) -> Option<(Range<D>, &T)>
+    where
+        D: TextDimension,
+        F: FnMut(&T) -> K,
+        K: Ord,
+    {
+        self.min_or_max_by_key(snapshot, Ordering::Greater, extract_key)
+    }
+
+    fn min_or_max_by_key<'a, D, F, K>(
+        &self,
+        snapshot: &'a MultiBufferSnapshot,
+        target_ordering: Ordering,
+        mut extract_key: F,
+    ) -> Option<(Range<D>, &T)>
+    where
+        D: TextDimension,
+        F: FnMut(&T) -> K,
+        K: Ord,
+    {
+        let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>();
+        let mut max = None;
+        for (excerpt_id, text_map) in &self.entries {
+            cursor.seek(excerpt_id, Bias::Left, &());
+            if let Some(excerpt) = cursor.item() {
+                if excerpt.id == *excerpt_id {
+                    if let Some((range, value)) =
+                        text_map.max_by_key(&excerpt.buffer, &mut extract_key)
+                    {
+                        if max.as_ref().map_or(true, |(_, max_value)| {
+                            extract_key(value).cmp(&extract_key(*max_value)) == target_ordering
+                        }) {
+                            let mut excerpt_start = D::from_text_summary(&cursor.start().text);
+                            excerpt_start.add_summary(&excerpt.header_summary(), &());
+                            let mut full_range = excerpt_start.clone()..excerpt_start.clone();
+                            full_range.start.add_assign(&range.start);
+                            full_range.end.add_assign(&range.end);
+                            max = Some((full_range, value));
+                        }
+                    }
+                }
+            }
+        }
+        max
+    }
+}
+
+impl ToOffset for Anchor {
+    fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
+        let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>();
+        cursor.seek(&self.excerpt_id, Bias::Left, &());
+        if let Some(excerpt) = cursor.item() {
+            if excerpt.id == self.excerpt_id {
+                let buffer_offset = self.text_anchor.to_offset(&excerpt.buffer);
+                return cursor.start().text.bytes
+                    + excerpt.header_height as usize
+                    + buffer_offset.saturating_sub(excerpt.range.start.to_offset(&excerpt.buffer));
+            }
+        }
+        cursor.start().text.bytes
+    }
+}

crates/language/src/multi_buffer/location.rs 🔗

@@ -0,0 +1,76 @@
+use smallvec::{smallvec, SmallVec};
+use std::iter;
+
+pub type ExcerptId = Location;
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Location(SmallVec<[u8; 4]>);
+
+impl Location {
+    pub fn min() -> Self {
+        Self(smallvec![u8::MIN])
+    }
+
+    pub fn max() -> Self {
+        Self(smallvec![u8::MAX])
+    }
+
+    pub fn between(lhs: &Self, rhs: &Self) -> Self {
+        let lhs = lhs.0.iter().copied().chain(iter::repeat(u8::MIN));
+        let rhs = rhs.0.iter().copied().chain(iter::repeat(u8::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)
+    }
+}
+
+impl Default for Location {
+    fn default() -> Self {
+        Self::min()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rand::prelude::*;
+    use std::mem;
+
+    #[gpui::test(iterations = 100)]
+    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/language/src/multi_buffer/selection.rs 🔗

@@ -0,0 +1,91 @@
+use super::{anchor::AnchorRangeMap, MultiBufferSnapshot, ToOffset};
+use std::{ops::Range, sync::Arc};
+use sum_tree::Bias;
+use text::{rope::TextDimension, Selection, SelectionSetId, SelectionState};
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct SelectionSet {
+    pub id: SelectionSetId,
+    pub active: bool,
+    pub selections: Arc<AnchorRangeMap<SelectionState>>,
+}
+
+impl SelectionSet {
+    pub fn len(&self) -> usize {
+        self.selections.len()
+    }
+
+    pub fn selections<'a, D>(
+        &'a self,
+        content: &'a MultiBufferSnapshot,
+    ) -> impl 'a + Iterator<Item = Selection<D>>
+    where
+        D: TextDimension,
+    {
+        self.selections
+            .ranges(content)
+            .map(|(range, state)| Selection {
+                id: state.id,
+                start: range.start,
+                end: range.end,
+                reversed: state.reversed,
+                goal: state.goal,
+            })
+    }
+
+    pub fn intersecting_selections<'a, D, I>(
+        &'a self,
+        range: Range<(I, Bias)>,
+        content: &'a MultiBufferSnapshot,
+    ) -> impl 'a + Iterator<Item = Selection<D>>
+    where
+        D: TextDimension,
+        I: 'a + ToOffset,
+    {
+        self.selections
+            .intersecting_ranges(range, content)
+            .map(|(range, state)| Selection {
+                id: state.id,
+                start: range.start,
+                end: range.end,
+                reversed: state.reversed,
+                goal: state.goal,
+            })
+    }
+
+    pub fn oldest_selection<'a, D>(
+        &'a self,
+        content: &'a MultiBufferSnapshot,
+    ) -> Option<Selection<D>>
+    where
+        D: TextDimension,
+    {
+        self.selections
+            .min_by_key(content, |selection| selection.id)
+            .map(|(range, state)| Selection {
+                id: state.id,
+                start: range.start,
+                end: range.end,
+                reversed: state.reversed,
+                goal: state.goal,
+            })
+    }
+
+    pub fn newest_selection<'a, D>(
+        &'a self,
+        content: &'a MultiBufferSnapshot,
+    ) -> Option<Selection<D>>
+    where
+        D: TextDimension,
+    {
+        self.selections
+            .max_by_key(content, |selection| selection.id)
+            .map(|(range, state)| Selection {
+                id: state.id,
+                start: range.start,
+                end: range.end,
+                reversed: state.reversed,
+                goal: state.goal,
+            })
+    }
+}

crates/text/src/anchor.rs 🔗

@@ -1,6 +1,5 @@
+use super::{FromAnchor, FullOffset, Point, ToOffset};
 use crate::{rope::TextDimension, BufferSnapshot};
-
-use super::{Buffer, FromAnchor, FullOffset, Point, ToOffset};
 use anyhow::Result;
 use std::{
     cmp::Ordering,
@@ -99,7 +98,7 @@ impl Anchor {
         Ok(offset_comparison.then_with(|| self.bias.cmp(&other.bias)))
     }
 
-    pub fn bias_left(&self, buffer: &Buffer) -> Anchor {
+    pub fn bias_left(&self, buffer: &BufferSnapshot) -> Anchor {
         if self.bias == Bias::Left {
             self.clone()
         } else {
@@ -107,7 +106,7 @@ impl Anchor {
         }
     }
 
-    pub fn bias_right(&self, buffer: &Buffer) -> Anchor {
+    pub fn bias_right(&self, buffer: &BufferSnapshot) -> Anchor {
         if self.bias == Bias::Right {
             self.clone()
         } else {

crates/text/src/rope.rs 🔗

@@ -685,6 +685,15 @@ impl sum_tree::Summary for TextSummary {
     }
 }
 
+impl<'a> std::ops::Add<Self> for TextSummary {
+    type Output = Self;
+
+    fn add(mut self, rhs: Self) -> Self::Output {
+        self.add_assign(&rhs);
+        self
+    }
+}
+
 impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
     fn add_assign(&mut self, other: &'a Self) {
         let joined_chars = self.last_line_chars + other.first_line_chars;