wip

Cole Miller and Conrad Irwin created

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/action_log/src/action_log.rs               | 10 ++
crates/agent_ui/src/message_editor.rs             |  2 
crates/buffer_diff/src/buffer_diff.rs             |  6 
crates/edit_prediction/src/capture_example.rs     |  2 
crates/edit_prediction/src/edit_prediction.rs     | 10 ++
crates/edit_prediction/src/udiff.rs               |  3 
crates/editor/src/code_completion_tests.rs        |  4 
crates/editor/src/editor_tests.rs                 |  2 
crates/git_ui/src/commit_view.rs                  | 10 ++
crates/git_ui/src/project_diff.rs                 |  4 
crates/language/src/diagnostic_set.rs             | 22 ++----
crates/language/src/proto.rs                      |  4 
crates/language/src/syntax_map.rs                 | 30 ++++-----
crates/multi_buffer/src/anchor.rs                 | 53 ++++------------
crates/multi_buffer/src/multi_buffer.rs           | 51 +++++++--------
crates/multi_buffer/src/multi_buffer_tests.rs     |  8 ++
crates/multi_buffer/src/path_key.rs               |  3 
crates/project/tests/integration/project_tests.rs | 33 ++++++---
crates/text/src/anchor.rs                         | 31 +--------
crates/text/src/text.rs                           | 23 ++++--
20 files changed, 147 insertions(+), 164 deletions(-)

Detailed changes

crates/action_log/src/action_log.rs 🔗

