use harper_core::Span;
use tower_lsp_server::lsp_types::{Position, Range};
pub fn span_to_range(source: &[char], span: Span<char>) -> Range {
let start = index_to_position(source, span.start);
let end = index_to_position(source, span.end);
Range { start, end }
}
fn index_to_position(source: &[char], index: usize) -> Position {
let before = &source[0..index];
let newline_indices: Vec<_> = before
.iter()
.enumerate()
.filter_map(|(idx, c)| if *c == '\n' { Some(idx + 1) } else { None })
.collect();
let lines = newline_indices.len();
let last_newline_idx = newline_indices.last().copied().unwrap_or(0);
let cols: usize = source[last_newline_idx..index]
.iter()
.map(|c| c.len_utf16())
.sum();
Position {
line: lines as u32,
character: cols as u32,
}
}
fn position_to_index(source: &[char], position: Position) -> usize {
let Some(target_line) = source
.split_inclusive(|char| *char == '\n')
.nth(position.line as usize)
else {
return source.len().saturating_sub(1);
};
let target_char_pointer = if position.character
< target_line
.len()
.try_into()
.expect("target_line.len() can fit in u32")
{
target_line
.as_ptr()
.wrapping_add(position.character as usize)
} else {
target_line.last().expect("line cannot be empty")
};
(target_char_pointer as usize - source.as_ptr() as usize) / size_of::<char>()
}
pub fn range_to_span(source: &[char], range: Range) -> Span<char> {
let start = position_to_index(source, range.start);
let end = position_to_index(source, range.end);
Span::new(start, end)
}
#[cfg(test)]
mod tests {
use tower_lsp_server::lsp_types::{Position, Range};
use super::{index_to_position, position_to_index, range_to_span};
#[test]
fn first_line_correct() {
let source: Vec<_> = "Hello there.".chars().collect();
let start = Position {
line: 0,
character: 4,
};
let i = position_to_index(&source, start);
assert_eq!(i, 4);
let p = index_to_position(&source, i);
assert_eq!(p, start)
}
#[test]
fn reversible_position_conv() {
let source: Vec<_> = "There was a man,\n his voice had timbre,\n unlike a boy."
.chars()
.collect();
let a = Position {
line: 1,
character: 2,
};
let b = position_to_index(&source, a);
assert_eq!(b, 19);
let c = index_to_position(&source, b);
let d = position_to_index(&source, a);
assert_eq!(a, c);
assert_eq!(b, d);
}
#[test]
fn end_of_line() {
let source: Vec<_> = "This is a short test\n".chars().collect();
let a = Position {
line: 0,
character: 20,
};
assert_eq!(position_to_index(&source, a), 20);
}
#[test]
fn end_of_file() {
let source: Vec<_> = "This is a short test".chars().collect();
let a = Position {
line: 0,
character: 19,
};
assert_eq!(position_to_index(&source, a), 19);
}
#[test]
fn issue_250() {
let source: Vec<_> = "Hello thur\n".chars().collect();
let range = Range {
start: Position {
line: 0,
character: 9,
},
end: Position {
line: 0,
character: 10,
},
};
let out = range_to_span(&source, range);
assert_eq!(out.start, 9);
assert_eq!(out.end, 10);
}
#[test]
fn pos_to_index_correct_for_l1_c0() {
let source: Vec<_> = ". one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty-one twenty-two twenty-three twenty-four twenty-five twenty-six twenty-seven twenty-eight twenty-nine thirty thirty-one\n".chars().collect();
let position = Position {
line: 1,
character: 0,
};
let out_index = position_to_index(&source, position);
assert_ne!(out_index, 0);
}
#[test]
fn pos_to_index_off_by_one_check_l0_c0() {
let source: Vec<_> = "abc\ndef\nghi\njkl".chars().collect();
let position = Position {
line: 0,
character: 0,
};
let out_index = position_to_index(&source, position);
assert_eq!(source[out_index], 'a');
}
#[test]
fn pos_to_index_off_by_one_check_l2_c1() {
let source: Vec<_> = "abc\ndef\nghi\njkl".chars().collect();
let position = Position {
line: 2,
character: 1,
};
let out_index = position_to_index(&source, position);
assert_eq!(source[out_index], 'h');
}
#[test]
fn pos_to_index_newline_only_l0_c0() {
let source: Vec<_> = "\n".chars().collect();
let position = Position {
line: 0,
character: 0,
};
let out_index = position_to_index(&source, position);
assert_eq!(out_index, 0);
}
#[test]
fn pos_to_index_newlines_only_l7_c0() {
let source: Vec<_> = "\n\n\n".chars().collect();
let position = Position {
line: 7,
character: 0,
};
let out_index = position_to_index(&source, position);
assert_eq!(out_index, 2);
}
#[test]
fn pos_to_index_out_of_bounds_char() {
let source: Vec<_> = "abc\ndef\nghi\njkl".chars().collect();
let position = Position {
line: 3, character: 8,
};
let out_index = position_to_index(&source, position);
assert_eq!(source[out_index], 'l');
}
}