Skip to main content

hjkl_lsp/
encoding.rs

1//! Position-encoding negotiation (LSP `general.positionEncodings`).
2//!
3//! The LSP spec lets client and server negotiate the unit used for the
4//! `character` field of every `Position`/`Range` exchanged between them:
5//! UTF-8 byte offsets, UTF-16 code units (the spec default — every server
6//! MUST support it even if it never says so), or UTF-32 code points (rare,
7//! unsupported here). hjkl advertises both `"utf-8"` and `"utf-16"` in
8//! `general.positionEncodings` (see `crate::server::initialize_handshake`)
9//! and honours whichever the server picks back in `initialize`'s
10//! `capabilities.positionEncoding`.
11
12/// The unit used for the `character` field of every `Position`/`Range`
13/// exchanged with a given server.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum PositionEncoding {
16    /// UTF-8 byte offsets within a line. hjkl's preferred encoding — skips
17    /// per-line UTF-16 unit counting when the server supports it.
18    Utf8,
19    /// UTF-16 code units within a line. The LSP spec's default: a server
20    /// that never mentions `positionEncoding` in its `initialize` response
21    /// is using this.
22    #[default]
23    Utf16,
24}
25
26impl PositionEncoding {
27    /// Parse the negotiated encoding from a server's `initialize` response
28    /// `capabilities`. Absence of `positionEncoding`, or any value other
29    /// than `"utf-8"`, means UTF-16 per spec — this also covers a server
30    /// that (incorrectly) echoes `"utf-32"`, which hjkl doesn't implement;
31    /// falling back to UTF-16 is the spec-mandated safe default rather than
32    /// silently mis-converting UTF-32 offsets as UTF-16 ones.
33    pub fn from_capabilities(capabilities: &serde_json::Value) -> Self {
34        match capabilities
35            .get("positionEncoding")
36            .and_then(|v| v.as_str())
37        {
38            Some("utf-8") => Self::Utf8,
39            _ => Self::Utf16,
40        }
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::PositionEncoding;
47    use serde_json::json;
48
49    #[test]
50    fn missing_field_defaults_to_utf16() {
51        assert_eq!(
52            PositionEncoding::from_capabilities(&json!({})),
53            PositionEncoding::Utf16
54        );
55    }
56
57    #[test]
58    fn explicit_utf8() {
59        assert_eq!(
60            PositionEncoding::from_capabilities(&json!({ "positionEncoding": "utf-8" })),
61            PositionEncoding::Utf8
62        );
63    }
64
65    #[test]
66    fn explicit_utf16() {
67        assert_eq!(
68            PositionEncoding::from_capabilities(&json!({ "positionEncoding": "utf-16" })),
69            PositionEncoding::Utf16
70        );
71    }
72
73    #[test]
74    fn unknown_value_defaults_to_utf16() {
75        assert_eq!(
76            PositionEncoding::from_capabilities(&json!({ "positionEncoding": "utf-32" })),
77            PositionEncoding::Utf16
78        );
79    }
80
81    #[test]
82    fn null_capabilities_defaults_to_utf16() {
83        assert_eq!(
84            PositionEncoding::from_capabilities(&serde_json::Value::Null),
85            PositionEncoding::Utf16
86        );
87    }
88}