Skip to main content

mcpls_core/config/
position_encodings.rs

1//! The client-preference order of position encodings offered to servers.
2
3use std::borrow::Cow;
4
5use serde::{Deserialize, Serialize};
6
7use crate::bridge::PositionEncoding;
8
9/// Why a list is not a valid [`PositionEncodings`].
10#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
11pub enum InvalidPositionEncodings {
12    /// The list had no entries.
13    #[error("position_encodings cannot be empty")]
14    Empty,
15}
16
17/// A non-empty position-encoding preference order, offered to each spawned
18/// server as `capabilities.general.positionEncodings` during `initialize`.
19///
20/// Deserializes from a list of `"utf-8"`, `"utf-16"` or `"utf-32"` strings and
21/// rejects an empty list or any other value at load time.
22///
23/// # Examples
24///
25/// ```
26/// use mcpls_core::bridge::PositionEncoding;
27/// use mcpls_core::config::PositionEncodings;
28///
29/// assert!(PositionEncodings::new(vec![]).is_err());
30/// let encodings = PositionEncodings::new(vec![PositionEncoding::Utf32]).unwrap();
31/// assert_eq!(encodings.as_slice(), [PositionEncoding::Utf32]);
32/// assert_eq!(
33///     PositionEncodings::DEFAULT.as_slice(),
34///     [PositionEncoding::Utf8, PositionEncoding::Utf16]
35/// );
36/// ```
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(try_from = "Vec<PositionEncoding>", into = "Vec<PositionEncoding>")]
39pub struct PositionEncodings(Cow<'static, [PositionEncoding]>);
40
41impl PositionEncodings {
42    /// `utf-8` first, then `utf-16`.
43    ///
44    /// `utf-8` is listed first deliberately: rust-analyzer and clangd both
45    /// negotiate down to it, so the non-UTF-16 conversion path in
46    /// `bridge/encoding.rs` is the common case, not an edge case.
47    pub const DEFAULT: Self = Self(Cow::Borrowed(&[
48        PositionEncoding::Utf8,
49        PositionEncoding::Utf16,
50    ]));
51
52    /// Builds a preference order from `encodings`.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`InvalidPositionEncodings::Empty`] for an empty list.
57    pub fn new(encodings: Vec<PositionEncoding>) -> Result<Self, InvalidPositionEncodings> {
58        if encodings.is_empty() {
59            return Err(InvalidPositionEncodings::Empty);
60        }
61        Ok(Self(Cow::Owned(encodings)))
62    }
63
64    /// The encodings in preference order; never empty.
65    #[must_use]
66    pub fn as_slice(&self) -> &[PositionEncoding] {
67        &self.0
68    }
69}
70
71impl Default for PositionEncodings {
72    fn default() -> Self {
73        Self::DEFAULT
74    }
75}
76
77impl TryFrom<Vec<PositionEncoding>> for PositionEncodings {
78    type Error = InvalidPositionEncodings;
79
80    fn try_from(encodings: Vec<PositionEncoding>) -> Result<Self, Self::Error> {
81        Self::new(encodings)
82    }
83}
84
85impl From<PositionEncodings> for Vec<PositionEncoding> {
86    fn from(encodings: PositionEncodings) -> Self {
87        encodings.0.into_owned()
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[derive(Debug, Serialize, Deserialize, PartialEq)]
96    struct Holder {
97        encodings: PositionEncodings,
98    }
99
100    #[test]
101    fn test_rejects_empty() {
102        assert_eq!(
103            PositionEncodings::new(vec![]),
104            Err(InvalidPositionEncodings::Empty)
105        );
106        assert!(toml::from_str::<Holder>("encodings = []").is_err());
107    }
108
109    #[test]
110    fn test_rejects_unknown_value_listing_valid_ones() {
111        let err = toml::from_str::<Holder>(r#"encodings = ["utf-7"]"#)
112            .unwrap_err()
113            .to_string();
114        assert!(err.contains("utf-7"), "{err}");
115        assert!(err.contains("utf-8") && err.contains("utf-16") && err.contains("utf-32"));
116    }
117
118    #[test]
119    fn test_preserves_order_and_round_trips() {
120        let holder: Holder = toml::from_str(r#"encodings = ["utf-32", "utf-8"]"#).unwrap();
121        assert_eq!(
122            holder.encodings.as_slice(),
123            [PositionEncoding::Utf32, PositionEncoding::Utf8]
124        );
125        let text = toml::to_string(&holder).unwrap();
126        assert_eq!(toml::from_str::<Holder>(&text).unwrap(), holder);
127    }
128
129    #[test]
130    fn test_default_round_trips() {
131        let holder = Holder {
132            encodings: PositionEncodings::DEFAULT,
133        };
134        let text = toml::to_string(&holder).unwrap();
135        assert_eq!(toml::from_str::<Holder>(&text).unwrap(), holder);
136    }
137}