use crate::{error::SourceRange, line_index::LineIndex};
use tower_lsp_server::ls_types::{Position, Range};
pub trait LspPosition {
fn byte_offset(&self, source_contents: &str, position: Position) -> Option<usize>;
fn position(&self, source_contents: &str, byte_offset: usize) -> Position;
fn range(&self, source_contents: &str, source_range: SourceRange) -> Range;
}
impl LspPosition for LineIndex {
fn byte_offset(&self, source_contents: &str, position: Position) -> Option<usize> {
let line = usize::try_from(position.line).ok()?;
let line_start = self.line_start(line)?;
let line_end = self.line_end(line)?;
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)
}
fn position(&self, source_contents: &str, byte_offset: usize) -> Position {
let byte_offset = byte_offset.min(source_contents.len());
let line = self
.line(byte_offset)
.expect("An offset within the source should be on a line.");
let line_start = self
.line_start(line)
.expect("The line containing an offset should exist.");
Position::new(
u32::try_from(line).unwrap_or(u32::MAX),
u32::try_from(
source_contents
.get(line_start..byte_offset)
.expect("Source ranges should end on UTF-8 character boundaries.")
.encode_utf16()
.count(),
)
.unwrap_or(u32::MAX),
)
}
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::LspPosition;
use crate::line_index::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));
}
}