use crate::error::SourceRange;
use tower_lsp_server::ls_types::{Position, Range};
#[derive(Clone, Debug)]
pub struct LineIndex {
line_starts: Vec<usize>, }
impl LineIndex {
pub fn new(source_contents: &str) -> Self {
Self {
line_starts: std::iter::once(0)
.chain(
source_contents
.match_indices('\n')
.map(|(index, _)| index + '\n'.len_utf8()),
)
.collect(),
}
}
pub fn byte_offset(&self, source_contents: &str, position: Position) -> Option<usize> {
let line = usize::try_from(position.line).ok()?;
let line_start = *self.line_starts.get(line)?;
let line_end = self
.line_starts
.get(line + 1)
.map_or(source_contents.len(), |next_line_start| {
next_line_start - '\n'.len_utf8()
});
let content_end = if source_contents.as_bytes().get(line_end) == Some(&b'\n')
&& source_contents[line_start..line_end].ends_with('\r')
{
line_end - '\r'.len_utf8()
} else {
line_end
};
let requested_character = usize::try_from(position.character).ok()?;
let mut utf16_character = 0;
for (index, character) in source_contents[line_start..content_end].char_indices() {
if utf16_character == requested_character {
return Some(line_start + index);
}
utf16_character += character.len_utf16();
if utf16_character > requested_character {
return None;
}
}
Some(content_end)
}
pub fn position(&self, source_contents: &str, byte_offset: usize) -> Position {
let byte_offset = byte_offset.min(source_contents.len());
let line = self
.line_starts
.partition_point(|line_start| *line_start <= byte_offset)
- 1;
Position::new(
u32::try_from(line).unwrap_or(u32::MAX),
u32::try_from(
source_contents
.get(self.line_starts[line]..byte_offset)
.expect("Source ranges should end on UTF-8 character boundaries.")
.encode_utf16()
.count(),
)
.unwrap_or(u32::MAX),
)
}
pub fn range(&self, source_contents: &str, source_range: SourceRange) -> Range {
Range::new(
self.position(source_contents, source_range.start),
self.position(source_contents, source_range.end),
)
}
}
#[cfg(test)]
mod tests {
use super::LineIndex;
use tower_lsp_server::ls_types::Position;
fn position(source: &str, byte_offset: usize) -> Position {
LineIndex::new(source).position(source, byte_offset)
}
fn byte_offset(source: &str, position: Position) -> Option<usize> {
LineIndex::new(source).byte_offset(source, position)
}
#[test]
fn positions_use_utf16_code_units() {
let source = "zero\n😀 café";
assert_eq!(position(source, 0), Position::new(0, 0));
assert_eq!(position(source, 4), Position::new(0, 4));
assert_eq!(position(source, 5), Position::new(1, 0));
assert_eq!(position(source, 9), Position::new(1, 2));
assert_eq!(position(source, source.len()), Position::new(1, 7));
}
#[test]
fn positions_after_final_line_break() {
let source = "zero\n";
assert_eq!(position(source, source.len()), Position::new(1, 0));
assert_eq!(byte_offset(source, Position::new(1, 0)), Some(source.len()));
assert_eq!(byte_offset(source, Position::new(2, 0)), None);
}
#[test]
fn byte_offsets_use_utf16_code_units() {
let source = "zero\n😀 café";
assert_eq!(byte_offset(source, Position::new(0, 0)), Some(0));
assert_eq!(byte_offset(source, Position::new(0, 9)), Some(4));
assert_eq!(byte_offset(source, Position::new(1, 0)), Some(5));
assert_eq!(byte_offset(source, Position::new(1, 1)), None);
assert_eq!(byte_offset(source, Position::new(1, 2)), Some(9));
assert_eq!(byte_offset(source, Position::new(1, 7)), Some(source.len()));
assert_eq!(byte_offset(source, Position::new(1, 8)), Some(source.len()));
assert_eq!(byte_offset(source, Position::new(2, 0)), None);
assert_eq!(byte_offset("ab\r\ncd", Position::new(0, 9)), Some(2));
assert_eq!(byte_offset("ab\r\ncd", Position::new(1, 1)), Some(5));
}
}