@@ -624,6 +624,7 @@ impl ActionLog {
                 let task = if let Some(existing_file_content) = existing_file_content {
                     // Capture the agent's content before restoring existing file content
                     let agent_content = buffer.read(cx).text();
+                    let buffer_id = buffer.read(cx).remote_id();
 
                     buffer.update(cx, |buffer, cx| {
                         buffer.start_transaction();
@@ -636,7 +637,10 @@ impl ActionLog {
 
                     undo_info = Some(PerBufferUndo {
                         buffer: buffer.downgrade(),
-                        edits_to_restore: vec![(Anchor::MIN..Anchor::MAX, agent_content)],
+                        edits_to_restore: vec![(
+                            Anchor::min_for_buffer(buffer_id)..Anchor::max_for_buffer(buffer_id),
+                            agent_content,
+                        )],
                         status: UndoBufferStatus::Created {
                             had_existing_content: true,
                         },
@@ -876,8 +880,8 @@ impl ActionLog {
                 let mut valid_edits = Vec::new();
 
                 for (anchor_range, text_to_restore) in per_buffer_undo.edits_to_restore {
-                    if anchor_range.start.buffer_id == Some(buffer.remote_id())
-                        && anchor_range.end.buffer_id == Some(buffer.remote_id())
+                    if anchor_range.start.buffer_id == buffer.remote_id()
+                        && anchor_range.end.buffer_id == buffer.remote_id()
                     {
                         valid_edits.push((anchor_range, text_to_restore));
                     }

crates/agent_ui/src/message_editor.rs 🔗

@@ -1645,7 +1645,7 @@ mod tests {
             completion_provider.completions(
                 excerpt_id,
                 &buffer,
-                text::Anchor::MAX,
+                text::Anchor::max_for_buffer(buffer.read(cx).remote_id()),
                 CompletionContext {
                     trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
                     trigger_character: Some("@".into()),

crates/buffer_diff/src/buffer_diff.rs 🔗

@@ -158,9 +158,9 @@ impl sum_tree::Item for PendingHunk {
 impl sum_tree::Summary for DiffHunkSummary {
     type Context<'a> = &'a text::BufferSnapshot;
 
-    fn zero(_cx: Self::Context<'_>) -> Self {
+    fn zero(buffer: &text::BufferSnapshot) -> Self {
         DiffHunkSummary {
-            buffer_range: Anchor::MIN..Anchor::MIN,
+            buffer_range: Anchor::min_min_range_for_buffer(buffer.remote_id()),
             diff_base_byte_range: 0..0,
         }
     }
@@ -1614,7 +1614,7 @@ impl BufferDiff {
     ) {
         let hunks = self
             .snapshot(cx)
-            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer)
+            .hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer.remote_id()), buffer)
             .collect::<Vec<_>>();
         let Some(secondary) = self.secondary_diff.clone() else {
             return;

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -1995,7 +1995,10 @@ impl EditPredictionStore {
         cx: &mut AsyncApp,
     ) -> Result<Option<(Entity<Buffer>, language::Anchor)>> {
         let collaborator_cursor_rows: Vec<u32> = active_buffer_snapshot
-            .selections_in_range(Anchor::MIN..Anchor::MAX, false)
+            .selections_in_range(
+                Anchor::min_max_range_for_buffer(active_buffer_snapshot.remote_id()),
+                false,
+            )
             .flat_map(|(_, _, _, selections)| {
                 selections.map(|s| s.head().to_point(active_buffer_snapshot).row)
             })
@@ -2071,7 +2074,10 @@ impl EditPredictionStore {
                     candidate_buffer.read_with(cx, |buffer, _cx| {
                         let snapshot = buffer.snapshot();
                         let has_collaborators = snapshot
-                            .selections_in_range(Anchor::MIN..Anchor::MAX, false)
+                            .selections_in_range(
+                                Anchor::max_max_range_for_buffer(snapshot.remote_id()),
+                                false,
+                            )
                             .next()
                             .is_some();
                         let position = buffer

crates/edit_prediction/src/udiff.rs 🔗

@@ -54,7 +54,6 @@ pub async fn apply_diff(
 
     let mut included_files: HashMap<String, Entity<Buffer>> = HashMap::default();
 
-    let ranges = [Anchor::MIN..Anchor::MAX];
     let mut diff = DiffParser::new(diff_str);
     let mut current_file = None;
     let mut edits: Vec<(std::ops::Range<Anchor>, Arc<str>)> = vec![];
@@ -115,7 +114,7 @@ pub async fn apply_diff(
                     edits.extend(resolve_hunk_edits_in_buffer(
                         hunk,
                         buffer,
-                        ranges.as_slice(),
+                        &[Anchor::min_max_range_for_buffer(buffer.remote_id())],
                         status,
                     )?);
                     anyhow::Ok(())

crates/editor/src/code_completion_tests.rs 🔗

@@ -7,7 +7,7 @@ use project::{Completion, CompletionSource};
 use settings::SnippetSortOrder;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
-use text::Anchor;
+use text::{Anchor, BufferId};
 
 #[gpui::test]
 async fn test_sort_kind(cx: &mut TestAppContext) {
@@ -393,7 +393,7 @@ impl CompletionBuilder {
         kind: Option<CompletionItemKind>,
     ) -> Completion {
         Completion {
-            replace_range: Anchor::MIN..Anchor::MAX,
+            replace_range: Anchor::min_max_range_for_buffer(BufferId::new(1)),
             new_text: label.to_string(),
             label: CodeLabel::plain(label.to_string(), filter_text),
             documentation: None,

crates/editor/src/editor_tests.rs 🔗

@@ -23412,7 +23412,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
             multibuffer.set_excerpts_for_path(
                 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
                 buffer.clone(),
-                vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
+                vec![Point::zero()..snapshot.max_point()],
                 2,
                 cx,
             );

crates/git_ui/src/commit_view.rs 🔗

@@ -223,7 +223,11 @@ impl CommitView {
                     editor
                         .buffer()
                         .read(cx)
-                        .buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx)
+                        .buffer_anchor_to_anchor(
+                            &message_buffer,
+                            Anchor::max_for_buffer(&message_buffer.read(cx).remote_id()),
+                            cx,
+                        )
                         .map(|anchor| BlockProperties {
                             placement: BlockPlacement::Below(anchor),
                             height: Some(1),
@@ -431,7 +435,9 @@ impl CommitView {
 
             let base_text = diff.base_text();
 
-            for hunk in diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer) {
+            for hunk in
+                diff.hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer_id), buffer)
+            {
                 let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
                 total_additions += added_rows;
 

crates/git_ui/src/project_diff.rs 🔗

@@ -559,7 +559,9 @@ impl ProjectDiff {
 
             let base_text = diff.base_text();
 
-            for hunk in diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer) {
+            for hunk in
+                diff.hunks_intersecting_range(Anchor::min_max_range_for_buffer(buffer_id), buffer)
+            {
                 let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
                 total_additions += added_rows;
 

crates/language/src/diagnostic_set.rs 🔗

@@ -326,23 +326,17 @@ impl DiagnosticEntry<Anchor> {
     }
 }
 
-impl Default for Summary {
-    fn default() -> Self {
-        Self {
-            start: Anchor::MIN,
-            end: Anchor::MAX,
-            min_start: Anchor::MAX,
-            max_end: Anchor::MIN,
-            count: 0,
-        }
-    }
-}
-
 impl sum_tree::Summary for Summary {
     type Context<'a> = &'a text::BufferSnapshot;
 
-    fn zero(_cx: Self::Context<'_>) -> Self {
-        Default::default()
+    fn zero(buffer: &text::BufferSnapshot) -> Self {
+        Self {
+            start: Anchor::min_for_buffer(buffer.remote_id()),
+            end: Anchor::max_for_buffer(buffer.remote_id()),
+            min_start: Anchor::max_for_buffer(buffer.remote_id()),
+            max_end: Anchor::min_for_buffer(buffer.remote_id()),
+            count: 0,
+        }
     }
 
     fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {

crates/language/src/proto.rs 🔗

@@ -260,7 +260,7 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
             Bias::Left => proto::Bias::Left as i32,
             Bias::Right => proto::Bias::Right as i32,
         },
-        buffer_id: anchor.buffer_id.map(Into::into),
+        buffer_id: Some(anchor.buffer_id.into()),
     }
 }
 
@@ -498,7 +498,7 @@ pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
         timestamp,
         anchor.offset as u32,
         bias,
-        buffer_id,
+        buffer_id.expect("FIXME rework anchor ser/de"),
     ))
 }
 

crates/language/src/syntax_map.rs 🔗

@@ -18,7 +18,7 @@ use std::{
 };
 use streaming_iterator::StreamingIterator;
 use sum_tree::{Bias, Dimensions, SeekTarget, SumTree};
-use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint};
+use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint};
 use tree_sitter::{
     Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatch, QueryMatches,
     QueryPredicateArg,
@@ -567,7 +567,7 @@ impl SyntaxSnapshot {
 
                 let bounded_position = SyntaxLayerPositionBeforeChange {
                     position: position.clone(),
-                    change: changed_regions.start_position(),
+                    change: changed_regions.start_position(text.remote_id()),
                 };
                 if bounded_position.cmp(cursor.start(), text).is_gt() {
                     let slice = cursor.slice(&bounded_position, Bias::Left);
@@ -1925,11 +1925,11 @@ impl ChangedRegion {
 }
 
 impl ChangeRegionSet {
-    fn start_position(&self) -> ChangeStartPosition {
+    fn start_position(&self, buffer_id: BufferId) -> ChangeStartPosition {
         self.0.first().map_or(
             ChangeStartPosition {
                 depth: usize::MAX,
-                position: Anchor::MAX,
+                position: Anchor::max_for_buffer(buffer_id),
             },
             |region| ChangeStartPosition {
                 depth: region.depth,
@@ -1978,32 +1978,28 @@ impl ChangeRegionSet {
     }
 }
 
-impl Default for SyntaxLayerSummary {
-    fn default() -> Self {
+impl sum_tree::Summary for SyntaxLayerSummary {
+    type Context<'a> = &'a BufferSnapshot;
+
+    fn zero(buffer: &BufferSnapshot) -> Self {
         Self {
             max_depth: 0,
             min_depth: 0,
-            range: Anchor::MAX..Anchor::MIN,
-            last_layer_range: Anchor::MIN..Anchor::MAX,
+            range: Anchor::max_for_buffer(buffer.remote_id())
+                ..Anchor::min_for_buffer(buffer.remote_id()),
+            last_layer_range: Anchor::min_for_buffer(buffer.remote_id())
+                ..Anchor::max_for_buffer(buffer.remote_id()),
             last_layer_language: None,
             contains_unknown_injections: false,
         }
     }
-}
-
-impl sum_tree::Summary for SyntaxLayerSummary {
-    type Context<'a> = &'a BufferSnapshot;
-
-    fn zero(_cx: &BufferSnapshot) -> Self {
-        Default::default()
-    }
 
     fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
         if other.max_depth > self.max_depth {
             self.max_depth = other.max_depth;
             self.range = other.range.clone();
         } else {
-            if self.range == (Anchor::MAX..Anchor::MAX) {
+            if self.range.start.is_min() && self.range.end.is_max() {
                 self.range.start = other.range.start;
             }
             if other.range.end.cmp(&self.range.end, buffer).is_gt() {

crates/multi_buffer/src/anchor.rs 🔗

@@ -10,29 +10,12 @@ use std::{
     ops::{Add, AddAssign, Range, Sub},
 };
 use sum_tree::Bias;
-use text::BufferId;
 
 /// A multibuffer anchor derived from an anchor into a specific excerpted buffer.
 #[derive(Clone, Copy, Eq, PartialEq, Hash)]
 pub 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) text_anchor: text::Anchor,
     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>,
 }
 
@@ -50,7 +33,7 @@ pub enum Anchor {
 impl std::fmt::Debug for ExcerptAnchor {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("Anchor")
-            .field("text_anchor", &self.text_anchor())
+            .field("text_anchor", &self.text_anchor)
             .field("diff_base_anchor", &self.diff_base_anchor)
             .finish()
     }
@@ -86,7 +69,7 @@ impl From<ExcerptAnchor> for Anchor {
 
 impl ExcerptAnchor {
     pub(crate) fn text_anchor(&self) -> text::Anchor {
-        text::Anchor::new(self.timestamp, self.offset, self.bias, Some(self.buffer_id))
+        self.text_anchor
     }
 
     pub(crate) fn with_diff_base_anchor(mut self, diff_base_anchor: text::Anchor) -> Self {
@@ -109,8 +92,8 @@ impl ExcerptAnchor {
         // 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);
+        if self.text_anchor.buffer_id != other.text_anchor.buffer_id {
+            return self.text_anchor.buffer_id.cmp(&other.text_anchor.buffer_id);
         }
 
         let Some(buffer) = snapshot.buffer_for_path(&self_path_key) else {
@@ -124,7 +107,7 @@ impl ExcerptAnchor {
         if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some())
             && let Some(base_text) = snapshot
                 .diffs
-                .get(&self.buffer_id)
+                .get(&self.text_anchor.buffer_id)
                 .map(|diff| diff.base_text())
         {
             let self_anchor = self.diff_base_anchor.filter(|a| a.is_valid(base_text));
@@ -147,16 +130,16 @@ impl ExcerptAnchor {
     }
 
     fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Self {
-        if self.bias == Bias::Left {
+        if self.text_anchor.bias == Bias::Left {
             return *self;
         }
-        let Some(buffer) = snapshot.buffer_for_id(self.buffer_id) else {
+        let Some(buffer) = snapshot.buffer_for_id(self.text_anchor.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)
+            if let Some(diff) = snapshot.diffs.get(&self.text_anchor.buffer_id)
                 && diff_base_anchor.is_valid(&diff.base_text())
             {
                 ret.with_diff_base_anchor(diff_base_anchor.bias_left(diff.base_text()))
@@ -169,16 +152,16 @@ impl ExcerptAnchor {
     }
 
     fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Self {
-        if self.bias == Bias::Right {
+        if self.text_anchor.bias == Bias::Right {
             return *self;
         }
-        let Some(buffer) = snapshot.buffer_for_id(self.buffer_id) else {
+        let Some(buffer) = snapshot.buffer_for_id(self.text_anchor.buffer_id) else {
             return *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)
+            if let Some(diff) = snapshot.diffs.get(&self.text_anchor.buffer_id)
                 && diff_base_anchor.is_valid(&diff.base_text())
             {
                 ret.with_diff_base_anchor(diff_base_anchor.bias_right(diff.base_text()))
@@ -192,16 +175,10 @@ impl ExcerptAnchor {
 
     #[track_caller]
     pub(crate) 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");
-        };
         ExcerptAnchor {
             path,
             diff_base_anchor: None,
-            timestamp: text_anchor.timestamp(),
-            buffer_id,
-            offset: text_anchor.offset,
-            bias: text_anchor.bias,
+            text_anchor,
         }
     }
 
@@ -214,7 +191,7 @@ impl ExcerptAnchor {
         let Some(excerpt) = cursor.item() else {
             return false;
         };
-        excerpt.buffer_id == self.buffer_id
+        excerpt.buffer_id == self.text_anchor.buffer_id
             && excerpt
                 .range
                 .context
@@ -291,7 +268,7 @@ impl Anchor {
         match self {
             Anchor::Min => Bias::Left,
             Anchor::Max => Bias::Right,
-            Anchor::Excerpt(anchor) => anchor.bias,
+            Anchor::Excerpt(anchor) => anchor.text_anchor.bias,
         }
     }
 

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -801,7 +801,7 @@ impl ExcerptRange<text::Anchor> {
 #[derive(Clone, Debug)]
 pub struct ExcerptSummary {
     path_key: PathKey,
-    max_anchor: text::Anchor,
+    max_anchor: Option<text::Anchor>,
     widest_line_number: u32,
     text: MBTextSummary,
     count: usize,
@@ -811,7 +811,7 @@ impl ExcerptSummary {
     pub fn min() -> Self {
         ExcerptSummary {
             path_key: PathKey::min(),
-            max_anchor: text::Anchor::MIN,
+            max_anchor: None,
             widest_line_number: 0,
             text: MBTextSummary::default(),
             count: 0,
@@ -1660,7 +1660,7 @@ impl MultiBuffer {
                         .start
                         .excerpt_anchor()
                         .map(|excerpt_anchor| excerpt_anchor.text_anchor())
-                        .unwrap_or(text::Anchor::MIN),
+                        .unwrap_or(text::Anchor::min_for_buffer(excerpt.buffer_id)),
                     buffer,
                 )
                 .clone();
@@ -1670,7 +1670,7 @@ impl MultiBuffer {
                         .end
                         .excerpt_anchor()
                         .map(|excerpt_anchor| excerpt_anchor.text_anchor())
-                        .unwrap_or(text::Anchor::MAX),
+                        .unwrap_or(text::Anchor::max_for_buffer(excerpt.buffer_id)),
                     buffer,
                 )
                 .clone();
@@ -1842,7 +1842,7 @@ impl MultiBuffer {
     pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option<Entity<Buffer>> {
         match anchor {
             Anchor::Min => Some(self.snapshot(cx).excerpts.first()?.buffer(self)),
-            Anchor::Excerpt(excerpt_anchor) => self.buffer(excerpt_anchor.buffer_id),
+            Anchor::Excerpt(excerpt_anchor) => self.buffer(excerpt_anchor.text_anchor.buffer_id),
             Anchor::Max => Some(self.snapshot(cx).excerpts.first()?.buffer(self)),
         }
     }
@@ -1935,14 +1935,14 @@ impl MultiBuffer {
         let mut futures = Vec::new();
         for anchor in anchors {
             if let Some(excerpt_anchor) = anchor.excerpt_anchor() {
-                if let Some(buffer) = self.buffers.get(&excerpt_anchor.buffer_id) {
+                if let Some(buffer) = self.buffers.get(&excerpt_anchor.text_anchor.buffer_id) {
                     buffer.buffer.update(cx, |buffer, _| {
                         futures.push(buffer.wait_for_anchors([excerpt_anchor.text_anchor()]))
                     });
                 } else {
                     error = Some(anyhow!(
                         "buffer {:?} is not part of this multi-buffer",
-                        excerpt_anchor.buffer_id
+                        excerpt_anchor.text_anchor.buffer_id
                     ));
                     break;
                 }
@@ -1966,7 +1966,11 @@ impl MultiBuffer {
     ) -> Option<(Entity<Buffer>, text::Anchor)> {
         let snapshot = self.read(cx);
         let anchor = snapshot.anchor_before(position).excerpt_anchor()?;
-        let buffer = self.buffers.get(&anchor.buffer_id)?.buffer.clone();
+        let buffer = self
+            .buffers
+            .get(&anchor.text_anchor.buffer_id)?
+            .buffer
+            .clone();
         Some((buffer, anchor.text_anchor()))
     }
 
@@ -3591,13 +3595,8 @@ impl MultiBufferSnapshot {
         let mut anchors = anchors.peekable();
         let mut cursor = self.excerpts.cursor::<ExcerptSummary>(());
         'anchors: while let Some(anchor) = anchors.peek() {
-            let Some(buffer_id) = anchor.buffer_id else {
-                anchors.next();
-                result.push(None);
-                continue 'anchors;
-            };
-            let mut same_buffer_anchors =
-                anchors.peeking_take_while(|a| a.buffer_id.is_some_and(|b| buffer_id == b));
+            let buffer_id = anchor.buffer_id;
+            let mut same_buffer_anchors = anchors.peeking_take_while(|a| a.buffer_id == buffer_id);
 
             if let Some(buffer) = self.buffers.get(&buffer_id) {
                 let path = &buffer.path_key;
@@ -4920,7 +4919,7 @@ impl MultiBufferSnapshot {
 
             // A right-biased anchor at a transform boundary belongs to the
             // *next* transform, so advance past the current one.
-            if anchor.bias == Bias::Right && at_transform_end {
+            if anchor.text_anchor.bias == Bias::Right && at_transform_end {
                 diff_transforms.next();
                 continue;
             }
@@ -5074,6 +5073,7 @@ impl MultiBufferSnapshot {
 
         let mut summaries = Vec::new();
         while let Some(anchor) = anchors.peek() {
+            dbg!(&anchor);
             let target = anchor.seek_target(self);
             let excerpt_anchor = match anchor {
                 Anchor::Min => {
@@ -5121,6 +5121,7 @@ impl MultiBufferSnapshot {
                             if !excerpt.contains(&excerpt_anchor, self) {
                                 return None;
                             }
+                            anchors.next();
                             Some((excerpt_anchor.text_anchor(), excerpt_anchor))
                         }),
                     )
@@ -5304,10 +5305,7 @@ impl MultiBufferSnapshot {
     /// Wraps the [`text::Anchor`] in a [`crate::Anchor`] if this multi-buffer is a singleton.
     pub fn as_singleton_anchor(&self, text_anchor: text::Anchor) -> Option<Anchor> {
         let buffer = self.as_singleton()?;
-        if text_anchor
-            .buffer_id
-            .is_none_or(|id| id == buffer.remote_id())
-        {
+        if text_anchor.buffer_id == buffer.remote_id() {
             // todo!() bit hacky, but always right for singletons
             Some(Anchor::in_buffer(PathKeyIndex(0), text_anchor))
         } else {
@@ -6242,7 +6240,7 @@ impl MultiBufferSnapshot {
                     anchor
                         .excerpt_anchor()
                         .map(|anchor| anchor.text_anchor())
-                        .unwrap_or(text::Anchor::MIN),
+                        .unwrap_or(text::Anchor::min_for_buffer(buffer_snapshot.remote_id())),
                     theme,
                 )
                 .into_iter()
@@ -6376,7 +6374,7 @@ impl MultiBufferSnapshot {
     pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option<BufferId> {
         match anchor {
             Anchor::Min => self.excerpts.first().map(|excerpt| excerpt.buffer_id),
-            Anchor::Excerpt(excerpt_anchor) => Some(excerpt_anchor.buffer_id),
+            Anchor::Excerpt(excerpt_anchor) => Some(excerpt_anchor.text_anchor.buffer_id),
             Anchor::Max => self.excerpts.last().map(|excerpt| excerpt.buffer_id),
         }
     }
@@ -7051,7 +7049,7 @@ impl Excerpt {
 
     pub(crate) fn contains(&self, anchor: &ExcerptAnchor, snapshot: &MultiBufferSnapshot) -> bool {
         self.path_key_index == anchor.path
-            && self.buffer_id == anchor.buffer_id
+            && self.buffer_id == anchor.text_anchor.buffer_id
             && self
                 .range
                 .contains(&anchor.text_anchor(), self.buffer_snapshot(snapshot))
@@ -7245,7 +7243,7 @@ impl sum_tree::Item for Excerpt {
         }
         ExcerptSummary {
             path_key: self.path_key.clone(),
-            max_anchor: self.range.context.end,
+            max_anchor: Some(self.range.context.end),
             widest_line_number: self.max_buffer_row,
             text: text.into(),
             count: 1,
@@ -7372,14 +7370,13 @@ impl sum_tree::SeekTarget<'_, ExcerptSummary, ExcerptSummary> for AnchorSeekTarg
                     // the goal is that if you seek to buffer_id 1 (path key a)
                     // and you find that path key a now belongs to buffer_id 2,
                     // we'll seek past buffer_id 2.
-                    if let Some(buffer_id) = cursor_location.max_anchor.buffer_id
-                        && buffer_id != snapshot.remote_id()
+                    if cursor_location.max_anchor.map(|a| a.buffer_id) != Some(snapshot.remote_id())
                     {
                         Ordering::Greater
                     } else {
                         anchor
                             .text_anchor()
-                            .cmp(&cursor_location.max_anchor, snapshot)
+                            .cmp(&cursor_location.max_anchor.unwrap(), snapshot)
                     }
                 } else {
                     // shouldn't happen because we expect this buffer not to have any excerpts

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -3623,6 +3623,7 @@ async fn test_summaries_for_anchors(cx: &mut TestAppContext) {
     });
     cx.run_until_parked();
 
+    dbg!("BEFORE");
     let multibuffer = cx.new(|cx| {
         let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
         multibuffer.set_all_diff_hunks_expanded(cx);
@@ -3645,10 +3646,12 @@ async fn test_summaries_for_anchors(cx: &mut TestAppContext) {
         multibuffer
     });
 
+    dbg!("BEFORE");
     let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
         (multibuffer.snapshot(cx), multibuffer.subscribe())
     });
 
+    dbg!("BEFORE");
     assert_new_snapshot(
         &multibuffer,
         &mut snapshot,
@@ -3665,6 +3668,7 @@ async fn test_summaries_for_anchors(cx: &mut TestAppContext) {
         ),
     );
 
+    dbg!("BEFORE");
     let anchor_1 = multibuffer.read_with(cx, |multibuffer, cx| {
         multibuffer
             .buffer_anchor_to_anchor(
@@ -3674,9 +3678,11 @@ async fn test_summaries_for_anchors(cx: &mut TestAppContext) {
             )
             .unwrap()
     });
+    dbg!("BEFORE");
     let point_1 = snapshot.summaries_for_anchors::<Point, _>([&anchor_1])[0];
     assert_eq!(point_1, Point::new(0, 0));
 
+    dbg!("BEFORE");
     let anchor_2 = multibuffer.read_with(cx, |multibuffer, cx| {
         multibuffer
             .buffer_anchor_to_anchor(
@@ -3686,7 +3692,9 @@ async fn test_summaries_for_anchors(cx: &mut TestAppContext) {
             )
             .unwrap()
     });
+    dbg!("BEFORE");
     let point_2 = snapshot.summaries_for_anchors::<Point, _>([&anchor_2])[0];
+    dbg!("AFTER");
     assert_eq!(point_2, Point::new(3, 0));
 }
 

crates/multi_buffer/src/path_key.rs 🔗

@@ -545,10 +545,11 @@ impl MultiBuffer {
             })
         }
         drop(cursor);
-        snapshot.excerpts = new_excerpts;
         if changed_trailing_excerpt {
             snapshot.trailing_excerpt_update_count += 1;
+            new_excerpts.update_last(|excerpt| excerpt.has_trailing_newline = false, ())
         }
+        snapshot.excerpts = new_excerpts;
 
         let edits =
             Self::sync_diff_transforms(&mut snapshot, vec![edit], DiffChangeKind::BufferEdited);

crates/project/tests/integration/project_tests.rs 🔗

@@ -1784,7 +1784,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
             DiagnosticSet::from_sorted_entries(
                 vec![DiagnosticEntry {
                     diagnostic: Default::default(),
-                    range: Anchor::MIN..Anchor::MAX,
+                    range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
                 }],
                 &buffer.snapshot(),
             ),
@@ -7986,9 +7986,10 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     unstaged_diff.update(cx, |unstaged_diff, cx| {
         let snapshot = buffer.read(cx).snapshot();
         assert_hunks(
-            unstaged_diff
-                .snapshot(cx)
-                .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            unstaged_diff.snapshot(cx).hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(snapshot.remote_id()),
+                &snapshot,
+            ),
             &snapshot,
             &unstaged_diff.base_text(cx).text(),
             &[(
@@ -8077,8 +8078,10 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     diff_1.update(cx, |diff, cx| {
         let snapshot = buffer_1.read(cx).snapshot();
         assert_hunks(
-            diff.snapshot(cx)
-                .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            diff.snapshot(cx).hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(snapshot.remote_id()),
+                &snapshot,
+            ),
             &snapshot,
             &diff.base_text_string(cx).unwrap(),
             &[
@@ -8119,8 +8122,10 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     diff_1.update(cx, |diff, cx| {
         let snapshot = buffer_1.read(cx).snapshot();
         assert_hunks(
-            diff.snapshot(cx)
-                .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            diff.snapshot(cx).hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(snapshot.remote_id()),
+                &snapshot,
+            ),
             &snapshot,
             &diff.base_text(cx).text(),
             &[(
@@ -8149,8 +8154,10 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     diff_2.update(cx, |diff, cx| {
         let snapshot = buffer_2.read(cx).snapshot();
         assert_hunks(
-            diff.snapshot(cx)
-                .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            diff.snapshot(cx).hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(snapshot.remote_id()),
+                &snapshot,
+            ),
             &snapshot,
             &diff.base_text_string(cx).unwrap(),
             &[(
@@ -8171,8 +8178,10 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
     diff_2.update(cx, |diff, cx| {
         let snapshot = buffer_2.read(cx).snapshot();
         assert_hunks(
-            diff.snapshot(cx)
-                .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
+            diff.snapshot(cx).hunks_intersecting_range(
+                Anchor::min_max_range_for_buffer(snapshot.remote_id()),
+                &snapshot,
+            ),
             &snapshot,
             &diff.base_text_string(cx).unwrap(),
             &[(

crates/text/src/anchor.rs 🔗

@@ -24,7 +24,7 @@ pub struct Anchor {
     /// Whether this anchor stays attached to the character *before* or *after*
     /// the offset.
     pub bias: Bias,
-    pub buffer_id: Option<BufferId>,
+    pub buffer_id: BufferId,
 }
 
 impl Debug for Anchor {
@@ -46,28 +46,7 @@ impl Debug for Anchor {
 }
 
 impl Anchor {
-    pub const MIN: Self = Self {
-        timestamp_replica_id: clock::Lamport::MIN.replica_id,
-        timestamp_value: clock::Lamport::MIN.value,
-        offset: u32::MIN,
-        bias: Bias::Left,
-        buffer_id: None,
-    };
-
-    pub const MAX: Self = Self {
-        timestamp_replica_id: clock::Lamport::MAX.replica_id,
-        timestamp_value: clock::Lamport::MAX.value,
-        offset: u32::MAX,
-        bias: Bias::Right,
-        buffer_id: None,
-    };
-
-    pub fn new(
-        timestamp: clock::Lamport,
-        offset: u32,
-        bias: Bias,
-        buffer_id: Option<BufferId>,
-    ) -> Self {
+    pub fn new(timestamp: clock::Lamport, offset: u32, bias: Bias, buffer_id: BufferId) -> Self {
         Self {
             timestamp_replica_id: timestamp.replica_id,
             timestamp_value: timestamp.value,
@@ -83,7 +62,7 @@ impl Anchor {
             timestamp_value: clock::Lamport::MIN.value,
             offset: u32::MIN,
             bias: Bias::Left,
-            buffer_id: Some(buffer_id),
+            buffer_id,
         }
     }
 
@@ -93,7 +72,7 @@ impl Anchor {
             timestamp_value: clock::Lamport::MAX.value,
             offset: u32::MAX,
             bias: Bias::Right,
-            buffer_id: Some(buffer_id),
+            buffer_id,
         }
     }
 
@@ -171,7 +150,7 @@ impl Anchor {
     pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool {
         if self.is_min() || self.is_max() {
             true
-        } else if self.buffer_id.is_none_or(|id| id != buffer.remote_id) {
+        } else if self.buffer_id != buffer.remote_id {
             false
         } else {
             let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else {

crates/text/src/text.rs 🔗

@@ -2413,7 +2413,7 @@ impl BufferSnapshot {
         } else if anchor.is_max() {
             self.visible_text.len()
         } else {
-            debug_assert_eq!(anchor.buffer_id, Some(self.remote_id));
+            debug_assert_eq!(anchor.buffer_id, self.remote_id);
             debug_assert!(
                 self.version.observed(anchor.timestamp()),
                 "Anchor timestamp {:?} not observed by buffer {:?}",
@@ -2445,7 +2445,7 @@ impl BufferSnapshot {
 
     #[cold]
     fn panic_bad_anchor(&self, anchor: &Anchor) -> ! {
-        if anchor.buffer_id.is_some_and(|id| id != self.remote_id) {
+        if anchor.buffer_id != self.remote_id {
             panic!(
                 "invalid anchor - buffer id does not match: anchor {anchor:?}; buffer id: {}, version: {:?}",
                 self.remote_id, self.version
@@ -2566,7 +2566,7 @@ impl BufferSnapshot {
                 fragment.timestamp,
                 fragment.insertion_offset + overshoot as u32,
                 bias,
-                Some(self.remote_id),
+                self.remote_id,
             )
         }
     }
@@ -2574,8 +2574,7 @@ impl BufferSnapshot {
     pub fn can_resolve(&self, anchor: &Anchor) -> bool {
         anchor.is_min()
             || anchor.is_max()
-            || (Some(self.remote_id) == anchor.buffer_id
-                && self.version.observed(anchor.timestamp()))
+            || (self.remote_id == anchor.buffer_id && self.version.observed(anchor.timestamp()))
     }
 
     pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
@@ -2601,7 +2600,10 @@ impl BufferSnapshot {
     where
         D: TextDimension + Ord,
     {
-        self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX)
+        self.edits_since_in_range(
+            since,
+            Anchor::min_for_buffer(self.remote_id)..Anchor::max_for_buffer(self.remote_id),
+        )
     }
 
     pub fn anchored_edits_since<'a, D>(
@@ -2611,7 +2613,10 @@ impl BufferSnapshot {
     where
         D: TextDimension + Ord,
     {
-        self.anchored_edits_since_in_range(since, Anchor::MIN..Anchor::MAX)
+        self.anchored_edits_since_in_range(
+            since,
+            Anchor::min_for_buffer(self.remote_id)..Anchor::max_for_buffer(self.remote_id),
+        )
     }
 
     pub fn edits_since_in_range<'a, D>(
@@ -2874,13 +2879,13 @@ impl<D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator for Ed
                 fragment.timestamp,
                 fragment.insertion_offset,
                 Bias::Right,
-                Some(self.buffer_id),
+                self.buffer_id,
             );
             let end_anchor = Anchor::new(
                 fragment.timestamp,
                 fragment.insertion_offset + fragment.len,
                 Bias::Left,
-                Some(self.buffer_id),
+                self.buffer_id,
             );
 
             if !fragment.was_visible(self.since, self.undos) && fragment.visible {