pub struct Ruler<'a> { /* private fields */ }Expand description
Converts between scales for one specific text.
A Ruler cannot be built without the text, which is the structural reason
a scale conversion can never be done “in general”: there is no such thing.
"héllo".len() is 6 bytes and 5 chars, and only the string knows.
Implementations§
Source§impl<'a> Ruler<'a>
impl<'a> Ruler<'a>
pub const fn new(text: &'a str) -> Ruler<'a>
Sourcepub fn snap(&self, at: Offset<Bytes>) -> Offset<Bytes>
pub fn snap(&self, at: Offset<Bytes>) -> Offset<Bytes>
Clamp into the text and snap DOWN to a character boundary.
Snapping down, not up, keeps the result inside the character the offset
pointed at — where an editor should underline. Mid-codepoint offsets are
a NORMAL arrival, not corruption: they come from parser error spans over
a buffer the user is halfway through typing a character into. Measured
2026-08-01: analyse("🔥🔥🔥") aborted on exactly this at offset 1.
Sourcepub fn to_chars(&self, at: Offset<Bytes>) -> Offset<Chars>
pub fn to_chars(&self, at: Offset<Bytes>) -> Offset<Chars>
Bytes → chars. Total: out-of-range and mid-codepoint inputs snap first.
Sourcepub fn to_bytes(&self, at: Offset<Chars>) -> Offset<Bytes>
pub fn to_bytes(&self, at: Offset<Chars>) -> Offset<Bytes>
Chars → bytes. Total: past-the-end saturates to the text’s end.
Sourcepub fn to_utf16(&self, at: Offset<Bytes>) -> Offset<Utf16Units>
pub fn to_utf16(&self, at: Offset<Bytes>) -> Offset<Utf16Units>
Bytes → UTF-16 code units, for LSP.
Separate from Self::to_chars because they differ, and the
difference is invisible until a user types an emoji: 🔥 is ONE char
and TWO UTF-16 units. Conflating them shifts every position after it.
Source§impl<'a> Ruler<'a>
impl<'a> Ruler<'a>
Sourcepub const fn ascending(&self) -> AscendingScan<'a>
pub const fn ascending(&self) -> AscendingScan<'a>
A forward-only reader for offsets visited in ASCENDING order.
Ruler::to_chars is O(n) per call — text[..b].chars().count()
re-walks from the start every time — because it must be TOTAL and
random-access. That is right for a caret, and wrong for converting
every match in a document: O(n) per call over m matches is O(n·m),
which is the cost escriba-search originally avoided by building a
dense usize-per-byte map.
This is the third option, better than both: O(n + m) total with O(1) extra memory, because the offsets arrive in order and the scan never needs to look back. The dense map allocated and zeroed EIGHT BYTES PER DOCUMENT BYTE on every keystroke of an incremental search; this allocates nothing.