use bynk_syntax::span::{LineIndex, Span};
use tower_lsp::lsp_types::{Position, Range};
pub struct PositionMap<'a> {
source: &'a str,
index: LineIndex,
}
impl<'a> PositionMap<'a> {
pub fn new(source: &'a str) -> Self {
Self {
source,
index: LineIndex::new(source),
}
}
pub fn position(&self, offset: usize) -> Position {
let (line, character) = self.index.utf16_line_col(self.source, offset);
Position { line, character }
}
pub fn range(&self, span: Span) -> Range {
Range {
start: self.position(span.start),
end: self.position(span.end),
}
}
pub fn end(&self) -> Position {
self.position(self.source.len())
}
}
pub fn offset_to_position(source: &str, offset: usize) -> Position {
let mut line: u32 = 0;
let mut column: u32 = 0;
let bytes = source.as_bytes();
let limit = offset.min(bytes.len());
let mut i = 0;
while i < limit {
let b = bytes[i];
if b == b'\n' {
line += 1;
column = 0;
i += 1;
continue;
}
let cp_len = utf8_char_len(b);
column += if cp_len == 4 { 2 } else { 1 };
i += cp_len;
}
Position {
line,
character: column,
}
}
pub fn position_to_offset(source: &str, position: Position) -> Option<usize> {
let target_line = position.line;
let target_char = position.character;
let mut line: u32 = 0;
let mut character: u32 = 0;
let bytes = source.as_bytes();
let mut i = 0;
while i < bytes.len() {
if line == target_line && character == target_char {
return Some(i);
}
let b = bytes[i];
if b == b'\n' {
if line == target_line {
return Some(i);
}
line += 1;
character = 0;
i += 1;
continue;
}
let cp_len = utf8_char_len(b);
character += if cp_len == 4 { 2 } else { 1 };
i += cp_len;
}
if line == target_line && character >= target_char {
Some(i)
} else {
None
}
}
fn utf8_char_len(first: u8) -> usize {
if first < 0x80 {
1
} else if first < 0xC0 {
1
} else if first < 0xE0 {
2
} else if first < 0xF0 {
3
} else {
4
}
}
pub fn span_to_range(source: &str, span: Span) -> Range {
Range {
start: offset_to_position(source, span.start),
end: offset_to_position(source, span.end),
}
}
pub fn end_position(source: &str) -> Position {
offset_to_position(source, source.len())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_offsets_match_columns() {
let src = "abc\ndef";
assert_eq!(offset_to_position(src, 0), Position::new(0, 0));
assert_eq!(offset_to_position(src, 2), Position::new(0, 2));
assert_eq!(offset_to_position(src, 4), Position::new(1, 0));
assert_eq!(offset_to_position(src, 6), Position::new(1, 2));
}
#[test]
fn position_round_trip() {
let src = "alpha\n beta\ngamma";
let p = Position::new(1, 4);
let off = position_to_offset(src, p).unwrap();
assert_eq!(offset_to_position(src, off), p);
}
#[test]
fn non_ascii_offsets_count_utf16_units() {
let src = "-- café\nlet x";
assert_eq!(position_to_offset(src, Position::new(0, 7)), Some(8));
assert_eq!(offset_to_position(src, 8), Position::new(0, 7));
assert_eq!(
position_to_offset(src, Position::new(1, 3)),
Some(src.find("let").unwrap() + 3)
);
let crab = "🦀ab";
assert_eq!(position_to_offset(crab, Position::new(0, 2)), Some(4));
assert_eq!(position_to_offset(crab, Position::new(0, 3)), Some(5));
assert_eq!(offset_to_position(crab, 4), Position::new(0, 2));
}
#[test]
fn non_ascii_round_trips_on_char_boundaries() {
let src = "π = 3\n-- naïve café €10 🦀\nend";
for line in 0..3u32 {
for character in 0..24u32 {
if let Some(off) = position_to_offset(src, Position::new(line, character)) {
assert!(
src.is_char_boundary(off),
"offset {off} for ({line},{character}) splits a codepoint"
);
let _ = &src[..off];
}
}
}
}
}