#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct LineIndexer {
offsets: Vec<usize>,
total_len: usize,
}
impl LineIndexer {
pub fn new(text: &str) -> Self {
let mut offsets = vec![0usize];
for (i, b) in text.bytes().enumerate() {
if b == b'\n' {
offsets.push(i + 1);
}
}
Self {
offsets,
total_len: text.len(),
}
}
pub fn line_count(&self) -> usize {
self.offsets.len()
}
pub fn line_offset(&self, line: usize) -> Option<usize> {
self.offsets.get(line).copied()
}
pub fn line_len(&self, line: usize) -> Option<usize> {
let start = *self.offsets.get(line)?;
let end = self
.offsets
.get(line + 1)
.copied()
.unwrap_or(self.total_len);
Some(end.saturating_sub(start).saturating_sub(1))
}
pub fn offset_to_line_col(&self, offset: usize) -> Option<(usize, usize)> {
if offset > self.total_len {
return None;
}
let line = match self.offsets.binary_search(&offset) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
};
let col = offset.saturating_sub(self.offsets[line]);
Some((line, col))
}
pub fn line_col_to_offset(&self, line: usize, col: usize) -> Option<usize> {
let start = self.offsets.get(line).copied()?;
Some(start + col)
}
pub fn all_offsets(&self) -> &[usize] {
&self.offsets
}
pub fn is_valid_offset(&self, offset: usize) -> bool {
offset <= self.total_len
}
}
pub fn build_line_indexer(text: &str) -> LineIndexer {
LineIndexer::new(text)
}
pub fn line_to_offset(indexer: &LineIndexer, line: usize) -> Option<usize> {
indexer.line_offset(line)
}
pub fn offset_to_position(indexer: &LineIndexer, offset: usize) -> Option<(usize, usize)> {
indexer.offset_to_line_col(offset)
}
#[cfg(test)]
mod tests {
use super::*;
const TEXT: &str = "hello\nworld\nfoo\n";
#[test]
fn test_line_count() {
let idx = LineIndexer::new(TEXT);
assert_eq!(idx.line_count(), 4);
}
#[test]
fn test_line_zero_offset_is_zero() {
let idx = LineIndexer::new(TEXT);
assert_eq!(idx.line_offset(0), Some(0));
}
#[test]
fn test_line_one_offset() {
let idx = LineIndexer::new(TEXT);
assert_eq!(idx.line_offset(1), Some(6));
}
#[test]
fn test_offset_to_line_col_start() {
let idx = LineIndexer::new(TEXT);
assert_eq!(idx.offset_to_line_col(0), Some((0, 0)));
}
#[test]
fn test_offset_to_line_col_mid_line() {
let idx = LineIndexer::new(TEXT);
assert_eq!(idx.offset_to_line_col(2), Some((0, 2)));
}
#[test]
fn test_line_col_to_offset_round_trip() {
let idx = LineIndexer::new(TEXT);
let (l, c) = idx.offset_to_line_col(8).expect("should succeed");
let back = idx.line_col_to_offset(l, c).expect("should succeed");
assert_eq!(back, 8);
}
#[test]
fn test_out_of_bounds_returns_none() {
let idx = LineIndexer::new("hi");
assert!(idx.line_offset(100).is_none());
}
#[test]
fn test_is_valid_offset() {
let idx = LineIndexer::new("ab");
assert!(idx.is_valid_offset(2));
assert!(!idx.is_valid_offset(99));
}
#[test]
fn test_build_line_indexer_helper() {
let idx = build_line_indexer("one\ntwo\n");
assert_eq!(idx.line_count(), 3);
}
#[test]
fn test_line_len() {
let idx = LineIndexer::new("abc\ndef\n");
assert_eq!(idx.line_len(0), Some(3));
}
}