Skip to main content

increparse_lsp/
encoding.rs

1//! Negotiation of the LSP `positionEncoding` capability.
2
3use lsp_types::PositionEncodingKind;
4
5/// The character-unit convention a client uses for `Position.character`.
6///
7/// LSP 3.17+ clients advertise the encodings they support via the
8/// `general.positionEncodings` client capability and the server picks one in
9/// its `InitializeResult`; older clients always mean UTF-16. All conversions
10/// in this crate funnel through this enum.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum PositionEncoding {
13    /// `Position.character` counts UTF-8 bytes (code units of UTF-8).
14    Utf8,
15    /// `Position.character` counts UTF-16 code units — the LSP default and
16    /// what most clients (VS Code included) use.
17    Utf16,
18    /// `Position.character` counts Unicode code points (UTF-32 code units).
19    Utf32,
20}
21
22impl PositionEncoding {
23    /// The capability to advertise in `InitializeResult.capabilities.position_encoding`.
24    pub fn capability(self) -> PositionEncodingKind {
25        match self {
26            PositionEncoding::Utf8 => PositionEncodingKind::UTF8,
27            PositionEncoding::Utf16 => PositionEncodingKind::UTF16,
28            PositionEncoding::Utf32 => PositionEncodingKind::UTF32,
29        }
30    }
31
32    /// Selects an encoding from a client-advertised capability list,
33    /// preferring UTF-8 (cheapest to convert) over UTF-16 over UTF-32.
34    ///
35    /// Returns `None` — and callers should fall back to
36    /// [`PositionEncoding::Utf16`], the LSP default — if the client
37    /// advertises none of the supported encodings.
38    pub fn negotiate(offered: &[PositionEncodingKind]) -> Option<Self> {
39        let from_str = |s: &str| match s {
40            "utf-8" => Some(PositionEncoding::Utf8),
41            "utf-16" => Some(PositionEncoding::Utf16),
42            "utf-32" => Some(PositionEncoding::Utf32),
43            _ => None,
44        };
45        offered
46            .iter()
47            .filter_map(|kind| from_str(kind.as_str()))
48            .min_by_key(|enc| match enc {
49                PositionEncoding::Utf8 => 0,
50                PositionEncoding::Utf16 => 1,
51                PositionEncoding::Utf32 => 2,
52            })
53    }
54
55    /// Number of code units `text` occupies in this encoding.
56    pub(crate) fn units_of(self, text: &str) -> u32 {
57        match self {
58            PositionEncoding::Utf8 => text.len() as u32,
59            PositionEncoding::Utf16 => text.chars().map(char::len_utf16).sum::<usize>() as u32,
60            PositionEncoding::Utf32 => text.chars().count() as u32,
61        }
62    }
63
64    /// Byte offset of the `units`-th code unit boundary within `text`.
65    ///
66    /// For UTF-8 the units are bytes, so the result is simply
67    /// `min(units, text.len())`. For UTF-16/UTF-32, positions that land
68    /// inside a multi-unit character (e.g. the middle of a surrogate pair)
69    /// round up to the next char boundary, and positions past the end of
70    /// `text` clamp to `text.len()`.
71    pub(crate) fn offset_of_units(self, text: &str, units: u32) -> usize {
72        if self == PositionEncoding::Utf8 {
73            return text.len().min(units as usize);
74        }
75        let mut seen = 0u32;
76        for (byte_idx, ch) in text.char_indices() {
77            if seen >= units {
78                return byte_idx;
79            }
80            seen += match self {
81                PositionEncoding::Utf8 => unreachable!(),
82                PositionEncoding::Utf16 => ch.len_utf16() as u32,
83                PositionEncoding::Utf32 => 1,
84            };
85        }
86        text.len()
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    const TEXT: &str = "aĆ©šŸ˜€b"; // 'a' (1) 'Ć©' (2) 'šŸ˜€' (4, 2 utf16 units) 'b' (1)
95
96    #[test]
97    fn units_of_counts_per_encoding() {
98        assert_eq!(PositionEncoding::Utf8.units_of(TEXT), 8);
99        assert_eq!(PositionEncoding::Utf16.units_of(TEXT), 5);
100        assert_eq!(PositionEncoding::Utf32.units_of(TEXT), 4);
101    }
102
103    #[test]
104    fn offset_of_units_hits_char_boundaries() {
105        let cases = [
106            (PositionEncoding::Utf8, 0usize, 0usize),
107            (PositionEncoding::Utf8, 1, 1),
108            (PositionEncoding::Utf8, 3, 3),
109            (PositionEncoding::Utf8, 7, 7),
110            (PositionEncoding::Utf8, 8, 8),
111            (PositionEncoding::Utf8, 100, 8),
112            (PositionEncoding::Utf16, 0, 0),
113            (PositionEncoding::Utf16, 1, 1),
114            (PositionEncoding::Utf16, 2, 3),
115            (PositionEncoding::Utf16, 3, 7),
116            (PositionEncoding::Utf16, 4, 7),
117            (PositionEncoding::Utf16, 5, 8),
118            (PositionEncoding::Utf16, 99, 8),
119            (PositionEncoding::Utf32, 0, 0),
120            (PositionEncoding::Utf32, 2, 3),
121            (PositionEncoding::Utf32, 3, 7),
122            (PositionEncoding::Utf32, 4, 8),
123        ];
124        for (enc, units, expected) in cases {
125            assert_eq!(
126                enc.offset_of_units(TEXT, units as u32),
127                expected,
128                "{enc:?} @{units}"
129            );
130        }
131    }
132
133    #[test]
134    fn negotiate_prefers_utf8_then_utf16() {
135        let utf8 = PositionEncodingKind::UTF8;
136        let utf16 = PositionEncodingKind::UTF16;
137        let utf32 = PositionEncodingKind::UTF32;
138
139        assert_eq!(PositionEncoding::negotiate(&[]), None);
140        assert_eq!(
141            PositionEncoding::negotiate(std::slice::from_ref(&utf16)),
142            Some(PositionEncoding::Utf16)
143        );
144        assert_eq!(
145            PositionEncoding::negotiate(&[utf16.clone(), utf8.clone()]),
146            Some(PositionEncoding::Utf8)
147        );
148        assert_eq!(
149            PositionEncoding::negotiate(&[utf32.clone(), utf16]),
150            Some(PositionEncoding::Utf16)
151        );
152        assert_eq!(
153            PositionEncoding::negotiate(&[utf32]),
154            Some(PositionEncoding::Utf32)
155        );
156    }
157}