use crate::text::{TextRange, TextSize};
pub(crate) struct LineCursor<'a> {
source: &'a str,
offsets: Vec<usize>,
total: usize,
pub line: usize,
}
impl<'a> LineCursor<'a> {
pub fn new(source: &'a str) -> Self {
let offsets = build_line_offsets(source);
let total = count_lines(source, &offsets);
Self {
source,
offsets,
total,
line: 0,
}
}
pub fn source(&self) -> &'a str {
self.source
}
pub fn full_range(&self) -> TextRange {
let last_line = self.total.saturating_sub(1);
let last_col = self.line_text(last_line).len();
self.make_range(0, 0, last_line, last_col)
}
pub fn is_eof(&self) -> bool {
self.line >= self.total
}
pub fn total_lines(&self) -> usize {
self.total
}
pub fn advance(&mut self) {
self.line += 1;
}
pub fn skip_blanks(&mut self) {
while !self.is_eof() && self.current_line_text().trim().is_empty() {
self.line += 1;
}
}
pub fn current_line_text(&self) -> &'a str {
self.line_text(self.line)
}
pub fn current_trimmed(&self) -> &'a str {
self.current_line_text().trim()
}
pub fn current_indent(&self) -> usize {
indent_len(self.current_line_text())
}
pub fn current_indent_columns(&self) -> usize {
indent_columns(self.current_line_text())
}
pub fn current_trimmed_range(&self) -> TextRange {
self.make_line_range(self.line, self.current_indent(), self.current_trimmed().len())
}
pub fn line_text(&self, idx: usize) -> &'a str {
if idx >= self.offsets.len() {
return "";
}
let start = self.offsets[idx];
let end = if idx + 1 < self.offsets.len() {
self.offsets[idx + 1].saturating_sub(1)
} else {
self.source.len()
};
if start >= self.source.len() {
return "";
}
&self.source[start..end]
}
pub fn make_range(&self, start_line: usize, start_col: usize, end_line: usize, end_col: usize) -> TextRange {
TextRange::new(
TextSize::new((self.offsets[start_line] + start_col) as u32),
TextSize::new((self.offsets[end_line] + end_col) as u32),
)
}
pub fn make_line_range(&self, line: usize, col: usize, len: usize) -> TextRange {
let start = self.offsets[line] + col;
TextRange::from_offset_len(start, len)
}
pub fn span_back_from_cursor(&self, start_offset: usize) -> TextRange {
let (start_line, start_col) = self.offset_to_line_col(start_offset);
let mut end_line = self.line.saturating_sub(1);
while end_line > start_line {
if !self.line_text(end_line).trim().is_empty() {
break;
}
end_line -= 1;
}
let end_text = self.line_text(end_line);
let end_col = indent_len(end_text) + end_text.trim().len();
self.make_range(start_line, start_col, end_line, end_col)
}
pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
let line = self.offsets.partition_point(|&o| o <= offset).saturating_sub(1);
let col = offset - self.offsets[line];
(line, col)
}
pub fn substr_offset(&self, inner: &str) -> usize {
inner.as_ptr() as usize - self.source.as_ptr() as usize
}
}
pub(crate) fn indent_len(line: &str) -> usize {
line.len() - line.trim_start().len()
}
pub(crate) fn indent_columns(line: &str) -> usize {
const TAB_WIDTH: usize = 4;
let mut col = 0;
for byte in line.bytes() {
match byte {
b'\t' => col = (col / TAB_WIDTH + 1) * TAB_WIDTH,
b' ' => col += 1,
_ => break,
}
}
col
}
fn build_line_offsets(input: &str) -> Vec<usize> {
let mut offsets = vec![0usize];
for (i, byte) in input.bytes().enumerate() {
if byte == b'\n' {
offsets.push(i + 1);
}
}
offsets
}
fn count_lines(source: &str, offsets: &[usize]) -> usize {
if source.is_empty() {
0
} else if source.ends_with('\n') {
offsets.len() - 1
} else {
offsets.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_indent_columns_spaces_only() {
assert_eq!(indent_columns(" hello"), 4);
assert_eq!(indent_columns("hello"), 0);
assert_eq!(indent_columns(" x"), 2);
}
#[test]
fn test_indent_columns_tab() {
assert_eq!(indent_columns("\thello"), 4);
assert_eq!(indent_columns("\t\thello"), 8);
}
#[test]
fn test_indent_columns_mixed_tab_space() {
assert_eq!(indent_columns(" \thello"), 4);
assert_eq!(indent_columns(" \thello"), 4);
assert_eq!(indent_columns("\t hello"), 6);
}
#[test]
fn test_indent_len_unchanged() {
assert_eq!(indent_len("\thello"), 1);
assert_eq!(indent_len(" hello"), 4);
assert_eq!(indent_len(" \thello"), 3);
}
}