codineer-lsp 0.6.4

Language Server Protocol client integration for Codineer
Documentation
use std::fmt::{Display, Formatter};
use std::path::PathBuf;

#[derive(Debug)]
pub enum LspError {
    Io(std::io::Error),
    Json(serde_json::Error),
    InvalidHeader(String),
    MissingContentLength,
    InvalidContentLength(String),
    UnsupportedDocument(PathBuf),
    UnknownServer(String),
    DuplicateExtension {
        extension: String,
        existing_server: String,
        new_server: String,
    },
    PathToUrl(PathBuf),
    Protocol(String),
    PayloadTooLarge {
        content_length: usize,
        limit: usize,
    },
}

impl Display for LspError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io(error) => write!(f, "{error}"),
            Self::Json(error) => write!(f, "{error}"),
            Self::InvalidHeader(header) => write!(f, "invalid LSP header: {header}"),
            Self::MissingContentLength => write!(f, "missing LSP Content-Length header"),
            Self::InvalidContentLength(value) => {
                write!(f, "invalid LSP Content-Length value: {value}")
            }
            Self::UnsupportedDocument(path) => {
                write!(f, "no LSP server configured for {}", path.display())
            }
            Self::UnknownServer(name) => write!(f, "unknown LSP server: {name}"),
            Self::DuplicateExtension {
                extension,
                existing_server,
                new_server,
            } => write!(
                f,
                "duplicate LSP extension mapping for {extension}: {existing_server} and {new_server}"
            ),
            Self::PathToUrl(path) => write!(f, "failed to convert path to file URL: {}", path.display()),
            Self::Protocol(message) => write!(f, "LSP protocol error: {message}"),
            Self::PayloadTooLarge {
                content_length,
                limit,
            } => write!(
                f,
                "LSP payload too large: Content-Length {content_length} exceeds {limit} byte limit"
            ),
        }
    }
}

impl std::error::Error for LspError {}

impl From<std::io::Error> for LspError {
    fn from(value: std::io::Error) -> Self {
        Self::Io(value)
    }
}

impl From<serde_json::Error> for LspError {
    fn from(value: serde_json::Error) -> Self {
        Self::Json(value)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_formats_all_variants() {
        let io_err = LspError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
        assert!(io_err.to_string().contains("gone"));

        let json_str = "not json";
        let json_err: serde_json::Error = serde_json::from_str::<bool>(json_str).unwrap_err();
        let json_display = LspError::Json(json_err);
        assert!(!json_display.to_string().is_empty());

        assert_eq!(
            LspError::InvalidHeader("bad".into()).to_string(),
            "invalid LSP header: bad"
        );
        assert_eq!(
            LspError::MissingContentLength.to_string(),
            "missing LSP Content-Length header"
        );
        assert_eq!(
            LspError::InvalidContentLength("xyz".into()).to_string(),
            "invalid LSP Content-Length value: xyz"
        );
        assert!(LspError::UnsupportedDocument(PathBuf::from("/foo.txt"))
            .to_string()
            .contains("/foo.txt"));
        assert_eq!(
            LspError::UnknownServer("rust-analyzer".into()).to_string(),
            "unknown LSP server: rust-analyzer"
        );
        assert!(LspError::DuplicateExtension {
            extension: ".rs".into(),
            existing_server: "a".into(),
            new_server: "b".into(),
        }
        .to_string()
        .contains(".rs"));
        assert!(LspError::PathToUrl(PathBuf::from("/bad"))
            .to_string()
            .contains("/bad"));
        assert_eq!(
            LspError::Protocol("timeout".into()).to_string(),
            "LSP protocol error: timeout"
        );
        assert!(LspError::PayloadTooLarge {
            content_length: 100_000_000,
            limit: 8_000_000,
        }
        .to_string()
        .contains("100000000"));
    }

    #[test]
    fn from_io_error_converts() {
        let err: LspError = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe").into();
        assert!(matches!(err, LspError::Io(_)));
    }

    #[test]
    fn from_json_error_converts() {
        let json_err: serde_json::Error = serde_json::from_str::<bool>("x").unwrap_err();
        let err: LspError = json_err.into();
        assert!(matches!(err, LspError::Json(_)));
    }
}