use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DaemonState {
Starting,
Ready,
Failed,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RustSymbolKind {
Module,
Struct,
Enum,
EnumVariant,
Trait,
Impl,
Function,
Method,
AssociatedFunction,
Field,
TypeAlias,
Const,
Static,
Macro,
Union,
TypeParameter,
Variable,
Unknown,
}
impl fmt::Display for RustSymbolKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self {
Self::Module => "module",
Self::Struct => "struct",
Self::Enum => "enum",
Self::EnumVariant => "enum_variant",
Self::Trait => "trait",
Self::Impl => "impl",
Self::Function => "function",
Self::Method => "method",
Self::AssociatedFunction => "associated_function",
Self::Field => "field",
Self::TypeAlias => "type_alias",
Self::Const => "const",
Self::Static => "static",
Self::Macro => "macro",
Self::Union => "union",
Self::TypeParameter => "type_parameter",
Self::Variable => "variable",
Self::Unknown => "unknown",
};
f.write_str(value)
}
}
impl RustSymbolKind {
pub const CLI_VALUES: &'static [&'static str] = &[
"module",
"struct",
"enum",
"enum_variant",
"trait",
"impl",
"function",
"method",
"associated_function",
"field",
"type_alias",
"const",
"static",
"macro",
"union",
"type_parameter",
"variable",
];
pub fn parse_filter(value: &str) -> Option<Self> {
match value {
"module" => Some(Self::Module),
"struct" => Some(Self::Struct),
"enum" => Some(Self::Enum),
"enum_variant" => Some(Self::EnumVariant),
"trait" => Some(Self::Trait),
"impl" => Some(Self::Impl),
"function" => Some(Self::Function),
"method" => Some(Self::Method),
"associated_function" => Some(Self::AssociatedFunction),
"field" => Some(Self::Field),
"type_alias" => Some(Self::TypeAlias),
"const" => Some(Self::Const),
"static" => Some(Self::Static),
"macro" => Some(Self::Macro),
"union" => Some(Self::Union),
"type_parameter" => Some(Self::TypeParameter),
"variable" => Some(Self::Variable),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CallDirection {
Incoming,
Outgoing,
}
impl fmt::Display for CallDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Incoming => "incoming",
Self::Outgoing => "outgoing",
})
}
}
impl FromStr for CallDirection {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"incoming" => Ok(Self::Incoming),
"outgoing" => Ok(Self::Outgoing),
_ => Err(format!(
"invalid call direction '{value}'; allowed values: incoming, outgoing"
)),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RelationMode {
Implementations,
}
impl fmt::Display for RelationMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Implementations => "implementations",
})
}
}
impl FromStr for RelationMode {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"implementations" => Ok(Self::Implementations),
_ => Err(format!(
"invalid relation mode '{value}'; allowed values: implementations"
)),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DaemonStatusResponse {
pub state: DaemonState,
pub workspace_root: String,
pub process_id: Option<u32>,
pub error: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SymbolQueryRequest {
pub name: String,
pub kind: String,
pub exact: bool,
#[serde(default)]
pub include_body: bool,
#[serde(default = "default_body_max_lines")]
pub max_lines: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SymbolItem {
pub name: String,
pub kind: RustSymbolKind,
pub file: String,
pub line: u32,
pub col: u32,
pub container_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OutlineQueryRequest {
pub file: PathBuf,
#[serde(default)]
pub include_body: bool,
#[serde(default = "default_body_max_lines")]
pub max_lines: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OutlineItem {
pub name: String,
pub kind: RustSymbolKind,
pub detail: Option<String>,
pub line: u32,
pub col: u32,
pub end_line: u32,
pub children: Vec<OutlineItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DefinitionQueryRequest {
pub file: PathBuf,
pub line: u32,
pub col: u32,
#[serde(default)]
pub include_body: bool,
#[serde(default = "default_body_max_lines")]
pub max_lines: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BodyQueryRequest {
pub file: PathBuf,
pub line: u32,
pub col: u32,
#[serde(default = "default_body_max_lines")]
pub max_lines: usize,
}
fn default_body_max_lines() -> usize {
100
}
fn default_call_depth() -> u32 {
1
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DefinitionItem {
pub file: String,
pub line: u32,
pub col: u32,
pub end_line: u32,
pub end_col: u32,
pub snippet: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RelationQueryRequest {
pub file: PathBuf,
pub line: u32,
pub col: u32,
pub mode: RelationMode,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RelationItem {
pub file: String,
pub line: u32,
pub col: u32,
pub end_line: u32,
pub end_col: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReferenceQueryRequest {
pub file: PathBuf,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CallQueryRequest {
pub file: PathBuf,
pub line: u32,
pub col: u32,
pub direction: CallDirection,
#[serde(default = "default_call_depth")]
pub depth: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReferenceItem {
pub file: String,
pub line: u32,
pub col: u32,
pub end_line: u32,
pub end_col: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CallItem {
pub name: String,
pub kind: RustSymbolKind,
pub file: String,
pub line: u32,
pub col: u32,
pub end_line: u32,
pub end_col: u32,
pub direction: CallDirection,
pub depth: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BodyItem {
pub file: String,
pub line: u32,
pub col: u32,
pub end_line: u32,
pub end_col: u32,
pub body: String,
pub total_lines: usize,
pub is_truncated: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HoverQueryRequest {
pub file: PathBuf,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HoverRange {
pub start_line: u32,
pub start_col: u32,
pub end_line: u32,
pub end_col: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HoverItem {
pub file: String,
pub line: u32,
pub col: u32,
pub contents: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub range: Option<HoverRange>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_daemon_status_json_roundtrip() {
let status = DaemonStatusResponse {
state: DaemonState::Ready,
workspace_root: "/path/to/workspace".to_string(),
process_id: Some(12345),
error: None,
};
let json = serde_json::to_string(&status).unwrap();
let decoded: DaemonStatusResponse = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.state, DaemonState::Ready);
assert_eq!(decoded.process_id, Some(12345));
assert_eq!(decoded.workspace_root, "/path/to/workspace");
}
#[test]
fn test_symbol_item_json_roundtrip() {
let item = SymbolItem {
name: "ExampleStruct".to_string(),
kind: RustSymbolKind::Struct,
file: "tests/code_example.rs".to_string(),
line: 15,
col: 1,
container_name: Some("lsp".to_string()),
body: None,
};
let json = serde_json::to_string(&item).unwrap();
let decoded: SymbolItem = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.name, "ExampleStruct");
assert_eq!(decoded.kind, RustSymbolKind::Struct);
}
#[test]
fn test_body_query_request_defaults_to_100_lines() {
let request: BodyQueryRequest =
serde_json::from_str(r#"{"file":"tests/code_example.rs","line":15,"col":10}"#).unwrap();
assert_eq!(request.max_lines, 100);
}
#[test]
fn test_hover_item_json_roundtrip() {
let item = HoverItem {
file: "tests/code_example.rs".to_string(),
line: 155,
col: 5,
contents: "Runs a long example.".to_string(),
range: Some(HoverRange {
start_line: 155,
start_col: 5,
end_line: 155,
end_col: 17,
}),
};
let json = serde_json::to_string(&item).unwrap();
let decoded: HoverItem = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.contents, "Runs a long example.");
assert_eq!(decoded.range.unwrap().start_line, 155);
}
#[test]
fn test_symbol_kind_filter_uses_canonical_values() {
assert_eq!(
RustSymbolKind::parse_filter("function"),
Some(RustSymbolKind::Function)
);
assert_eq!(
RustSymbolKind::parse_filter("trait"),
Some(RustSymbolKind::Trait)
);
assert_eq!(RustSymbolKind::parse_filter("fn"), None);
assert_eq!(RustSymbolKind::parse_filter("var"), None);
}
#[test]
fn test_navigation_modes_use_stable_wire_values() {
assert_eq!(CallDirection::Outgoing.to_string(), "outgoing");
assert_eq!(
serde_json::to_string(&CallDirection::Incoming).unwrap(),
"\"incoming\""
);
assert_eq!(
serde_json::from_str::<RelationMode>("\"implementations\"").unwrap(),
RelationMode::Implementations
);
assert!("invalid".parse::<CallDirection>().is_err());
assert!("invalid".parse::<RelationMode>().is_err());
let call_request: CallQueryRequest = serde_json::from_str(
r#"{"file":"tests/code_example.rs","line":1,"col":1,"direction":"incoming"}"#,
)
.unwrap();
assert_eq!(call_request.depth, 1);
}
}