1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::path::PathBuf;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "snake_case")]
8pub enum DaemonState {
9 Starting,
10 Ready,
11 Failed,
12}
13
14#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub enum RustSymbolKind {
17 Module,
18 Struct,
19 Enum,
20 EnumVariant,
21 Trait,
22 Impl,
23 Function,
24 Method,
25 AssociatedFunction,
26 Field,
27 TypeAlias,
28 Const,
29 Static,
30 Macro,
31 Union,
32 TypeParameter,
33 Variable,
34 Unknown,
35}
36
37impl fmt::Display for RustSymbolKind {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 let value = match self {
40 Self::Module => "module",
41 Self::Struct => "struct",
42 Self::Enum => "enum",
43 Self::EnumVariant => "enum_variant",
44 Self::Trait => "trait",
45 Self::Impl => "impl",
46 Self::Function => "function",
47 Self::Method => "method",
48 Self::AssociatedFunction => "associated_function",
49 Self::Field => "field",
50 Self::TypeAlias => "type_alias",
51 Self::Const => "const",
52 Self::Static => "static",
53 Self::Macro => "macro",
54 Self::Union => "union",
55 Self::TypeParameter => "type_parameter",
56 Self::Variable => "variable",
57 Self::Unknown => "unknown",
58 };
59 f.write_str(value)
60 }
61}
62
63impl RustSymbolKind {
64 pub const CLI_VALUES: &'static [&'static str] = &[
65 "module",
66 "struct",
67 "enum",
68 "enum_variant",
69 "trait",
70 "impl",
71 "function",
72 "method",
73 "associated_function",
74 "field",
75 "type_alias",
76 "const",
77 "static",
78 "macro",
79 "union",
80 "type_parameter",
81 "variable",
82 ];
83
84 pub fn parse_filter(value: &str) -> Option<Self> {
85 match value {
86 "module" => Some(Self::Module),
87 "struct" => Some(Self::Struct),
88 "enum" => Some(Self::Enum),
89 "enum_variant" => Some(Self::EnumVariant),
90 "trait" => Some(Self::Trait),
91 "impl" => Some(Self::Impl),
92 "function" => Some(Self::Function),
93 "method" => Some(Self::Method),
94 "associated_function" => Some(Self::AssociatedFunction),
95 "field" => Some(Self::Field),
96 "type_alias" => Some(Self::TypeAlias),
97 "const" => Some(Self::Const),
98 "static" => Some(Self::Static),
99 "macro" => Some(Self::Macro),
100 "union" => Some(Self::Union),
101 "type_parameter" => Some(Self::TypeParameter),
102 "variable" => Some(Self::Variable),
103 _ => None,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
109#[serde(rename_all = "lowercase")]
110pub enum CallDirection {
111 Incoming,
112 Outgoing,
113}
114
115impl fmt::Display for CallDirection {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.write_str(match self {
118 Self::Incoming => "incoming",
119 Self::Outgoing => "outgoing",
120 })
121 }
122}
123
124impl FromStr for CallDirection {
125 type Err = String;
126
127 fn from_str(value: &str) -> Result<Self, Self::Err> {
128 match value {
129 "incoming" => Ok(Self::Incoming),
130 "outgoing" => Ok(Self::Outgoing),
131 _ => Err(format!(
132 "invalid call direction '{value}'; allowed values: incoming, outgoing"
133 )),
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
139#[serde(rename_all = "snake_case")]
140pub enum RelationMode {
141 Implementations,
142}
143
144impl fmt::Display for RelationMode {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 f.write_str(match self {
147 Self::Implementations => "implementations",
148 })
149 }
150}
151
152impl FromStr for RelationMode {
153 type Err = String;
154
155 fn from_str(value: &str) -> Result<Self, Self::Err> {
156 match value {
157 "implementations" => Ok(Self::Implementations),
158 _ => Err(format!(
159 "invalid relation mode '{value}'; allowed values: implementations"
160 )),
161 }
162 }
163}
164
165#[derive(Debug, Serialize, Deserialize)]
166pub struct DaemonStatusResponse {
167 pub state: DaemonState,
168 pub workspace_root: String,
169 pub process_id: Option<u32>,
170 pub error: Option<String>,
171}
172
173#[derive(Debug, Serialize, Deserialize)]
174pub struct SymbolQueryRequest {
175 pub name: String,
176 pub kind: String,
177 pub exact: bool,
178 #[serde(default)]
179 pub include_body: bool,
180 #[serde(default = "default_body_max_lines")]
181 pub max_lines: usize,
182}
183
184#[derive(Debug, Serialize, Deserialize)]
185pub struct SymbolItem {
186 pub name: String,
187 pub kind: RustSymbolKind,
188 pub file: String,
189 pub line: u32,
190 pub col: u32,
191 pub container_name: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub body: Option<String>,
194}
195
196#[derive(Debug, Serialize, Deserialize)]
197pub struct OutlineQueryRequest {
198 pub file: PathBuf,
199 #[serde(default)]
200 pub include_body: bool,
201 #[serde(default = "default_body_max_lines")]
202 pub max_lines: usize,
203}
204
205#[derive(Debug, Serialize, Deserialize)]
206pub struct OutlineItem {
207 pub name: String,
208 pub kind: RustSymbolKind,
209 pub detail: Option<String>,
210 pub line: u32,
211 pub col: u32,
212 pub end_line: u32,
213 pub children: Vec<OutlineItem>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub body: Option<String>,
216}
217
218#[derive(Debug, Serialize, Deserialize)]
219pub struct DefinitionQueryRequest {
220 pub file: PathBuf,
221 pub line: u32,
222 pub col: u32,
223 #[serde(default)]
224 pub include_body: bool,
225 #[serde(default = "default_body_max_lines")]
226 pub max_lines: usize,
227}
228
229#[derive(Debug, Serialize, Deserialize)]
230pub struct BodyQueryRequest {
231 pub file: PathBuf,
232 pub line: u32,
233 pub col: u32,
234 #[serde(default = "default_body_max_lines")]
235 pub max_lines: usize,
236}
237
238fn default_body_max_lines() -> usize {
240 100
241}
242
243fn default_call_depth() -> u32 {
244 1
245}
246
247#[derive(Debug, Serialize, Deserialize)]
248pub struct DefinitionItem {
249 pub file: String,
250 pub line: u32,
251 pub col: u32,
252 pub end_line: u32,
253 pub end_col: u32,
254 pub snippet: Option<String>,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub body: Option<String>,
257}
258
259#[derive(Debug, Serialize, Deserialize)]
260pub struct RelationQueryRequest {
261 pub file: PathBuf,
262 pub line: u32,
263 pub col: u32,
264 pub mode: RelationMode,
265}
266
267#[derive(Debug, Serialize, Deserialize)]
268pub struct RelationItem {
269 pub file: String,
270 pub line: u32,
271 pub col: u32,
272 pub end_line: u32,
273 pub end_col: u32,
274}
275
276#[derive(Debug, Serialize, Deserialize)]
277pub struct ReferenceQueryRequest {
278 pub file: PathBuf,
279 pub line: u32,
280 pub col: u32,
281}
282
283#[derive(Debug, Serialize, Deserialize)]
284pub struct CallQueryRequest {
285 pub file: PathBuf,
286 pub line: u32,
287 pub col: u32,
288 pub direction: CallDirection,
289 #[serde(default = "default_call_depth")]
290 pub depth: u32,
291}
292
293#[derive(Debug, Serialize, Deserialize)]
294pub struct ReferenceItem {
295 pub file: String,
296 pub line: u32,
297 pub col: u32,
298 pub end_line: u32,
299 pub end_col: u32,
300}
301
302#[derive(Debug, Serialize, Deserialize)]
303pub struct CallItem {
304 pub name: String,
305 pub kind: RustSymbolKind,
306 pub file: String,
307 pub line: u32,
308 pub col: u32,
309 pub end_line: u32,
310 pub end_col: u32,
311 pub direction: CallDirection,
312 pub depth: u32,
313}
314
315#[derive(Debug, Serialize, Deserialize)]
316pub struct BodyItem {
317 pub file: String,
318 pub line: u32,
319 pub col: u32,
320 pub end_line: u32,
321 pub end_col: u32,
322 pub body: String,
323 pub total_lines: usize,
324 pub is_truncated: bool,
325}
326
327#[derive(Debug, Serialize, Deserialize)]
328pub struct HoverQueryRequest {
329 pub file: PathBuf,
330 pub line: u32,
331 pub col: u32,
332}
333
334#[derive(Debug, Serialize, Deserialize)]
335pub struct HoverRange {
336 pub start_line: u32,
337 pub start_col: u32,
338 pub end_line: u32,
339 pub end_col: u32,
340}
341
342#[derive(Debug, Serialize, Deserialize)]
343pub struct HoverItem {
344 pub file: String,
345 pub line: u32,
346 pub col: u32,
347 pub contents: String,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub range: Option<HoverRange>,
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_daemon_status_json_roundtrip() {
358 let status = DaemonStatusResponse {
359 state: DaemonState::Ready,
360 workspace_root: "/path/to/workspace".to_string(),
361 process_id: Some(12345),
362 error: None,
363 };
364 let json = serde_json::to_string(&status).unwrap();
365 let decoded: DaemonStatusResponse = serde_json::from_str(&json).unwrap();
366 assert_eq!(decoded.state, DaemonState::Ready);
367 assert_eq!(decoded.process_id, Some(12345));
368 assert_eq!(decoded.workspace_root, "/path/to/workspace");
369 }
370
371 #[test]
372 fn test_symbol_item_json_roundtrip() {
373 let item = SymbolItem {
374 name: "ExampleStruct".to_string(),
375 kind: RustSymbolKind::Struct,
376 file: "tests/code_example.rs".to_string(),
377 line: 15,
378 col: 1,
379 container_name: Some("lsp".to_string()),
380 body: None,
381 };
382
383 let json = serde_json::to_string(&item).unwrap();
384 let decoded: SymbolItem = serde_json::from_str(&json).unwrap();
385 assert_eq!(decoded.name, "ExampleStruct");
386 assert_eq!(decoded.kind, RustSymbolKind::Struct);
387 }
388
389 #[test]
390 fn test_body_query_request_defaults_to_100_lines() {
391 let request: BodyQueryRequest =
392 serde_json::from_str(r#"{"file":"tests/code_example.rs","line":15,"col":10}"#).unwrap();
393 assert_eq!(request.max_lines, 100);
394 }
395
396 #[test]
397 fn test_hover_item_json_roundtrip() {
398 let item = HoverItem {
399 file: "tests/code_example.rs".to_string(),
400 line: 155,
401 col: 5,
402 contents: "Runs a long example.".to_string(),
403 range: Some(HoverRange {
404 start_line: 155,
405 start_col: 5,
406 end_line: 155,
407 end_col: 17,
408 }),
409 };
410
411 let json = serde_json::to_string(&item).unwrap();
412 let decoded: HoverItem = serde_json::from_str(&json).unwrap();
413 assert_eq!(decoded.contents, "Runs a long example.");
414 assert_eq!(decoded.range.unwrap().start_line, 155);
415 }
416
417 #[test]
418 fn test_symbol_kind_filter_uses_canonical_values() {
419 assert_eq!(
420 RustSymbolKind::parse_filter("function"),
421 Some(RustSymbolKind::Function)
422 );
423 assert_eq!(
424 RustSymbolKind::parse_filter("trait"),
425 Some(RustSymbolKind::Trait)
426 );
427 assert_eq!(RustSymbolKind::parse_filter("fn"), None);
428 assert_eq!(RustSymbolKind::parse_filter("var"), None);
429 }
430
431 #[test]
432 fn test_navigation_modes_use_stable_wire_values() {
433 assert_eq!(CallDirection::Outgoing.to_string(), "outgoing");
434 assert_eq!(
435 serde_json::to_string(&CallDirection::Incoming).unwrap(),
436 "\"incoming\""
437 );
438 assert_eq!(
439 serde_json::from_str::<RelationMode>("\"implementations\"").unwrap(),
440 RelationMode::Implementations
441 );
442 assert!("invalid".parse::<CallDirection>().is_err());
443 assert!("invalid".parse::<RelationMode>().is_err());
444 let call_request: CallQueryRequest = serde_json::from_str(
445 r#"{"file":"tests/code_example.rs","line":1,"col":1,"direction":"incoming"}"#,
446 )
447 .unwrap();
448 assert_eq!(call_request.depth, 1);
449 }
450}