ricecoder_external_lsp/merger/
diagnostics.rs

1//! Diagnostics merging
2
3use crate::types::MergeConfig;
4use ricecoder_lsp::types::Diagnostic;
5use std::collections::HashSet;
6
7/// Merges diagnostics from external LSP and internal providers
8pub struct DiagnosticsMerger;
9
10impl DiagnosticsMerger {
11    /// Create a new diagnostics merger
12    pub fn new() -> Self {
13        Self
14    }
15
16    /// Merge diagnostics from external LSP and internal provider
17    ///
18    /// # Arguments
19    ///
20    /// * `external` - Diagnostics from external LSP server (if available)
21    /// * `internal` - Diagnostics from internal provider
22    /// * `config` - Merge configuration
23    ///
24    /// # Returns
25    ///
26    /// Merged and deduplicated diagnostics
27    pub fn merge(
28        external: Option<Vec<Diagnostic>>,
29        internal: Vec<Diagnostic>,
30        config: &MergeConfig,
31    ) -> Vec<Diagnostic> {
32        let mut result = Vec::new();
33
34        // External diagnostics are authoritative - use them if available
35        if let Some(ext) = external {
36            result.extend(ext);
37        } else if config.include_internal {
38            // Only use internal diagnostics if no external available
39            result.extend(internal);
40        }
41
42        // Deduplicate if configured
43        if config.deduplicate {
44            result = Self::deduplicate(result);
45        }
46
47        result
48    }
49
50    /// Deduplicate diagnostics by range and message
51    fn deduplicate(diagnostics: Vec<Diagnostic>) -> Vec<Diagnostic> {
52        let mut seen = HashSet::new();
53        let mut result = Vec::new();
54
55        for diag in diagnostics {
56            // Create a key from range and message
57            let key = format!(
58                "{}:{}:{}:{}:{}",
59                diag.range.start.line,
60                diag.range.start.character,
61                diag.range.end.line,
62                diag.range.end.character,
63                diag.message
64            );
65
66            if !seen.contains(&key) {
67                seen.insert(key);
68                result.push(diag);
69            }
70        }
71
72        result
73    }
74}
75
76impl Default for DiagnosticsMerger {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use ricecoder_lsp::types::{DiagnosticSeverity, Position, Range};
86
87    fn create_diagnostic(line: u32, message: &str) -> Diagnostic {
88        Diagnostic::new(
89            Range::new(Position::new(line, 0), Position::new(line, 5)),
90            DiagnosticSeverity::Error,
91            message.to_string(),
92        )
93    }
94
95    #[test]
96    fn test_merge_external_only() {
97        let external = vec![
98            create_diagnostic(0, "error 1"),
99            create_diagnostic(1, "error 2"),
100        ];
101        let internal = vec![];
102        let config = MergeConfig::default();
103
104        let result = DiagnosticsMerger::merge(Some(external), internal, &config);
105
106        assert_eq!(result.len(), 2);
107        assert_eq!(result[0].message, "error 1");
108        assert_eq!(result[1].message, "error 2");
109    }
110
111    #[test]
112    fn test_merge_internal_only() {
113        let external = None;
114        let internal = vec![
115            create_diagnostic(0, "warning 1"),
116            create_diagnostic(1, "warning 2"),
117        ];
118        let config = MergeConfig::default();
119
120        let result = DiagnosticsMerger::merge(external, internal, &config);
121
122        assert_eq!(result.len(), 2);
123        assert_eq!(result[0].message, "warning 1");
124        assert_eq!(result[1].message, "warning 2");
125    }
126
127    #[test]
128    fn test_merge_external_ignores_internal() {
129        let external = vec![create_diagnostic(0, "external error")];
130        let internal = vec![create_diagnostic(1, "internal warning")];
131        let config = MergeConfig::default();
132
133        let result = DiagnosticsMerger::merge(Some(external), internal, &config);
134
135        assert_eq!(result.len(), 1);
136        assert_eq!(result[0].message, "external error");
137    }
138
139    #[test]
140    fn test_merge_without_internal() {
141        let external = None;
142        let internal = vec![create_diagnostic(0, "warning")];
143        let config = MergeConfig {
144            include_internal: false,
145            deduplicate: true,
146        };
147
148        let result = DiagnosticsMerger::merge(external, internal, &config);
149
150        assert_eq!(result.len(), 0);
151    }
152
153    #[test]
154    fn test_merge_with_deduplication() {
155        let external = vec![
156            create_diagnostic(0, "error 1"),
157            create_diagnostic(0, "error 1"), // Duplicate
158        ];
159        let internal = vec![];
160        let config = MergeConfig {
161            include_internal: true,
162            deduplicate: true,
163        };
164
165        let result = DiagnosticsMerger::merge(Some(external), internal, &config);
166
167        assert_eq!(result.len(), 1);
168        assert_eq!(result[0].message, "error 1");
169    }
170
171    #[test]
172    fn test_merge_without_deduplication() {
173        let external = vec![
174            create_diagnostic(0, "error 1"),
175            create_diagnostic(0, "error 1"), // Duplicate
176        ];
177        let internal = vec![];
178        let config = MergeConfig {
179            include_internal: true,
180            deduplicate: false,
181        };
182
183        let result = DiagnosticsMerger::merge(Some(external), internal, &config);
184
185        assert_eq!(result.len(), 2);
186    }
187
188    #[test]
189    fn test_merge_empty_external() {
190        let external = Some(vec![]);
191        let internal = vec![create_diagnostic(0, "warning")];
192        let config = MergeConfig::default();
193
194        let result = DiagnosticsMerger::merge(external, internal, &config);
195
196        assert_eq!(result.len(), 0);
197    }
198}