use crate::location::{offset_to_location, Location, Span};
#[expect(clippy::exhaustive_structs, reason = "public API type")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LspPosition {
pub line: u32,
pub character: u32,
}
impl LspPosition {
#[must_use]
pub const fn new(line: u32, character: u32) -> Self {
Self { line, character }
}
#[must_use]
pub fn to_location(&self, source: &str) -> Location {
let offset = Self::to_offset(source, *self);
offset_to_location(offset, source)
}
#[must_use]
pub fn to_offset(source: &str, position: Self) -> usize {
let mut current_line = 0u32;
let mut byte_offset = 0usize;
for (idx, ch) in source.char_indices() {
if current_line == position.line {
for (char_count, (char_idx, _)) in source[byte_offset..].char_indices().enumerate()
{
if char_count == usize::try_from(position.character).unwrap_or(usize::MAX) {
return byte_offset.saturating_add(char_idx);
}
if source[byte_offset.saturating_add(char_idx)..].starts_with('\n') {
break;
}
}
let line_end = source[byte_offset..]
.find('\n')
.map_or(source.len(), |n| byte_offset.saturating_add(n));
return line_end;
}
if ch == '\n' {
current_line = current_line.saturating_add(1);
byte_offset = idx.saturating_add(ch.len_utf8());
}
}
source.len()
}
}
impl From<Location> for LspPosition {
fn from(location: Location) -> Self {
Self {
line: u32::try_from(location.line.saturating_sub(1)).unwrap_or(u32::MAX),
character: u32::try_from(location.column.saturating_sub(1)).unwrap_or(u32::MAX),
}
}
}
#[must_use]
pub const fn span_contains_offset(span: &Span, offset: usize) -> bool {
span.start.offset <= offset && offset < span.end.offset
}
#[must_use]
pub fn span_contains_lsp_position(span: &Span, position: LspPosition, source: &str) -> bool {
let offset = LspPosition::to_offset(source, position);
span_contains_offset(span, offset)
}
#[must_use]
pub fn get_line_at_position(source: &str, position: LspPosition) -> &str {
let lines: Vec<&str> = source.lines().collect();
lines.get(position.line as usize).copied().unwrap_or("")
}
#[must_use]
pub fn get_word_at_offset(source: &str, offset: usize) -> Option<(String, usize, usize)> {
if offset > source.len() {
return None;
}
let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
let start = source[..offset]
.rfind(|c: char| !is_word_char(c))
.map_or(0, |i| i.saturating_add(1));
let end = source[offset..]
.find(|c: char| !is_word_char(c))
.map_or(source.len(), |i| offset.saturating_add(i));
if start < end {
let word = source[start..end].to_string();
Some((word, start, end))
} else {
None
}
}
#[must_use]
pub fn get_word_at_lsp_position(
source: &str,
position: LspPosition,
) -> Option<(String, usize, usize)> {
let offset = LspPosition::to_offset(source, position);
get_word_at_offset(source, offset)
}