wip introduce ExcerptAnchor

Cole Miller created

Change summary

crates/multi_buffer/src/anchor.rs       | 406 +++++++++++---------------
crates/multi_buffer/src/multi_buffer.rs |  49 +--
2 files changed, 198 insertions(+), 257 deletions(-)

Detailed changes

crates/multi_buffer/src/anchor.rs 🔗

@@ -12,6 +12,30 @@ use sum_tree::Bias;
 use text::BufferId;
 use util::debug_panic;
 
+/// A multibuffer anchor derived from an anchor into a specific excerpted buffer.
+#[derive(Clone, Copy, Eq, PartialEq, Hash)]
+pub(crate) struct ExcerptAnchor {
+    /// The position within the excerpt's underlying buffer. This is a stable
+    /// reference that remains valid as the buffer text is edited.
+    pub(crate) timestamp: clock::Lamport,
+
+    /// The byte offset into the text inserted in the operation
+    /// at `timestamp`.
+    pub(crate) offset: u32,
+    /// Whether this anchor stays attached to the character *before* or *after*
+    /// the offset.
+    pub(crate) bias: Bias,
+    pub(crate) buffer_id: BufferId,
+    /// Refers to the path key that the buffer had when this anchor was created,
+    /// so that ordering is stable when the path key for a buffer changes
+    pub(crate) path: PathKeyIndex,
+    /// When present, indicates this anchor points into deleted text within an
+    /// expanded diff hunk. The anchor references a position in the diff base
+    /// (original) text rather than the current buffer text. This is used when
+    /// displaying inline diffs where deleted lines are shown.
+    pub(crate) diff_base_anchor: Option<text::Anchor>,
+}
+
 /// A stable reference to a position within a [`MultiBuffer`](super::MultiBuffer).
 ///
 /// Unlike simple offsets, anchors remain valid as the text is edited, automatically
@@ -19,95 +43,140 @@ use util::debug_panic;
 #[derive(Clone, Copy, Eq, PartialEq, Hash)]
 pub enum Anchor {
     Min,
+    Excerpt(ExcerptAnchor),
     Max,
-    Text {
-        /// The position within the excerpt's underlying buffer. This is a stable
-        /// reference that remains valid as the buffer text is edited.
-        timestamp: clock::Lamport,
-
-        /// The byte offset into the text inserted in the operation
-        /// at `timestamp`.
-        offset: u32,
-        /// Whether this anchor stays attached to the character *before* or *after*
-        /// the offset.
-        bias: Bias,
-        buffer_id: BufferId,
-        /// Refers to the path key that the buffer had when this anchor was created,
-        /// so that ordering is stable when the path key for a buffer changes
-        path: PathKeyIndex,
-        /// When present, indicates this anchor points into deleted text within an
-        /// expanded diff hunk. The anchor references a position in the diff base
-        /// (original) text rather than the current buffer text. This is used when
-        /// displaying inline diffs where deleted lines are shown.
-        diff_base_anchor: Option<text::Anchor>,
-    },
 }
 
-impl std::fmt::Debug for Anchor {
+impl std::fmt::Debug for ExcerptAnchor {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        if self.is_min() {
-            return write!(f, "Anchor::Min");
-        }
-        if self.is_max() {
-            return write!(f, "Anchor::Max");
-        }
-
         f.debug_struct("Anchor")
-            .field("text_anchor", &self.text_anchor().unwrap())
-            .field("diff_base_anchor", &self.diff_base_anchor())
+            .field("text_anchor", &self.text_anchor())
+            .field("diff_base_anchor", &self.diff_base_anchor)
             .finish()
     }
 }
 
-impl Anchor {
-    pub fn text_anchor(&self) -> Option<(PathKeyIndex, text::Anchor, Option<text::Anchor>)> {
+impl std::fmt::Debug for Anchor {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
-            Self::Min | Self::Max => None,
-            Self::Text {
-                timestamp,
-                offset,
-                bias,
-                buffer_id,
-                path,
-                diff_base_anchor,
-                ..
-            } => Some((
-                *path,
-                text::Anchor::new(*timestamp, *offset, *bias, Some(*buffer_id)),
-                *diff_base_anchor,
-            )),
+            Anchor::Min => write!(f, "Anchor::Min"),
+            Anchor::Max => write!(f, "Anchor::Max"),
+            Anchor::Excerpt(excerpt_anchor) => write!(f, "{excerpt_anchor:?}"),
         }
     }
+}
 
