use lsp_types::PositionEncodingKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PositionEncoding {
Utf8,
Utf16,
Utf32,
}
impl PositionEncoding {
pub fn capability(self) -> PositionEncodingKind {
match self {
PositionEncoding::Utf8 => PositionEncodingKind::UTF8,
PositionEncoding::Utf16 => PositionEncodingKind::UTF16,
PositionEncoding::Utf32 => PositionEncodingKind::UTF32,
}
}
pub fn negotiate(offered: &[PositionEncodingKind]) -> Option<Self> {
let from_str = |s: &str| match s {
"utf-8" => Some(PositionEncoding::Utf8),
"utf-16" => Some(PositionEncoding::Utf16),
"utf-32" => Some(PositionEncoding::Utf32),
_ => None,
};
offered
.iter()
.filter_map(|kind| from_str(kind.as_str()))
.min_by_key(|enc| match enc {
PositionEncoding::Utf8 => 0,
PositionEncoding::Utf16 => 1,
PositionEncoding::Utf32 => 2,
})
}
pub(crate) fn units_of(self, text: &str) -> u32 {
match self {
PositionEncoding::Utf8 => text.len() as u32,
PositionEncoding::Utf16 => text.chars().map(char::len_utf16).sum::<usize>() as u32,
PositionEncoding::Utf32 => text.chars().count() as u32,
}
}
pub(crate) fn offset_of_units(self, text: &str, units: u32) -> usize {
if self == PositionEncoding::Utf8 {
return text.len().min(units as usize);
}
let mut seen = 0u32;
for (byte_idx, ch) in text.char_indices() {
if seen >= units {
return byte_idx;
}
seen += match self {
PositionEncoding::Utf8 => unreachable!(),
PositionEncoding::Utf16 => ch.len_utf16() as u32,
PositionEncoding::Utf32 => 1,
};
}
text.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEXT: &str = "aĆ©šb";
#[test]
fn units_of_counts_per_encoding() {
assert_eq!(PositionEncoding::Utf8.units_of(TEXT), 8);
assert_eq!(PositionEncoding::Utf16.units_of(TEXT), 5);
assert_eq!(PositionEncoding::Utf32.units_of(TEXT), 4);
}
#[test]
fn offset_of_units_hits_char_boundaries() {
let cases = [
(PositionEncoding::Utf8, 0usize, 0usize),
(PositionEncoding::Utf8, 1, 1),
(PositionEncoding::Utf8, 3, 3),
(PositionEncoding::Utf8, 7, 7),
(PositionEncoding::Utf8, 8, 8),
(PositionEncoding::Utf8, 100, 8),
(PositionEncoding::Utf16, 0, 0),
(PositionEncoding::Utf16, 1, 1),
(PositionEncoding::Utf16, 2, 3),
(PositionEncoding::Utf16, 3, 7),
(PositionEncoding::Utf16, 4, 7),
(PositionEncoding::Utf16, 5, 8),
(PositionEncoding::Utf16, 99, 8),
(PositionEncoding::Utf32, 0, 0),
(PositionEncoding::Utf32, 2, 3),
(PositionEncoding::Utf32, 3, 7),
(PositionEncoding::Utf32, 4, 8),
];
for (enc, units, expected) in cases {
assert_eq!(
enc.offset_of_units(TEXT, units as u32),
expected,
"{enc:?} @{units}"
);
}
}
#[test]
fn negotiate_prefers_utf8_then_utf16() {
let utf8 = PositionEncodingKind::UTF8;
let utf16 = PositionEncodingKind::UTF16;
let utf32 = PositionEncodingKind::UTF32;
assert_eq!(PositionEncoding::negotiate(&[]), None);
assert_eq!(
PositionEncoding::negotiate(std::slice::from_ref(&utf16)),
Some(PositionEncoding::Utf16)
);
assert_eq!(
PositionEncoding::negotiate(&[utf16.clone(), utf8.clone()]),
Some(PositionEncoding::Utf8)
);
assert_eq!(
PositionEncoding::negotiate(&[utf32.clone(), utf16]),
Some(PositionEncoding::Utf16)
);
assert_eq!(
PositionEncoding::negotiate(&[utf32]),
Some(PositionEncoding::Utf32)
);
}
}