From 694680809a10801cc1d31f8dfb1067b8dc646444 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Wed, 18 Feb 2026 08:46:17 +0700 Subject: [PATCH] attributed_text: make `AttributeSegments` report exact length Implement exact iterator cardinality for `AttributeSegments` by adding: - `Iterator::size_hint` with exact lower/upper bounds - `ExactSizeIterator` with `len()` `AttributeSegments` iterates over remaining adjacent boundary pairs, so remaining item count is exactly `boundaries.len() - 1 - index`. Also adds a regression test that validates `size_hint` as iteration advances. --- attributed_text/src/attribute_segments.rs | 43 ++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/attributed_text/src/attribute_segments.rs b/attributed_text/src/attribute_segments.rs index 11dcdc2f..df028bb2 100644 --- a/attributed_text/src/attribute_segments.rs +++ b/attributed_text/src/attribute_segments.rs @@ -263,15 +263,23 @@ impl<'w, 'a, T: Debug + TextStorage, Attr: Debug> AttributeSegments<'w, 'a, T, A impl Iterator for AttributeSegments<'_, '_, T, Attr> { type Item = Range; + fn size_hint(&self) -> (usize, Option) { + // Remaining segments are remaining adjacent boundary pairs: [i, i + 1). + let remaining = self + .workspace + .boundaries + .len() + .saturating_sub(self.index + 1); + (remaining, Some(remaining)) + } + fn next(&mut self) -> Option { - while self.index + 1 < self.workspace.boundaries.len() { + if self.index + 1 < self.workspace.boundaries.len() { self.update_active_for_boundary(self.index); let start = self.workspace.boundaries[self.index] as usize; let end = self.workspace.boundaries[self.index + 1] as usize; self.index += 1; - if start == end { - continue; - } + debug_assert!(start < end, "boundaries are sorted + deduped"); return Some(start..end); } @@ -280,6 +288,16 @@ impl Iterator for AttributeSegments<'_, '_, } } +impl ExactSizeIterator for AttributeSegments<'_, '_, T, Attr> { + fn len(&self) -> usize { + // Remaining segments are remaining adjacent boundary pairs: [i, i + 1). + self.workspace + .boundaries + .len() + .saturating_sub(self.index + 1) + } +} + /// A view of the attribute spans active over a particular segment. /// /// Provides iteration in both application order (ascending span id) and reverse @@ -356,6 +374,23 @@ mod tests { assert_eq!(segments.next(), None); } + #[test] + fn size_hint_tracks_remaining_segments() { + let mut at = AttributedText::new("hello"); + at.apply_attribute(TextRange::new(at.text(), 1..3).unwrap(), Color::Red); + let mut workspace = AttributeSegmentsWorkspace::new(); + let mut segments = workspace.segments(&at); + + assert_eq!(segments.size_hint(), (3, Some(3))); + assert_eq!(segments.next(), Some(0..1)); + assert_eq!(segments.size_hint(), (2, Some(2))); + assert_eq!(segments.next(), Some(1..3)); + assert_eq!(segments.size_hint(), (1, Some(1))); + assert_eq!(segments.next(), Some(3..5)); + assert_eq!(segments.size_hint(), (0, Some(0))); + assert_eq!(segments.next(), None); + } + #[test] fn single_full_span() { let mut at = AttributedText::new("hello");