-    pub fn diff_base_anchor(&self) -> Option<text::Anchor> {
-        match self {
-            Self::Min | Self::Max => None,
-            Self::Text {
-                diff_base_anchor, ..
-            } => *diff_base_anchor,
+impl ExcerptAnchor {
+    fn text_anchor(&self) -> text::Anchor {
+        text::Anchor::new(self.timestamp, self.offset, self.bias, Some(self.buffer_id))
+    }
+
+    fn with_diff_base_anchor(mut self, diff_base_anchor: text::Anchor) -> Self {
+        self.diff_base_anchor = Some(diff_base_anchor);
+        self
+    }
+
+    fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> Ordering {
+        let Some(self_path_key) = snapshot.path_keys_by_index.get(&self.path) else {
+            panic!("anchor's path was never added to multibuffer")
+        };
+        let Some(other_path_key) = snapshot.path_keys_by_index.get(&other.path) else {
+            panic!("anchor's path was never added to multibuffer")
+        };
+
+        if self_path_key.cmp(other_path_key) != Ordering::Equal {
+            return self_path_key.cmp(other_path_key);
+        }
+
+        // in the case that you removed the buffer containing self,
+        // and added the buffer containing other with the same path key
+        // (ordering is arbitrary but consistent)
+        if self.buffer_id != other.buffer_id {
+            return self.buffer_id.cmp(&other.buffer_id);
+        }
+
+        let Some(buffer) = snapshot.buffer_for_path(&self_path_key) else {
+            return Ordering::Equal;
+        };
+        let text_cmp = self.text_anchor().cmp(&other.text_anchor(), buffer);
+        if text_cmp != Ordering::Equal {
+            return text_cmp;
         }
+
+        if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some())
+            && let Some(base_text) = snapshot
+                .diffs
+                .get(&self.buffer_id)
+                .map(|diff| diff.base_text())
+        {
+            let self_anchor = self.diff_base_anchor.filter(|a| a.is_valid(base_text));
+            let other_anchor = other.diff_base_anchor.filter(|a| a.is_valid(base_text));
+            return match (self_anchor, other_anchor) {
+                (Some(a), Some(b)) => a.cmp(&b, base_text),
+                (Some(_), None) => match other.text_anchor().bias {
+                    Bias::Left => Ordering::Greater,
+                    Bias::Right => Ordering::Less,
+                },
+                (None, Some(_)) => match self.text_anchor().bias {
+                    Bias::Left => Ordering::Less,
+                    Bias::Right => Ordering::Greater,
+                },
+                (None, None) => Ordering::Equal,
+            };
+        }
+
+        Ordering::Equal
     }
 
-    pub fn with_diff_base_anchor(mut self, diff_base_anchor: text::Anchor) -> Self {
-        match &mut self {
-            Self::Min | Self::Max => {
-                debug_panic!("with_diff_base_anchor called on min or max anchor");
-            }
-            Self::Text {
-                diff_base_anchor: self_diff_base_anchor,
-                ..
-            } => {
-                *self_diff_base_anchor = Some(diff_base_anchor);
+    fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Self {
+        if self.bias == Bias::Left {
+            return *self;
+        }
+        let Some(buffer) = snapshot.buffer_for_id(self.buffer_id) else {
+            return *self;
+        };
+        let text_anchor = self.text_anchor().bias_left(&buffer);
+        let ret = Self::in_buffer(self.path, text_anchor);
+        if let Some(diff_base_anchor) = self.diff_base_anchor {
+            if let Some(diff) = snapshot.diffs.get(&self.buffer_id)
+                && diff_base_anchor.is_valid(&diff.base_text())
+            {
+                ret.with_diff_base_anchor(diff_base_anchor.bias_left(diff.base_text()))
+            } else {
+                ret.with_diff_base_anchor(diff_base_anchor)
             }
+        } else {
+            ret
+        }
+    }
+
+    fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Self {
+        if self.bias == Bias::Right {
+            return *self;
+        }
+        let Some(buffer) = snapshot.buffer_for_id(self.buffer_id) else {
+            return *self;
         };
-        self
+        let text_anchor = self.text_anchor().bias_right(&buffer);
+        let ret = Self::in_buffer(self.path, text_anchor);
+        if let Some(diff_base_anchor) = self.diff_base_anchor {
+            if let Some(diff) = snapshot.diffs.get(&self.buffer_id)
+                && diff_base_anchor.is_valid(&diff.base_text())
+            {
+                ret.with_diff_base_anchor(diff_base_anchor.bias_right(diff.base_text()))
+            } else {
+                ret.with_diff_base_anchor(diff_base_anchor)
+            }
+        } else {
+            ret
+        }
     }
 
-    pub fn text(path: PathKeyIndex, text_anchor: text::Anchor) -> Self {
+    fn in_buffer(path: PathKeyIndex, text_anchor: text::Anchor) -> Self {
         let Some(buffer_id) = text_anchor.buffer_id else {
             panic!("text_anchor must have a buffer_id");
         };
-        Self::Text {
+        ExcerptAnchor {
             path,
             diff_base_anchor: None,
             timestamp: text_anchor.timestamp(),
@@ -117,18 +186,32 @@ impl Anchor {
         }
     }
 
-    pub fn range_in_buffer(path: PathKeyIndex, range: Range<text::Anchor>) -> Range<Self> {
-        Self::text(path, range.start)..Self::text(path, range.end)
-    }
-
-    pub fn bias(&self) -> Bias {
-        match self {
-            Self::Min => Bias::Left,
-            Self::Max => Bias::Right,
-            Self::Text { bias, .. } => *bias,
-        }
+    fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
+        let Some(target) = snapshot.try_anchor_seek_target(*self) else {
+            return false;
+        };
+        let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>(());
+        cursor.seek(&target, Bias::Left);
+        let Some(excerpt) = cursor.item() else {
+            return false;
+        };
+        excerpt.buffer.remote_id() == self.buffer_id
+            && excerpt
+                .range
+                .context
+                .start
+                .cmp(&self.text_anchor(), &excerpt.buffer)
+                .is_le()
+            && excerpt
+                .range
+                .context
+                .end
+                .cmp(&self.text_anchor(), &excerpt.buffer)
+                .is_ge()
     }
+}
 
+impl Anchor {
     pub fn min() -> Self {
         Self::Min
     }
@@ -145,87 +228,33 @@ impl Anchor {
         matches!(self, Self::Max)
     }
 
+    pub fn in_buffer(path: PathKeyIndex, text_anchor: text::Anchor) -> Self {
+        Self::Excerpt(ExcerptAnchor::in_buffer(path, text_anchor))
+    }
+
+    pub fn range_in_buffer(path: PathKeyIndex, range: Range<text::Anchor>) -> Range<Self> {
+        Self::in_buffer(path, range.start)..Self::in_buffer(path, range.end)
+    }
+
     pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
-        let (self_text_anchor, self_path, other_text_anchor, other_path) = match (self, other) {
+        match (self, other) {
             (Anchor::Min, Anchor::Min) => return Ordering::Equal,
             (Anchor::Max, Anchor::Max) => return Ordering::Equal,
             (Anchor::Min, _) => return Ordering::Less,
             (Anchor::Max, _) => return Ordering::Greater,
             (_, Anchor::Max) => return Ordering::Less,
             (_, Anchor::Min) => return Ordering::Greater,
-            (
-                Anchor::Text {
-                    path: self_path, ..
-                },
-                Anchor::Text {
-                    path: other_path, ..
-                },
-            ) => (
-                self.text_anchor().unwrap(),
-                self_path,
-                other.text_anchor().unwrap(),
-                other_path,
-            ),
-        };
-        let self_buffer_id = self_text_anchor.buffer_id.unwrap();
-        let other_buffer_id = other_text_anchor.buffer_id.unwrap();
-
-        let Some(self_path_key) = snapshot.path_keys_by_index.get(&self_path) else {
-            panic!("anchor's path was never added to multibuffer")
-        };
-        let Some(other_path_key) = snapshot.path_keys_by_index.get(&other_path) else {
-            panic!("anchor's path was never added to multibuffer")
-        };
-
-        if self_path_key.cmp(other_path_key) != Ordering::Equal {
-            return self_path_key.cmp(other_path_key);
-        }
-
-        // in the case that you removed the buffer contianing self,
-        // and added the buffer containing other with the same path key
-        // (ordering is arbitrary but consistent)
-        if self_buffer_id != other_buffer_id {
-            return self_buffer_id.cmp(&other_buffer_id);
-        }
-
-        let Some(buffer) = snapshot.buffer_for_path(&self_path_key) else {
-            return Ordering::Equal;
-        };
-        let text_cmp = self_text_anchor.cmp(&other_text_anchor, buffer);
-        if text_cmp != Ordering::Equal {
-            return text_cmp;
-        }
-
-        if (self.diff_base_anchor().is_some() || other.diff_base_anchor().is_some())
-            && let Some(base_text) = snapshot
-                .diffs
-                .get(&self_buffer_id)
-                .map(|diff| diff.base_text())
-        {
-            let self_anchor = self.diff_base_anchor().filter(|a| a.is_valid(base_text));
-            let other_anchor = other.diff_base_anchor().filter(|a| a.is_valid(base_text));
-            return match (self_anchor, other_anchor) {
-                (Some(a), Some(b)) => a.cmp(&b, base_text),
-                (Some(_), None) => match other_text_anchor.bias {
-                    Bias::Left => Ordering::Greater,
-                    Bias::Right => Ordering::Less,
-                },
-                (None, Some(_)) => match self_text_anchor.bias {
-                    Bias::Left => Ordering::Less,
-                    Bias::Right => Ordering::Greater,
-                },
-                (None, None) => Ordering::Equal,
-            };
+            (Anchor::Excerpt(self_excerpt_anchor), Anchor::Excerpt(other_excerpt_anchor)) => {
+                self_excerpt_anchor.cmp(other_excerpt_anchor, snapshot)
+            }
         }
-
-        Ordering::Equal
     }
 
     pub fn bias(&self) -> Bias {
         match self {
             Anchor::Min => Bias::Left,
             Anchor::Max => Bias::Right,
-            Anchor::Text { bias, .. } => *bias,
+            Anchor::Excerpt(anchor) => anchor.bias,
         }
     }
 
@@ -233,65 +262,15 @@ impl Anchor {
         match self {
             Anchor::Min => *self,
             Anchor::Max => snapshot.anchor_before(snapshot.max_point()),
-            Anchor::Text {
-                path,
-                bias,
-                buffer_id,
-                ..
-            } => {
-                if *bias == Bias::Left {
-                    return *self;
-                }
-                let Some(buffer) = snapshot.buffer_for_id(*buffer_id) else {
-                    return *self;
-                };
-                let text_anchor = self.text_anchor().unwrap().bias_left(&buffer);
-                let ret = Self::text(*path, text_anchor);
-                if let Some(diff_base_anchor) = self.diff_base_anchor() {
-                    if let Some(diff) = snapshot.diffs.get(&buffer_id)
-                        && diff_base_anchor.is_valid(&diff.base_text())
-                    {
-                        ret.with_diff_base_anchor(diff_base_anchor.bias_left(diff.base_text()))
-                    } else {
-                        ret.with_diff_base_anchor(diff_base_anchor)
-                    }
-                } else {
-                    ret
-                }
-            }
+            Anchor::Excerpt(anchor) => Anchor::Excerpt(anchor.bias_left(snapshot)),
         }
     }
 
     pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
         match self {
-            Anchor::Min => *self,
-            Anchor::Max => snapshot.anchor_after(Point::zero()),
-            Anchor::Text {
-                path,
-                bias,
-                buffer_id,
-                ..
-            } => {
-                if *bias == Bias::Right {
-                    return *self;
-                }
-                let Some(buffer) = snapshot.buffer_for_id(*buffer_id) else {
-                    return *self;
-                };
-                let text_anchor = self.text_anchor().unwrap().bias_right(&buffer);
-                let ret = Self::text(*path, text_anchor);
-                if let Some(diff_base_anchor) = self.diff_base_anchor() {
-                    if let Some(diff) = snapshot.diffs.get(&buffer_id)
-                        && diff_base_anchor.is_valid(&diff.base_text())
-                    {
-                        ret.with_diff_base_anchor(diff_base_anchor.bias_right(diff.base_text()))
-                    } else {
-                        ret.with_diff_base_anchor(diff_base_anchor)
-                    }
-                } else {
-                    ret
-                }
-            }
+            Anchor::Max => *self,
+            Anchor::Min => snapshot.anchor_after(Point::zero()),
+            Anchor::Excerpt(anchor) => Anchor::Excerpt(anchor.bias_right(snapshot)),
         }
     }
 
@@ -309,35 +288,10 @@ impl Anchor {
     }
 
     pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
-        let Some(text_anchor) = self.text_anchor() else {
-            return true;
-        };
-        let Some(buffer_id) = text_anchor.buffer_id else {
-            debug_panic!("missing buffer_id for anchor");
-            return false;
-        };
-
-        let Some(target) = snapshot.anchor_seek_target(*self) else {
-            return false;
-        };
-        let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>(());
-        cursor.seek(&target, Bias::Left);
-        let Some(excerpt) = cursor.item() else {
-            return false;
-        };
-        excerpt.buffer.remote_id() == buffer_id
-            && excerpt
-                .range
-                .context
-                .start
-                .cmp(&text_anchor, &excerpt.buffer)
-                .is_le()
-            && excerpt
-                .range
-                .context
-                .end
-                .cmp(&text_anchor, &excerpt.buffer)
-                .is_ge()
+        match self {
+            Anchor::Min | Anchor::Max => true,
+            Anchor::Excerpt(excerpt_anchor) => excerpt_anchor.is_valid(snapshot),
+        }
     }
 }
 

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -4,6 +4,8 @@ mod multi_buffer_tests;
 mod path_key;
 mod transaction;
 
+use crate::anchor::ExcerptAnchor;
+
 use self::transaction::History;
 
 pub use anchor::{Anchor, AnchorRangeExt};
@@ -6518,21 +6520,20 @@ impl MultiBufferSnapshot {
         ))
     }
 
+    fn try_anchor_seek_target(&self, anchor: ExcerptAnchor) -> Option<AnchorSeekTarget> {
+        let path_key = self.try_path_for_anchor(anchor)?;
+        let snapshot = self.buffer_for_path(&path_key);
+
+        Some(AnchorSeekTarget::Text {
+            path_key: path_key.clone(),
+            anchor: anchor.text_anchor(),
+            snapshot: snapshot.cloned(),
+        })
+    }
+
     fn anchor_seek_target(&self, anchor: Anchor) -> AnchorSeekTarget {
-        match anchor {
-            Anchor::Min => AnchorSeekTarget::Min,
-            Anchor::Max => AnchorSeekTarget::Max,
-            Anchor::Text { path, .. } => {
-                let path_key = self.path_for_anchor(anchor);
-                let snapshot = self.buffer_for_path(path_key);
-
-                Some(AnchorSeekTarget::Text {
-                    path_key: path_key.clone(),
-                    anchor: anchor.text_anchor().unwrap(),
-                    snapshot: snapshot.cloned(),
-                })
-            }
-        }
+        self.try_anchor_seek_target(anchor)
+            .expect("invalid anchor: path was never added to multibuffer")
     }
 
     fn excerpt_locator_for_id(&self, id: ExcerptId) -> &Locator {
@@ -6611,25 +6612,11 @@ impl MultiBufferSnapshot {
         self.buffer_for_path(self.path_keys_by_buffer.get(&id)?)
     }
 
-    pub fn try_path_for_anchor(&self, anchor: Anchor) -> Option<PathKey> {
-        match anchor {
-            Anchor::Min => Some(
-                self.excerpts
-                    .first()
-                    .map(|excerpt| excerpt.path_key.clone())
-                    .unwrap_or(PathKey::min()),
-            ),
-            Anchor::Max => Some(
-                self.excerpts
-                    .last()
-                    .map(|excerpt| excerpt.path_key.clone())
-                    .unwrap_or(PathKey::min()),
-            ),
-            Anchor::Text { path, .. } => self.path_keys_by_index.get(&path).cloned(),
-        }
+    fn try_path_for_anchor(&self, anchor: ExcerptAnchor) -> Option<PathKey> {
+        self.path_keys_by_index.get(&anchor.path).cloned()
     }
 
-    pub fn path_for_anchor(&self, anchor: Anchor) -> PathKey {
+    fn path_for_anchor(&self, anchor: ExcerptAnchor) -> PathKey {
         self.try_path_for_anchor(anchor)
             .expect("invalid anchor: path was never added to multibuffer")
     }