From e34bfe217f73adb1ad831c0aaf01dea08ec48425 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 2 Mar 2026 18:08:29 -0500 Subject: [PATCH] wip introduce ExcerptAnchor --- crates/multi_buffer/src/anchor.rs | 406 +++++++++++------------- crates/multi_buffer/src/multi_buffer.rs | 49 ++- 2 files changed, 198 insertions(+), 257 deletions(-) diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 9005bc5c96d8ff9b73d2e7d44e1cb61d4ea77191..118b9efc9296b8fe21b47404907a922640dd1661 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/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, +} + /// 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, - }, } -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)> { +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 { - 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) -> Range { - 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::(()); + 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) -> Range { + 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::(()); - 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), + } } } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 8cfebc165ae352ab8ab4e3b4ece6a443ee9c5e25..ff952d0284b2b13e0bd2c1dc5dd4d607b28b0815 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/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 { + 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 { - 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 { + 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") }