use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct ServerSpawnFailure {
pub language_id: String,
pub command: String,
pub message: String,
}
impl std::fmt::Display for ServerSpawnFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} ({}): {}",
self.language_id, self.command, self.message
)
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("LSP server initialization failed: {message}")]
LspInitFailed {
message: String,
},
#[error("LSP server error: {code} - {message}")]
LspServerError {
code: i32,
message: String,
},
#[error("MCP server error: {0}")]
McpServer(String),
#[error("document not found: {0}")]
DocumentNotFound(PathBuf),
#[error("no LSP server configured for language: {0}")]
NoServerForLanguage(String),
#[error("no LSP server configured")]
NoServerConfigured,
#[error("configuration error: {0}")]
Config(String),
#[error("configuration file not found: {0}")]
ConfigNotFound(PathBuf),
#[error("invalid configuration: {0}")]
InvalidConfig(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("TOML parsing error: {0}")]
TomlDe(#[from] toml::de::Error),
#[error("TOML serialization error: {0}")]
TomlSer(#[from] toml::ser::Error),
#[error("transport error: {0}")]
Transport(String),
#[error("request timed out after {0} seconds")]
Timeout(u64),
#[error("server shutdown requested")]
Shutdown,
#[error("failed to spawn LSP server '{command}': {source}")]
ServerSpawnFailed {
command: String,
#[source]
source: std::io::Error,
},
#[error("LSP protocol error: {0}")]
LspProtocolError(String),
#[error("invalid URI: {0}")]
InvalidUri(String),
#[error("position encoding error: {0}")]
EncodingError(String),
#[error("LSP server process terminated unexpectedly")]
ServerTerminated,
#[error("invalid tool parameters: {0}")]
InvalidToolParams(String),
#[error("file I/O error for {path:?}: {source}")]
FileIo {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("path outside workspace: {0}")]
PathOutsideWorkspace(PathBuf),
#[error("document limit exceeded: {current}/{max}")]
DocumentLimitExceeded {
current: usize,
max: usize,
},
#[error("file size limit exceeded: {size} bytes (max: {max} bytes)")]
FileSizeLimitExceeded {
size: u64,
max: u64,
},
#[error("some LSP servers failed to initialize: {failed_count}/{total_count} servers")]
PartialServerInit {
failed_count: usize,
total_count: usize,
failures: Vec<ServerSpawnFailure>,
},
#[error("all LSP servers failed to initialize ({count} configured)")]
AllServersFailedToInit {
count: usize,
failures: Vec<ServerSpawnFailure>,
},
#[error("{0}")]
NoServersAvailable(String),
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display_lsp_init_failed() {
let err = Error::LspInitFailed {
message: "server not found".to_string(),
};
assert_eq!(
err.to_string(),
"LSP server initialization failed: server not found"
);
}
#[test]
fn test_error_display_lsp_server_error() {
let err = Error::LspServerError {
code: -32600,
message: "Invalid request".to_string(),
};
assert_eq!(
err.to_string(),
"LSP server error: -32600 - Invalid request"
);
}
#[test]
fn test_error_display_document_not_found() {
let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
assert!(err.to_string().contains("document not found"));
assert!(err.to_string().contains("file.rs"));
}
#[test]
fn test_error_display_no_server_for_language() {
let err = Error::NoServerForLanguage("rust".to_string());
assert_eq!(
err.to_string(),
"no LSP server configured for language: rust"
);
}
#[test]
fn test_error_display_timeout() {
let err = Error::Timeout(30);
assert_eq!(err.to_string(), "request timed out after 30 seconds");
}
#[test]
fn test_error_display_document_limit() {
let err = Error::DocumentLimitExceeded {
current: 150,
max: 100,
};
assert_eq!(err.to_string(), "document limit exceeded: 150/100");
}
#[test]
fn test_error_display_file_size_limit() {
let err = Error::FileSizeLimitExceeded {
size: 20_000_000,
max: 10_000_000,
};
assert!(err.to_string().contains("file size limit exceeded"));
}
#[test]
fn test_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
assert!(matches!(err, Error::Io(_)));
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_error_from_json() {
let json_str = "{invalid json}";
let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
let err: Error = json_err.into();
assert!(matches!(err, Error::Json(_)));
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_error_from_toml_de() {
let toml_str = "[invalid toml";
let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
let err: Error = toml_err.into();
assert!(matches!(err, Error::TomlDe(_)));
}
#[test]
fn test_result_type_alias() {
fn _returns_error() -> Result<i32> {
Err(Error::Config("test error".to_string()))
}
let result: Result<i32> = Ok(42);
assert!(result.is_ok());
if let Ok(value) = result {
assert_eq!(value, 42);
}
}
#[test]
fn test_error_source_chain() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err = Error::ServerSpawnFailed {
command: "rust-analyzer".to_string(),
source: io_err,
};
let source = std::error::Error::source(&err);
assert!(source.is_some());
}
#[test]
fn test_server_spawn_failure_display() {
let failure = ServerSpawnFailure {
language_id: "rust".to_string(),
command: "rust-analyzer".to_string(),
message: "No such file or directory".to_string(),
};
assert_eq!(
failure.to_string(),
"rust (rust-analyzer): No such file or directory"
);
}
#[test]
fn test_server_spawn_failure_debug() {
let failure = ServerSpawnFailure {
language_id: "python".to_string(),
command: "pyright".to_string(),
message: "command not found".to_string(),
};
let debug_str = format!("{failure:?}");
assert!(debug_str.contains("python"));
assert!(debug_str.contains("pyright"));
assert!(debug_str.contains("command not found"));
}
#[test]
fn test_server_spawn_failure_clone() {
let failure = ServerSpawnFailure {
language_id: "typescript".to_string(),
command: "tsserver".to_string(),
message: "failed to start".to_string(),
};
let cloned = failure.clone();
assert_eq!(failure.language_id, cloned.language_id);
assert_eq!(failure.command, cloned.command);
assert_eq!(failure.message, cloned.message);
}
#[test]
fn test_error_display_partial_server_init() {
let err = Error::PartialServerInit {
failed_count: 2,
total_count: 3,
failures: vec![],
};
assert_eq!(
err.to_string(),
"some LSP servers failed to initialize: 2/3 servers"
);
}
#[test]
fn test_error_display_all_servers_failed_to_init() {
let err = Error::AllServersFailedToInit {
count: 2,
failures: vec![],
};
assert_eq!(
err.to_string(),
"all LSP servers failed to initialize (2 configured)"
);
}
#[test]
fn test_error_all_servers_failed_with_failures() {
let failures = vec![
ServerSpawnFailure {
language_id: "rust".to_string(),
command: "rust-analyzer".to_string(),
message: "not found".to_string(),
},
ServerSpawnFailure {
language_id: "python".to_string(),
command: "pyright".to_string(),
message: "permission denied".to_string(),
},
];
let err = Error::AllServersFailedToInit { count: 2, failures };
assert!(err.to_string().contains("all LSP servers failed"));
assert!(err.to_string().contains("2 configured"));
}
#[test]
fn test_error_partial_server_init_with_failures() {
let failures = vec![ServerSpawnFailure {
language_id: "python".to_string(),
command: "pyright".to_string(),
message: "not found".to_string(),
}];
let err = Error::PartialServerInit {
failed_count: 1,
total_count: 2,
failures,
};
assert!(err.to_string().contains("some LSP servers failed"));
assert!(err.to_string().contains("1/2"));
}
#[test]
fn test_error_display_no_servers_available() {
let err =
Error::NoServersAvailable("none configured or all failed to initialize".to_string());
assert_eq!(
err.to_string(),
"none configured or all failed to initialize"
);
}
#[test]
fn test_error_no_servers_available_with_custom_message() {
let custom_msg = "none configured or all failed to initialize";
let err = Error::NoServersAvailable(custom_msg.to_string());
assert_eq!(err.to_string(), custom_msg);
}
}