use rowan::TextSize;
#[derive(Debug)]
pub struct LineIndex {
line_starts: Vec<u32>,
source: String,
}
impl LineIndex {
pub fn new(source: &str) -> Self {
let mut line_starts = vec![0u32];
for (i, byte) in source.bytes().enumerate() {
if byte == b'\n' {
line_starts.push(u32::try_from(i).unwrap_or(u32::MAX) + 1);
}
}
Self {
line_starts,
source: source.to_owned(),
}
}
#[must_use]
pub fn line_text(&self, line: u32) -> Option<&str> {
let start = *self.line_starts.get(line as usize)? as usize;
let end = self
.line_starts
.get(line as usize + 1)
.map_or(self.source.len(), |next| (*next as usize).saturating_sub(1));
self.source.get(start..end)
}
pub fn line_col(&self, offset: TextSize) -> (u32, u32) {
let offset = u32::from(offset);
let line = self
.line_starts
.partition_point(|&start| start <= offset)
.saturating_sub(1);
let line_start = self.line_starts[line] as usize;
let offset_usize = offset as usize;
let col_utf16 = self.source[line_start..offset_usize]
.chars()
.map(|c| u32::try_from(c.len_utf16()).unwrap_or(1))
.sum();
(u32::try_from(line).unwrap_or(u32::MAX), col_utf16)
}
pub fn offset(&self, line: u32, col: u32) -> TextSize {
let line_idx = line as usize;
let line_start = if line_idx < self.line_starts.len() {
self.line_starts[line_idx] as usize
} else {
return TextSize::from(u32::try_from(self.source.len()).unwrap_or(u32::MAX));
};
let rest = &self.source[line_start..];
let mut utf16_count = 0u32;
let mut byte_offset = 0usize;
for c in rest.chars() {
if utf16_count >= col {
break;
}
utf16_count += u32::try_from(c.len_utf16()).unwrap_or(1);
byte_offset += c.len_utf8();
}
TextSize::from(u32::try_from(line_start + byte_offset).unwrap_or(u32::MAX))
}
}
#[must_use]
pub fn doc_extended_start(source: &str, decl_start: usize) -> usize {
let decl_start = decl_start.min(source.len());
let mut line_start = source[..decl_start].rfind('\n').map_or(0, |i| i + 1);
let mut extended = None;
while line_start > 0 {
let prev_newline = line_start - 1;
let prev_start = source[..prev_newline].rfind('\n').map_or(0, |i| i + 1);
if source[prev_start..prev_newline]
.trim_start()
.starts_with("///")
{
extended = Some(prev_start);
line_start = prev_start;
} else {
break;
}
}
extended.unwrap_or(decl_start)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_source() {
let idx = LineIndex::new("");
assert_eq!(idx.line_col(TextSize::from(0)), (0, 0));
assert_eq!(idx.offset(0, 0), TextSize::from(0));
}
#[test]
fn single_line() {
let idx = LineIndex::new("hello");
assert_eq!(idx.line_col(TextSize::from(0)), (0, 0));
assert_eq!(idx.line_col(TextSize::from(3)), (0, 3));
assert_eq!(idx.line_col(TextSize::from(5)), (0, 5));
assert_eq!(idx.offset(0, 3), TextSize::from(3));
}
#[test]
fn multi_line() {
let src = "abc\ndef\nghi";
let idx = LineIndex::new(src);
assert_eq!(idx.line_col(TextSize::from(0)), (0, 0));
assert_eq!(idx.line_col(TextSize::from(4)), (1, 0));
assert_eq!(idx.line_col(TextSize::from(8)), (2, 0));
assert_eq!(idx.line_col(TextSize::from(10)), (2, 2));
assert_eq!(idx.offset(1, 0), TextSize::from(4));
assert_eq!(idx.offset(2, 2), TextSize::from(10));
}
#[test]
fn trailing_newline() {
let src = "abc\n";
let idx = LineIndex::new(src);
assert_eq!(idx.line_col(TextSize::from(4)), (1, 0));
assert_eq!(idx.offset(1, 0), TextSize::from(4));
}
#[test]
fn multibyte_utf8() {
let src = "a€𝄞b";
let idx = LineIndex::new(src);
assert_eq!(idx.line_col(TextSize::from(0)), (0, 0));
assert_eq!(idx.line_col(TextSize::from(1)), (0, 1));
assert_eq!(idx.line_col(TextSize::from(4)), (0, 2));
assert_eq!(idx.line_col(TextSize::from(8)), (0, 4));
assert_eq!(idx.offset(0, 0), TextSize::from(0));
assert_eq!(idx.offset(0, 1), TextSize::from(1));
assert_eq!(idx.offset(0, 2), TextSize::from(4));
assert_eq!(idx.offset(0, 4), TextSize::from(8));
}
#[test]
fn offset_to_line_col_roundtrip() {
let src = "abc\ndef\nghi";
let idx = LineIndex::new(src);
let (line, col) = idx.line_col(TextSize::from(4));
assert_eq!(idx.offset(line, col), TextSize::from(4));
let (line, col) = idx.line_col(TextSize::from(10));
assert_eq!(idx.offset(line, col), TextSize::from(10));
}
}