use async_lsp::lsp_types::{Position, Range};
use ropey::Rope;
use wcl_lang::lang::span::Span;
pub fn span_to_lsp_range(span: Span, rope: &Rope) -> Range {
let start = offset_to_lsp_position(span.start, rope);
let end = offset_to_lsp_position(span.end, rope);
Range { start, end }
}
pub fn offset_to_lsp_position(offset: usize, rope: &Rope) -> Position {
let offset = offset.min(rope.len_bytes());
let line = rope.byte_to_line(offset);
let line_start_byte = rope.line_to_byte(line);
let line_slice = rope.line(line);
let byte_diff = offset - line_start_byte;
let mut utf16_col = 0u32;
let mut bytes_consumed = 0usize;
for ch in line_slice.chars() {
if bytes_consumed >= byte_diff {
break;
}
utf16_col += ch.len_utf16() as u32;
bytes_consumed += ch.len_utf8();
}
Position {
line: line as u32,
character: utf16_col,
}
}
pub fn lsp_position_to_offset(pos: Position, rope: &Rope) -> usize {
let line = pos.line as usize;
if line >= rope.len_lines() {
return rope.len_bytes();
}
let line_start_byte = rope.line_to_byte(line);
let line_slice = rope.line(line);
let target_utf16 = pos.character as usize;
let mut utf16_col = 0usize;
let mut byte_offset = 0usize;
for ch in line_slice.chars() {
if utf16_col >= target_utf16 {
break;
}
utf16_col += ch.len_utf16();
byte_offset += ch.len_utf8();
}
line_start_byte + byte_offset
}
#[cfg(test)]
mod tests {
use super::*;
use wcl_lang::lang::span::FileId;
#[test]
fn test_offset_position_roundtrip_ascii() {
let text = "hello\nworld\nfoo";
let rope = Rope::from_str(text);
let pos = offset_to_lsp_position(6, &rope);
assert_eq!(pos.line, 1);
assert_eq!(pos.character, 0);
let back = lsp_position_to_offset(pos, &rope);
assert_eq!(back, 6);
}
#[test]
fn test_offset_position_roundtrip_multibyte() {
let text = "a\u{00e9}b\nc"; let rope = Rope::from_str(text);
let pos = offset_to_lsp_position(3, &rope);
assert_eq!(pos.line, 0);
assert_eq!(pos.character, 2);
let back = lsp_position_to_offset(pos, &rope);
assert_eq!(back, 3);
}
#[test]
fn test_span_to_range() {
let text = "hello\nworld";
let rope = Rope::from_str(text);
let span = Span::new(FileId(0), 6, 11);
let range = span_to_lsp_range(span, &rope);
assert_eq!(range.start.line, 1);
assert_eq!(range.start.character, 0);
assert_eq!(range.end.line, 1);
assert_eq!(range.end.character, 5);
}
#[test]
fn test_offset_at_end() {
let text = "abc";
let rope = Rope::from_str(text);
let pos = offset_to_lsp_position(3, &rope);
assert_eq!(pos.line, 0);
assert_eq!(pos.character, 3);
}
#[test]
fn test_crlf_line_endings() {
let text = "hello\r\nworld\r\nfoo";
let rope = Rope::from_str(text);
let pos = offset_to_lsp_position(0, &rope);
assert_eq!(pos.line, 0);
assert_eq!(pos.character, 0);
let pos = offset_to_lsp_position(7, &rope);
assert_eq!(pos.line, 1);
assert_eq!(pos.character, 0);
let pos = offset_to_lsp_position(14, &rope);
assert_eq!(pos.line, 2);
assert_eq!(pos.character, 0);
let back = lsp_position_to_offset(
Position {
line: 1,
character: 0,
},
&rope,
);
assert_eq!(back, 7);
let pos = offset_to_lsp_position(11, &rope);
assert_eq!(pos.line, 1);
assert_eq!(pos.character, 4);
let span = Span::new(FileId(0), 7, 12); let range = span_to_lsp_range(span, &rope);
assert_eq!(range.start.line, 1);
assert_eq!(range.start.character, 0);
assert_eq!(range.end.line, 1);
assert_eq!(range.end.character, 5);
}
#[test]
fn test_offset_beyond_end_clamped() {
let text = "ab";
let rope = Rope::from_str(text);
let pos = offset_to_lsp_position(100, &rope);
assert_eq!(pos.line, 0);
assert_eq!(pos.character, 2);
}
}