#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn range(&self) -> std::ops::Range<usize> {
self.start..self.end
}
pub fn offset(self, delta: usize) -> Span {
Span {
start: self.start + delta,
end: self.end + delta,
}
}
pub fn merge(self, other: Span) -> Span {
Span {
start: self.start.min(other.start),
end: self.end.max(other.end),
}
}
}
impl From<std::ops::Range<usize>> for Span {
fn from(r: std::ops::Range<usize>) -> Self {
Span {
start: r.start,
end: r.end,
}
}
}
#[cfg(test)]
mod line_index_tests {
use super::{LineIndex, line_col};
#[test]
fn line_index_matches_scanning_line_col() {
for src in [
"",
"abc",
"abc\ndef",
"abc\ndef\n",
"\n\n\n",
"π = 3\n-- naïve café €10 🦀\nend",
] {
let index = LineIndex::new(src);
for offset in 0..=src.len() + 2 {
if !src.is_char_boundary(offset.min(src.len())) {
continue;
}
assert_eq!(
index.line_col(src, offset),
line_col(src, offset),
"mismatch at offset {offset} in {src:?}",
);
}
}
}
#[test]
fn utf16_line_col_counts_code_units() {
let src = "-- café\nlet 🦀 x";
let index = LineIndex::new(src);
let after_cafe = "-- café".len();
assert_eq!(index.utf16_line_col(src, after_cafe), (0, 7));
let line1 = src.find("let").unwrap();
assert_eq!(index.utf16_line_col(src, line1), (1, 0));
let after_crab = line1 + "let 🦀".len();
assert_eq!(index.utf16_line_col(src, after_crab), (1, 6));
}
#[test]
fn line_and_line_start_round_trip() {
let src = "one\ntwo\nthree";
let index = LineIndex::new(src);
assert_eq!(index.line(0), 0);
assert_eq!(index.line(3), 0); assert_eq!(index.line(4), 1); assert_eq!(index.line(src.len()), 2);
assert_eq!(index.line_start(1), 4);
assert_eq!(index.line_start(2), 8);
}
}
pub fn line_col(source: &str, offset: usize) -> (usize, usize) {
let mut line = 1;
let mut col = 1;
for (i, ch) in source.char_indices() {
if i >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
#[derive(Debug, Clone)]
pub struct LineIndex {
line_starts: Vec<usize>,
len: usize,
}
impl LineIndex {
pub fn new(source: &str) -> Self {
let mut line_starts = vec![0usize];
for (i, b) in source.bytes().enumerate() {
if b == b'\n' {
line_starts.push(i + 1);
}
}
Self {
line_starts,
len: source.len(),
}
}
pub fn line(&self, offset: usize) -> usize {
match self.line_starts.binary_search(&offset) {
Ok(i) => i,
Err(i) => i - 1,
}
}
pub fn line_start(&self, line: usize) -> usize {
self.line_starts[line]
}
pub fn line_col(&self, source: &str, offset: usize) -> (usize, usize) {
let offset = offset.min(self.len);
let line = self.line(offset);
let start = self.line_starts[line];
let mut col = 1;
for (i, _) in source[start..].char_indices() {
if start + i >= offset {
break;
}
col += 1;
}
(line + 1, col)
}
pub fn utf16_line_col(&self, source: &str, offset: usize) -> (u32, u32) {
let offset = offset.min(self.len);
let line = self.line(offset);
let start = self.line_starts[line];
let mut col: u32 = 0;
for (i, ch) in source[start..].char_indices() {
if start + i >= offset {
break;
}
col += ch.len_utf16() as u32;
}
(line as u32, col)
}
}