pub struct LineIndex<'src> { /* private fields */ }Expand description
An index over a single source string that maps byte offsets to line/column coordinates and back.
A LineIndex is built once per source. Construction is a single linear scan
that records the byte offset at which each line begins; after that, a forward
lookup (line_col) is a binary search over those line
starts — O(log lines) — followed by a character count within the one located
line. Neither lookup direction allocates.
The index borrows the source rather than owning it: this crate maps positions and does not load text, so the caller keeps ownership of the buffer the index points into.
§Line endings
A line begins immediately after each \n. A \r\n sequence is therefore one
line break, not two — the \r is the final character of the preceding line.
A source with no trailing newline ends with a final line that has no
terminator, and the empty string is one empty line. A lone \r not followed
by \n is an ordinary character, not a line break, matching how language
front-ends split source.
§Examples
use span_lang::{BytePos, LineCol, LineIndex};
let index = LineIndex::new("let x = 1;\nlet y = 2;\n");
// Forward: byte offset -> (line, column).
let lc = index.line_col(BytePos::new(11)); // first byte of line 2
assert_eq!(lc, LineCol::new(2, 1));
// Inverse: (line, column) -> byte offset.
assert_eq!(index.offset(lc), Some(BytePos::new(11)));Implementations§
Source§impl<'src> LineIndex<'src>
impl<'src> LineIndex<'src>
Sourcepub fn new(src: &'src str) -> Self
pub fn new(src: &'src str) -> Self
Builds an index over src.
This is the only O(n) operation in the type; every subsequent lookup is
sub-linear. src must be at most u32::MAX bytes — the addressing limit
of BytePos — which holds for any single source a language front-end
loads.
§Examples
use span_lang::LineIndex;
let index = LineIndex::new("one\ntwo\nthree");
assert_eq!(index.line_count(), 3);Sourcepub fn line_count(&self) -> usize
pub fn line_count(&self) -> usize
Returns the number of lines in the source.
This counts line starts: one, plus the number of \n bytes. The empty
string is one line, and a trailing newline introduces a final empty line.
§Examples
use span_lang::LineIndex;
assert_eq!(LineIndex::new("").line_count(), 1);
assert_eq!(LineIndex::new("a\nb").line_count(), 2);
assert_eq!(LineIndex::new("a\nb\n").line_count(), 3);Sourcepub fn line_col(&self, pos: BytePos) -> LineCol
pub fn line_col(&self, pos: BytePos) -> LineCol
Resolves a byte offset to a 1-based LineCol.
The line is found by binary search over the recorded line starts in
O(log lines); the column is the number of characters between the start of
that line and pos, plus one.
Resolution is total and never panics. An offset past the end of the source is treated as the end, and an offset that falls inside a multi-byte character is rounded down to the start of that character — so the returned coordinate is always a real position in the source.
§Examples
use span_lang::{BytePos, LineCol, LineIndex};
let index = LineIndex::new("αβγ\nδε");
// The column counts characters, so γ is column 3 despite being byte 4.
assert_eq!(index.line_col(BytePos::new(4)), LineCol::new(1, 3));
// Past-the-end clamps to the final position rather than panicking.
assert_eq!(index.line_col(BytePos::new(9_999)), index.line_col(BytePos::new(11)));Sourcepub fn offset(&self, line_col: LineCol) -> Option<BytePos>
pub fn offset(&self, line_col: LineCol) -> Option<BytePos>
Resolves a 1-based LineCol back to a byte offset.
Returns None if the coordinate does not exist in the source: a line or
column of 0, a line past the last, or a column past the end of its line.
This is the inverse of line_col — for every valid
byte position, resolving forward and then back returns the original offset.
§Examples
use span_lang::{BytePos, LineCol, LineIndex};
let index = LineIndex::new("αβ\nγδ");
// Line 2, column 2 is the second character of the second line.
assert_eq!(index.offset(LineCol::new(2, 2)), Some(BytePos::new(7)));
// Coordinates outside the source resolve to `None`.
assert_eq!(index.offset(LineCol::new(0, 1)), None);
assert_eq!(index.offset(LineCol::new(9, 1)), None);
assert_eq!(index.offset(LineCol::new(1, 99)), None);Sourcepub fn line_span(&self, line: u32) -> Option<Span>
pub fn line_span(&self, line: u32) -> Option<Span>
Returns the byte span of a 1-based line’s text, excluding its terminator.
The span slices the source to exactly the line’s content: the trailing
\n — and a \r immediately before it, for a \r\n ending — is not
included, so &src[start..end] is the text a diagnostic would underline.
This is the lookup a renderer uses to print the offending line. Returns
None if line is 0 or past the last line.
The line’s start is found in O(log lines); trimming the terminator
inspects at most two bytes, so the whole operation is allocation-free and
never re-scans the source.
§Examples
use span_lang::LineIndex;
let src = "first\r\nsecond\nthird";
let index = LineIndex::new(src);
let line2 = index.line_span(2).expect("line 2 exists");
assert_eq!(&src[line2.start().to_usize()..line2.end().to_usize()], "second");
// The final, unterminated line is covered too.
let line3 = index.line_span(3).expect("line 3 exists");
assert_eq!(&src[line3.start().to_usize()..line3.end().to_usize()], "third");
assert_eq!(index.line_span(0), None);
assert_eq!(index.line_span(99), None);