Skip to main content

rustant_tools/lsp/
types.rs

1//! Subset of Language Server Protocol (LSP) types for Rustant's LSP client integration.
2//!
3//! These types are self-contained and do not depend on any external LSP crate.
4//! They implement the portions of the LSP specification needed for hover, completion,
5//! diagnostics, references, rename, formatting, and initialization requests.
6//!
7//! All types use `serde` for JSON serialization/deserialization, with `camelCase`
8//! field renaming applied where the LSP specification requires it.
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::HashMap;
13
14// ---------------------------------------------------------------------------
15// Basic location types
16// ---------------------------------------------------------------------------
17
18/// A zero-based position in a text document (line and character offset).
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct Position {
21    /// Zero-based line number.
22    pub line: u32,
23    /// Zero-based character offset (UTF-16 code units).
24    pub character: u32,
25}
26
27/// A range within a text document expressed as start and end [`Position`]s.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct Range {
30    pub start: Position,
31    pub end: Position,
32}
33
34/// Represents a location inside a resource, such as a line inside a text file.
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct Location {
37    pub uri: String,
38    pub range: Range,
39}
40
41// ---------------------------------------------------------------------------
42// Text document identifiers
43// ---------------------------------------------------------------------------
44
45/// Identifies a text document by its URI.
46#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub struct TextDocumentIdentifier {
48    pub uri: String,
49}
50
51/// An item to transfer a text document from the client to the server.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct TextDocumentItem {
55    pub uri: String,
56    pub language_id: String,
57    pub version: i32,
58    pub text: String,
59}
60
61/// Parameters for requests that operate on a text document at a given position.
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct TextDocumentPositionParams {
65    pub text_document: TextDocumentIdentifier,
66    pub position: Position,
67}
68
69// ---------------------------------------------------------------------------
70// Hover
71// ---------------------------------------------------------------------------
72
73/// The result of a hover request.
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct HoverResult {
76    pub contents: HoverContents,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub range: Option<Range>,
79}
80
81/// The contents of a hover result.  Three shapes are accepted by the LSP spec.
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(untagged)]
84pub enum HoverContents {
85    /// A single `MarkedString`.
86    Scalar(MarkedString),
87    /// A `MarkupContent` value.
88    Markup(MarkupContent),
89    /// An array of `MarkedString` values.
90    Array(Vec<MarkedString>),
91}
92
93/// A marked string is either a plain string or a code block with a language.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(untagged)]
96pub enum MarkedString {
97    /// A plain string.
98    String(String),
99    /// A code block with a language identifier.
100    LanguageString { language: String, value: String },
101}
102
103/// Human-readable text with a rendering format.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct MarkupContent {
106    /// The format of the content (e.g. `"plaintext"` or `"markdown"`).
107    pub kind: String,
108    pub value: String,
109}
110
111// ---------------------------------------------------------------------------
112// Completion
113// ---------------------------------------------------------------------------
114
115/// A completion item represents a text snippet that is proposed to complete
116/// text that is being typed.
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct CompletionItem {
120    /// The label of this completion item (shown in the UI).
121    pub label: String,
122    /// The kind of this completion item (maps to `CompletionItemKind`).
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub kind: Option<u32>,
125    /// A human-readable string with additional information.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub detail: Option<String>,
128    /// Documentation for this completion item (can be string or MarkupContent).
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub documentation: Option<Value>,
131    /// A string that should be inserted into a document when selecting this item.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub insert_text: Option<String>,
134}
135
136/// The response to a completion request.
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[serde(untagged)]
139pub enum CompletionResponse {
140    /// A simple array of completion items.
141    Array(Vec<CompletionItem>),
142    /// A completion list that can indicate whether the list is incomplete.
143    List(CompletionList),
144}
145
146/// A collection of completion items along with an `is_incomplete` flag.
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct CompletionList {
150    /// If `true`, further typing should re-request completions.
151    pub is_incomplete: bool,
152    pub items: Vec<CompletionItem>,
153}
154
155// ---------------------------------------------------------------------------
156// Diagnostics
157// ---------------------------------------------------------------------------
158
159/// Diagnostic severity levels as defined by LSP.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161#[repr(u8)]
162pub enum DiagnosticSeverity {
163    Error = 1,
164    Warning = 2,
165    Information = 3,
166    Hint = 4,
167}
168
169impl Serialize for DiagnosticSeverity {
170    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
171    where
172        S: serde::Serializer,
173    {
174        serializer.serialize_u8(*self as u8)
175    }
176}
177
178impl<'de> Deserialize<'de> for DiagnosticSeverity {
179    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
180    where
181        D: serde::Deserializer<'de>,
182    {
183        let value = u8::deserialize(deserializer)?;
184        match value {
185            1 => Ok(DiagnosticSeverity::Error),
186            2 => Ok(DiagnosticSeverity::Warning),
187            3 => Ok(DiagnosticSeverity::Information),
188            4 => Ok(DiagnosticSeverity::Hint),
189            other => Err(serde::de::Error::custom(format!(
190                "invalid DiagnosticSeverity value: {other}"
191            ))),
192        }
193    }
194}
195
196/// Represents a diagnostic, such as a compiler error or warning.
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198pub struct Diagnostic {
199    pub range: Range,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub severity: Option<DiagnosticSeverity>,
202    pub message: String,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub source: Option<String>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub code: Option<Value>,
207}
208
209/// Parameters for the `textDocument/publishDiagnostics` notification.
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub struct PublishDiagnosticsParams {
212    pub uri: String,
213    pub diagnostics: Vec<Diagnostic>,
214}
215
216// ---------------------------------------------------------------------------
217// Edits
218// ---------------------------------------------------------------------------
219
220/// A text edit applicable to a text document.
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase")]
223pub struct TextEdit {
224    pub range: Range,
225    pub new_text: String,
226}
227
228/// A workspace edit represents changes to many resources managed in the workspace.
229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
230pub struct WorkspaceEdit {
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub changes: Option<HashMap<String, Vec<TextEdit>>>,
233}
234
235// ---------------------------------------------------------------------------
236// Rename
237// ---------------------------------------------------------------------------
238
239/// Parameters for the `textDocument/rename` request.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct RenameParams {
243    pub text_document: TextDocumentIdentifier,
244    pub position: Position,
245    pub new_name: String,
246}
247
248// ---------------------------------------------------------------------------
249// Formatting
250// ---------------------------------------------------------------------------
251
252/// Value-object describing what options formatting should use.
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(rename_all = "camelCase")]
255pub struct FormattingOptions {
256    pub tab_size: u32,
257    pub insert_spaces: bool,
258}
259
260/// Parameters for the `textDocument/formatting` request.
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct DocumentFormattingParams {
264    pub text_document: TextDocumentIdentifier,
265    pub options: FormattingOptions,
266}
267
268// ---------------------------------------------------------------------------
269// References
270// ---------------------------------------------------------------------------
271
272/// Parameters for the `textDocument/references` request.
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct ReferenceParams {
276    pub text_document: TextDocumentIdentifier,
277    pub position: Position,
278    pub context: ReferenceContext,
279}
280
281/// Context carried with a `textDocument/references` request.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284pub struct ReferenceContext {
285    pub include_declaration: bool,
286}
287
288// ---------------------------------------------------------------------------
289// Initialize
290// ---------------------------------------------------------------------------
291
292/// Parameters sent with the `initialize` request from client to server.
293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub struct InitializeParams {
296    /// The process ID of the parent process that started the server.
297    /// `None` if the process has not been started by another process.
298    pub process_id: Option<u32>,
299    /// The root URI of the workspace.  Preferred over the deprecated `rootPath`.
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub root_uri: Option<String>,
302    /// The capabilities provided by the client (editor or tool).
303    pub capabilities: ClientCapabilities,
304    /// User-provided initialization options.
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub initialization_options: Option<Value>,
307}
308
309/// Capabilities the client (editor) declares to the server.
310///
311/// This is a simplified representation; the full LSP spec is much larger.
312#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct ClientCapabilities {
315    /// Text document specific client capabilities.
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub text_document: Option<Value>,
318    /// Workspace specific client capabilities.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub workspace: Option<Value>,
321}
322
323/// Simplified server capabilities returned in the `initialize` response.
324#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct ServerCapabilities {
327    /// The server provides hover support.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub hover_provider: Option<bool>,
330    /// The server provides goto-definition support.
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub definition_provider: Option<bool>,
333    /// The server provides find-references support.
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub references_provider: Option<bool>,
336    /// The server provides completion support (provider options).
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub completion_provider: Option<Value>,
339    /// The server provides rename support.
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub rename_provider: Option<Value>,
342    /// The server provides document formatting support.
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub document_formatting_provider: Option<bool>,
345    /// How text documents are synced (0 = None, 1 = Full, 2 = Incremental).
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub text_document_sync: Option<Value>,
348}
349
350// ---------------------------------------------------------------------------
351// Tests
352// ---------------------------------------------------------------------------
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use serde_json::json;
358
359    // -- Position, Range, Location round-trips --------------------------------
360
361    #[test]
362    fn test_position_round_trip() {
363        let pos = Position {
364            line: 10,
365            character: 5,
366        };
367        let json = serde_json::to_string(&pos).unwrap();
368        let decoded: Position = serde_json::from_str(&json).unwrap();
369        assert_eq!(pos, decoded);
370    }
371
372    #[test]
373    fn test_position_json_shape() {
374        let pos = Position {
375            line: 3,
376            character: 12,
377        };
378        let v: Value = serde_json::to_value(pos).unwrap();
379        assert_eq!(v, json!({"line": 3, "character": 12}));
380    }
381
382    #[test]
383    fn test_range_round_trip() {
384        let range = Range {
385            start: Position {
386                line: 0,
387                character: 0,
388            },
389            end: Position {
390                line: 0,
391                character: 10,
392            },
393        };
394        let json = serde_json::to_string(&range).unwrap();
395        let decoded: Range = serde_json::from_str(&json).unwrap();
396        assert_eq!(range, decoded);
397    }
398
399    #[test]
400    fn test_location_round_trip() {
401        let loc = Location {
402            uri: "file:///src/main.rs".to_string(),
403            range: Range {
404                start: Position {
405                    line: 5,
406                    character: 0,
407                },
408                end: Position {
409                    line: 5,
410                    character: 20,
411                },
412            },
413        };
414        let json = serde_json::to_string(&loc).unwrap();
415        let decoded: Location = serde_json::from_str(&json).unwrap();
416        assert_eq!(loc, decoded);
417    }
418
419    // -- HoverResult with different HoverContents variants --------------------
420
421    #[test]
422    fn test_hover_result_scalar_string() {
423        let hover = HoverResult {
424            contents: HoverContents::Scalar(MarkedString::String("hello world".to_string())),
425            range: None,
426        };
427        let json = serde_json::to_string(&hover).unwrap();
428        let decoded: HoverResult = serde_json::from_str(&json).unwrap();
429        assert_eq!(hover, decoded);
430    }
431
432    #[test]
433    fn test_hover_result_scalar_language_string() {
434        let hover = HoverResult {
435            contents: HoverContents::Scalar(MarkedString::LanguageString {
436                language: "rust".to_string(),
437                value: "fn main() {}".to_string(),
438            }),
439            range: Some(Range {
440                start: Position {
441                    line: 1,
442                    character: 0,
443                },
444                end: Position {
445                    line: 1,
446                    character: 12,
447                },
448            }),
449        };
450        let json = serde_json::to_string(&hover).unwrap();
451        let decoded: HoverResult = serde_json::from_str(&json).unwrap();
452        assert_eq!(hover, decoded);
453    }
454
455    #[test]
456    fn test_hover_result_markup_content() {
457        let hover = HoverResult {
458            contents: HoverContents::Markup(MarkupContent {
459                kind: "markdown".to_string(),
460                value: "# Hello\nWorld".to_string(),
461            }),
462            range: None,
463        };
464        let json = serde_json::to_string(&hover).unwrap();
465        let decoded: HoverResult = serde_json::from_str(&json).unwrap();
466        assert_eq!(hover, decoded);
467    }
468
469    #[test]
470    fn test_hover_result_array() {
471        let hover = HoverResult {
472            contents: HoverContents::Array(vec![
473                MarkedString::String("first".to_string()),
474                MarkedString::LanguageString {
475                    language: "python".to_string(),
476                    value: "print('hi')".to_string(),
477                },
478            ]),
479            range: None,
480        };
481        let json = serde_json::to_string(&hover).unwrap();
482        let decoded: HoverResult = serde_json::from_str(&json).unwrap();
483        assert_eq!(hover, decoded);
484    }
485
486    // -- CompletionItem with and without optional fields ----------------------
487
488    #[test]
489    fn test_completion_item_full() {
490        let item = CompletionItem {
491            label: "println!".to_string(),
492            kind: Some(3),
493            detail: Some("macro".to_string()),
494            documentation: Some(json!("Prints to stdout.")),
495            insert_text: Some("println!(\"$1\")".to_string()),
496        };
497        let json = serde_json::to_string(&item).unwrap();
498        let decoded: CompletionItem = serde_json::from_str(&json).unwrap();
499        assert_eq!(item, decoded);
500    }
501
502    #[test]
503    fn test_completion_item_minimal() {
504        let item = CompletionItem {
505            label: "my_func".to_string(),
506            kind: None,
507            detail: None,
508            documentation: None,
509            insert_text: None,
510        };
511        let json = serde_json::to_string(&item).unwrap();
512
513        // Ensure optional fields are omitted from the JSON output
514        let v: Value = serde_json::from_str(&json).unwrap();
515        assert!(v.get("kind").is_none());
516        assert!(v.get("detail").is_none());
517        assert!(v.get("documentation").is_none());
518        assert!(v.get("insertText").is_none());
519
520        let decoded: CompletionItem = serde_json::from_str(&json).unwrap();
521        assert_eq!(item, decoded);
522    }
523
524    #[test]
525    fn test_completion_item_camel_case() {
526        let item = CompletionItem {
527            label: "foo".to_string(),
528            kind: Some(1),
529            detail: None,
530            documentation: None,
531            insert_text: Some("foo()".to_string()),
532        };
533        let v: Value = serde_json::to_value(&item).unwrap();
534        // `insert_text` should serialize as `insertText`
535        assert!(v.get("insertText").is_some());
536        assert!(v.get("insert_text").is_none());
537    }
538
539    #[test]
540    fn test_completion_response_array() {
541        let resp = CompletionResponse::Array(vec![CompletionItem {
542            label: "a".to_string(),
543            kind: None,
544            detail: None,
545            documentation: None,
546            insert_text: None,
547        }]);
548        let json = serde_json::to_string(&resp).unwrap();
549        let decoded: CompletionResponse = serde_json::from_str(&json).unwrap();
550        assert_eq!(resp, decoded);
551    }
552
553    #[test]
554    fn test_completion_response_list() {
555        let resp = CompletionResponse::List(CompletionList {
556            is_incomplete: true,
557            items: vec![CompletionItem {
558                label: "b".to_string(),
559                kind: Some(6),
560                detail: None,
561                documentation: None,
562                insert_text: None,
563            }],
564        });
565        let json = serde_json::to_string(&resp).unwrap();
566        let decoded: CompletionResponse = serde_json::from_str(&json).unwrap();
567        assert_eq!(resp, decoded);
568    }
569
570    #[test]
571    fn test_completion_list_camel_case() {
572        let list = CompletionList {
573            is_incomplete: false,
574            items: vec![],
575        };
576        let v: Value = serde_json::to_value(&list).unwrap();
577        assert!(v.get("isIncomplete").is_some());
578        assert!(v.get("is_incomplete").is_none());
579    }
580
581    // -- Diagnostic with severity ---------------------------------------------
582
583    #[test]
584    fn test_diagnostic_round_trip() {
585        let diag = Diagnostic {
586            range: Range {
587                start: Position {
588                    line: 10,
589                    character: 4,
590                },
591                end: Position {
592                    line: 10,
593                    character: 15,
594                },
595            },
596            severity: Some(DiagnosticSeverity::Error),
597            message: "expected `;`".to_string(),
598            source: Some("rustc".to_string()),
599            code: Some(json!("E0308")),
600        };
601        let json = serde_json::to_string(&diag).unwrap();
602        let decoded: Diagnostic = serde_json::from_str(&json).unwrap();
603        assert_eq!(diag, decoded);
604    }
605
606    #[test]
607    fn test_diagnostic_no_optional_fields() {
608        let diag = Diagnostic {
609            range: Range {
610                start: Position {
611                    line: 0,
612                    character: 0,
613                },
614                end: Position {
615                    line: 0,
616                    character: 1,
617                },
618            },
619            severity: None,
620            message: "something".to_string(),
621            source: None,
622            code: None,
623        };
624        let json = serde_json::to_string(&diag).unwrap();
625        let v: Value = serde_json::from_str(&json).unwrap();
626        assert!(v.get("severity").is_none());
627        assert!(v.get("source").is_none());
628        assert!(v.get("code").is_none());
629
630        let decoded: Diagnostic = serde_json::from_str(&json).unwrap();
631        assert_eq!(diag, decoded);
632    }
633
634    // -- DiagnosticSeverity integer serialization -----------------------------
635
636    #[test]
637    fn test_diagnostic_severity_serialize_as_integer() {
638        assert_eq!(
639            serde_json::to_value(DiagnosticSeverity::Error).unwrap(),
640            json!(1)
641        );
642        assert_eq!(
643            serde_json::to_value(DiagnosticSeverity::Warning).unwrap(),
644            json!(2)
645        );
646        assert_eq!(
647            serde_json::to_value(DiagnosticSeverity::Information).unwrap(),
648            json!(3)
649        );
650        assert_eq!(
651            serde_json::to_value(DiagnosticSeverity::Hint).unwrap(),
652            json!(4)
653        );
654    }
655
656    #[test]
657    fn test_diagnostic_severity_deserialize_from_integer() {
658        let err: DiagnosticSeverity = serde_json::from_value(json!(1)).unwrap();
659        assert_eq!(err, DiagnosticSeverity::Error);
660
661        let warn: DiagnosticSeverity = serde_json::from_value(json!(2)).unwrap();
662        assert_eq!(warn, DiagnosticSeverity::Warning);
663
664        let info: DiagnosticSeverity = serde_json::from_value(json!(3)).unwrap();
665        assert_eq!(info, DiagnosticSeverity::Information);
666
667        let hint: DiagnosticSeverity = serde_json::from_value(json!(4)).unwrap();
668        assert_eq!(hint, DiagnosticSeverity::Hint);
669    }
670
671    #[test]
672    fn test_diagnostic_severity_invalid_value() {
673        let result = serde_json::from_value::<DiagnosticSeverity>(json!(0));
674        assert!(result.is_err());
675
676        let result = serde_json::from_value::<DiagnosticSeverity>(json!(5));
677        assert!(result.is_err());
678    }
679
680    // -- TextEdit & WorkspaceEdit ---------------------------------------------
681
682    #[test]
683    fn test_text_edit_round_trip() {
684        let edit = TextEdit {
685            range: Range {
686                start: Position {
687                    line: 2,
688                    character: 0,
689                },
690                end: Position {
691                    line: 2,
692                    character: 5,
693                },
694            },
695            new_text: "hello".to_string(),
696        };
697        let json = serde_json::to_string(&edit).unwrap();
698        let decoded: TextEdit = serde_json::from_str(&json).unwrap();
699        assert_eq!(edit, decoded);
700    }
701
702    #[test]
703    fn test_text_edit_camel_case() {
704        let edit = TextEdit {
705            range: Range {
706                start: Position {
707                    line: 0,
708                    character: 0,
709                },
710                end: Position {
711                    line: 0,
712                    character: 0,
713                },
714            },
715            new_text: "inserted".to_string(),
716        };
717        let v: Value = serde_json::to_value(&edit).unwrap();
718        assert!(v.get("newText").is_some());
719        assert!(v.get("new_text").is_none());
720    }
721
722    #[test]
723    fn test_workspace_edit_with_changes() {
724        let mut changes = HashMap::new();
725        changes.insert(
726            "file:///src/lib.rs".to_string(),
727            vec![TextEdit {
728                range: Range {
729                    start: Position {
730                        line: 0,
731                        character: 0,
732                    },
733                    end: Position {
734                        line: 0,
735                        character: 3,
736                    },
737                },
738                new_text: "pub".to_string(),
739            }],
740        );
741        let ws_edit = WorkspaceEdit {
742            changes: Some(changes),
743        };
744        let json = serde_json::to_string(&ws_edit).unwrap();
745        let decoded: WorkspaceEdit = serde_json::from_str(&json).unwrap();
746        assert_eq!(ws_edit, decoded);
747    }
748
749    #[test]
750    fn test_workspace_edit_empty() {
751        let ws_edit = WorkspaceEdit { changes: None };
752        let json = serde_json::to_string(&ws_edit).unwrap();
753        let v: Value = serde_json::from_str(&json).unwrap();
754        assert!(v.get("changes").is_none());
755
756        let decoded: WorkspaceEdit = serde_json::from_str(&json).unwrap();
757        assert_eq!(ws_edit, decoded);
758    }
759
760    // -- camelCase field naming ------------------------------------------------
761
762    #[test]
763    fn test_text_document_item_camel_case() {
764        let item = TextDocumentItem {
765            uri: "file:///test.rs".to_string(),
766            language_id: "rust".to_string(),
767            version: 1,
768            text: "fn main() {}".to_string(),
769        };
770        let v: Value = serde_json::to_value(&item).unwrap();
771        assert!(v.get("languageId").is_some());
772        assert!(v.get("language_id").is_none());
773    }
774
775    #[test]
776    fn test_text_document_position_params_camel_case() {
777        let params = TextDocumentPositionParams {
778            text_document: TextDocumentIdentifier {
779                uri: "file:///test.rs".to_string(),
780            },
781            position: Position {
782                line: 0,
783                character: 0,
784            },
785        };
786        let v: Value = serde_json::to_value(&params).unwrap();
787        assert!(v.get("textDocument").is_some());
788        assert!(v.get("text_document").is_none());
789    }
790
791    #[test]
792    fn test_rename_params_camel_case() {
793        let params = RenameParams {
794            text_document: TextDocumentIdentifier {
795                uri: "file:///test.rs".to_string(),
796            },
797            position: Position {
798                line: 5,
799                character: 10,
800            },
801            new_name: "bar".to_string(),
802        };
803        let v: Value = serde_json::to_value(&params).unwrap();
804        assert!(v.get("textDocument").is_some());
805        assert!(v.get("newName").is_some());
806        assert!(v.get("text_document").is_none());
807        assert!(v.get("new_name").is_none());
808    }
809
810    #[test]
811    fn test_formatting_options_camel_case() {
812        let opts = FormattingOptions {
813            tab_size: 4,
814            insert_spaces: true,
815        };
816        let v: Value = serde_json::to_value(&opts).unwrap();
817        assert!(v.get("tabSize").is_some());
818        assert!(v.get("insertSpaces").is_some());
819        assert!(v.get("tab_size").is_none());
820        assert!(v.get("insert_spaces").is_none());
821    }
822
823    #[test]
824    fn test_document_formatting_params_camel_case() {
825        let params = DocumentFormattingParams {
826            text_document: TextDocumentIdentifier {
827                uri: "file:///test.rs".to_string(),
828            },
829            options: FormattingOptions {
830                tab_size: 2,
831                insert_spaces: true,
832            },
833        };
834        let v: Value = serde_json::to_value(&params).unwrap();
835        assert!(v.get("textDocument").is_some());
836    }
837
838    #[test]
839    fn test_reference_params_camel_case() {
840        let params = ReferenceParams {
841            text_document: TextDocumentIdentifier {
842                uri: "file:///test.rs".to_string(),
843            },
844            position: Position {
845                line: 3,
846                character: 7,
847            },
848            context: ReferenceContext {
849                include_declaration: true,
850            },
851        };
852        let v: Value = serde_json::to_value(&params).unwrap();
853        assert!(v.get("textDocument").is_some());
854        let ctx = v.get("context").unwrap();
855        assert!(ctx.get("includeDeclaration").is_some());
856        assert!(ctx.get("include_declaration").is_none());
857    }
858
859    #[test]
860    fn test_initialize_params_camel_case() {
861        let params = InitializeParams {
862            process_id: Some(1234),
863            root_uri: Some("file:///workspace".to_string()),
864            capabilities: ClientCapabilities::default(),
865            initialization_options: None,
866        };
867        let v: Value = serde_json::to_value(&params).unwrap();
868        assert!(v.get("processId").is_some());
869        assert!(v.get("rootUri").is_some());
870        assert!(v.get("process_id").is_none());
871        assert!(v.get("root_uri").is_none());
872    }
873
874    #[test]
875    fn test_server_capabilities_camel_case() {
876        let caps = ServerCapabilities {
877            hover_provider: Some(true),
878            definition_provider: Some(true),
879            references_provider: Some(false),
880            completion_provider: Some(json!({"triggerCharacters": [".", ":"]})),
881            rename_provider: Some(json!(true)),
882            document_formatting_provider: Some(true),
883            text_document_sync: Some(json!(2)),
884        };
885        let v: Value = serde_json::to_value(&caps).unwrap();
886        assert!(v.get("hoverProvider").is_some());
887        assert!(v.get("definitionProvider").is_some());
888        assert!(v.get("referencesProvider").is_some());
889        assert!(v.get("completionProvider").is_some());
890        assert!(v.get("renameProvider").is_some());
891        assert!(v.get("documentFormattingProvider").is_some());
892        assert!(v.get("textDocumentSync").is_some());
893        // snake_case keys must not appear
894        assert!(v.get("hover_provider").is_none());
895        assert!(v.get("text_document_sync").is_none());
896    }
897
898    #[test]
899    fn test_server_capabilities_omits_none() {
900        let caps = ServerCapabilities::default();
901        let v: Value = serde_json::to_value(&caps).unwrap();
902        assert!(v.get("hoverProvider").is_none());
903        assert!(v.get("definitionProvider").is_none());
904    }
905
906    // -- PublishDiagnosticsParams round-trip -----------------------------------
907
908    #[test]
909    fn test_publish_diagnostics_round_trip() {
910        let params = PublishDiagnosticsParams {
911            uri: "file:///src/main.rs".to_string(),
912            diagnostics: vec![Diagnostic {
913                range: Range {
914                    start: Position {
915                        line: 1,
916                        character: 0,
917                    },
918                    end: Position {
919                        line: 1,
920                        character: 10,
921                    },
922                },
923                severity: Some(DiagnosticSeverity::Warning),
924                message: "unused variable".to_string(),
925                source: Some("rustc".to_string()),
926                code: Some(json!("W0001")),
927            }],
928        };
929        let json = serde_json::to_string(&params).unwrap();
930        let decoded: PublishDiagnosticsParams = serde_json::from_str(&json).unwrap();
931        assert_eq!(params, decoded);
932    }
933
934    // -- Deserialization from raw JSON (simulating server responses) -----------
935
936    #[test]
937    fn test_deserialize_hover_from_raw_json() {
938        let raw = r##"{
939            "contents": {"kind": "markdown", "value": "# Docs\nSome info"},
940            "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 5}}
941        }"##;
942        let hover: HoverResult = serde_json::from_str(raw).unwrap();
943        assert!(matches!(hover.contents, HoverContents::Markup(_)));
944        assert!(hover.range.is_some());
945    }
946
947    #[test]
948    fn test_deserialize_completion_list_from_raw_json() {
949        let raw = r#"{
950            "isIncomplete": false,
951            "items": [
952                {"label": "println!", "kind": 3, "insertText": "println!(\"$1\")"},
953                {"label": "eprintln!", "kind": 3}
954            ]
955        }"#;
956        let list: CompletionList = serde_json::from_str(raw).unwrap();
957        assert!(!list.is_incomplete);
958        assert_eq!(list.items.len(), 2);
959        assert_eq!(list.items[0].label, "println!");
960        assert!(list.items[0].insert_text.is_some());
961        assert!(list.items[1].insert_text.is_none());
962    }
963
964    #[test]
965    fn test_deserialize_diagnostic_from_raw_json() {
966        let raw = json!({
967            "range": {
968                "start": {"line": 3, "character": 0},
969                "end": {"line": 3, "character": 10}
970            },
971            "severity": 1,
972            "message": "unused variable"
973        });
974        let diag: Diagnostic = serde_json::from_value(raw).unwrap();
975        assert_eq!(diag.severity, Some(DiagnosticSeverity::Error));
976        assert_eq!(diag.message, "unused variable");
977        assert_eq!(diag.range.start.line, 3);
978    }
979
980    #[test]
981    fn test_deserialize_text_edit_from_raw_json() {
982        let raw = json!({
983            "range": {
984                "start": {"line": 0, "character": 0},
985                "end": {"line": 0, "character": 10}
986            },
987            "newText": "fn main() {"
988        });
989        let edit: TextEdit = serde_json::from_value(raw).unwrap();
990        assert_eq!(edit.new_text, "fn main() {");
991        assert_eq!(edit.range.end.character, 10);
992    }
993
994    #[test]
995    fn test_deserialize_workspace_edit_from_raw_json() {
996        let raw = json!({
997            "changes": {
998                "file:///src/main.rs": [
999                    {
1000                        "range": {
1001                            "start": {"line": 10, "character": 4},
1002                            "end": {"line": 10, "character": 7}
1003                        },
1004                        "newText": "new_func"
1005                    }
1006                ]
1007            }
1008        });
1009        let edit: WorkspaceEdit = serde_json::from_value(raw).unwrap();
1010        let changes = edit.changes.unwrap();
1011        assert_eq!(changes.len(), 1);
1012        let edits = changes.get("file:///src/main.rs").unwrap();
1013        assert_eq!(edits.len(), 1);
1014        assert_eq!(edits[0].new_text, "new_func");
1015    }
1016
1017    // -- Additional round-trip tests ------------------------------------------
1018
1019    #[test]
1020    fn test_text_document_item_round_trip() {
1021        let item = TextDocumentItem {
1022            uri: "file:///src/main.rs".to_string(),
1023            language_id: "rust".to_string(),
1024            version: 3,
1025            text: "fn main() {}".to_string(),
1026        };
1027        let json = serde_json::to_string(&item).unwrap();
1028        let decoded: TextDocumentItem = serde_json::from_str(&json).unwrap();
1029        assert_eq!(item, decoded);
1030    }
1031
1032    #[test]
1033    fn test_initialize_params_round_trip() {
1034        let params = InitializeParams {
1035            process_id: Some(42),
1036            root_uri: Some("file:///workspace".to_string()),
1037            capabilities: ClientCapabilities {
1038                text_document: Some(json!({"hover": {"contentFormat": ["plaintext"]}})),
1039                workspace: None,
1040            },
1041            initialization_options: Some(json!({"customSetting": true})),
1042        };
1043        let json = serde_json::to_string(&params).unwrap();
1044        let decoded: InitializeParams = serde_json::from_str(&json).unwrap();
1045        assert_eq!(params, decoded);
1046    }
1047
1048    #[test]
1049    fn test_initialize_params_null_process_id() {
1050        let params = InitializeParams {
1051            process_id: None,
1052            root_uri: None,
1053            capabilities: ClientCapabilities::default(),
1054            initialization_options: None,
1055        };
1056        let v: Value = serde_json::to_value(&params).unwrap();
1057        // processId should be present but null (not omitted).
1058        assert!(v.get("processId").is_some());
1059        assert!(v["processId"].is_null());
1060        // rootUri and initializationOptions should be omitted.
1061        assert!(v.get("rootUri").is_none());
1062        assert!(v.get("initializationOptions").is_none());
1063    }
1064
1065    #[test]
1066    fn test_reference_params_round_trip() {
1067        let params = ReferenceParams {
1068            text_document: TextDocumentIdentifier {
1069                uri: "file:///test.rs".to_string(),
1070            },
1071            position: Position {
1072                line: 10,
1073                character: 3,
1074            },
1075            context: ReferenceContext {
1076                include_declaration: false,
1077            },
1078        };
1079        let json = serde_json::to_string(&params).unwrap();
1080        let decoded: ReferenceParams = serde_json::from_str(&json).unwrap();
1081        assert_eq!(params, decoded);
1082    }
1083
1084    #[test]
1085    fn test_document_formatting_params_round_trip() {
1086        let params = DocumentFormattingParams {
1087            text_document: TextDocumentIdentifier {
1088                uri: "file:///src/lib.rs".to_string(),
1089            },
1090            options: FormattingOptions {
1091                tab_size: 4,
1092                insert_spaces: true,
1093            },
1094        };
1095        let json = serde_json::to_string(&params).unwrap();
1096        let decoded: DocumentFormattingParams = serde_json::from_str(&json).unwrap();
1097        assert_eq!(params, decoded);
1098    }
1099}