Skip to main content

rez_lsp_server/server/
diagnostics.rs

1//! Diagnostic management for the LSP server.
2
3use crate::core::Result;
4use crate::validation::{Severity as ValidationSeverity, ValidationEngine, ValidationResult};
5use std::collections::HashMap;
6use std::sync::Arc;
7use tokio::sync::RwLock;
8use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range, Url};
9
10/// Manages diagnostics for the LSP server.
11pub struct DiagnosticsManager {
12    /// Validation engine for checking package.py files
13    validation_engine: Arc<ValidationEngine>,
14    /// Current diagnostics for each file
15    diagnostics: Arc<RwLock<HashMap<Url, Vec<Diagnostic>>>>,
16}
17
18impl DiagnosticsManager {
19    /// Create a new diagnostics manager.
20    pub fn new() -> Result<Self> {
21        let validation_engine = Arc::new(ValidationEngine::new()?);
22        let diagnostics = Arc::new(RwLock::new(HashMap::new()));
23
24        Ok(Self {
25            validation_engine,
26            diagnostics,
27        })
28    }
29
30    /// Validate a file and update diagnostics.
31    pub async fn validate_file(&self, uri: &Url, content: &str) -> Result<Vec<Diagnostic>> {
32        let file_path = uri.path();
33
34        // Run validation
35        let validation_result = self.validation_engine.validate_file(content, file_path)?;
36
37        // Convert validation issues to LSP diagnostics
38        let diagnostics = self.convert_validation_result(&validation_result);
39
40        // Store diagnostics
41        {
42            let mut diag_map = self.diagnostics.write().await;
43            diag_map.insert(uri.clone(), diagnostics.clone());
44        }
45
46        Ok(diagnostics)
47    }
48
49    /// Get current diagnostics for a file.
50    pub async fn get_diagnostics(&self, uri: &Url) -> Vec<Diagnostic> {
51        let diag_map = self.diagnostics.read().await;
52        diag_map.get(uri).cloned().unwrap_or_default()
53    }
54
55    /// Clear diagnostics for a file.
56    pub async fn clear_diagnostics(&self, uri: &Url) {
57        let mut diag_map = self.diagnostics.write().await;
58        diag_map.remove(uri);
59    }
60
61    /// Get all files with diagnostics.
62    pub async fn get_all_diagnostics(&self) -> HashMap<Url, Vec<Diagnostic>> {
63        let diag_map = self.diagnostics.read().await;
64        diag_map.clone()
65    }
66
67    /// Convert validation result to LSP diagnostics.
68    fn convert_validation_result(&self, result: &ValidationResult) -> Vec<Diagnostic> {
69        result
70            .issues
71            .iter()
72            .map(|issue| {
73                let severity = match issue.severity {
74                    ValidationSeverity::Critical => DiagnosticSeverity::ERROR,
75                    ValidationSeverity::Error => DiagnosticSeverity::ERROR,
76                    ValidationSeverity::Warning => DiagnosticSeverity::WARNING,
77                    ValidationSeverity::Info => DiagnosticSeverity::INFORMATION,
78                };
79
80                let start_pos = Position {
81                    line: issue.line - 1,        // Convert to 0-based
82                    character: issue.column - 1, // Convert to 0-based
83                };
84
85                let end_pos = Position {
86                    line: issue.line - 1,
87                    character: issue.column - 1 + issue.length,
88                };
89
90                let range = Range {
91                    start: start_pos,
92                    end: end_pos,
93                };
94
95                let mut diagnostic = Diagnostic {
96                    range,
97                    severity: Some(severity),
98                    code: Some(NumberOrString::String(issue.code.clone())),
99                    code_description: None,
100                    source: Some("rez-lsp".to_string()),
101                    message: issue.message.clone(),
102                    related_information: None,
103                    tags: None,
104                    data: None,
105                };
106
107                // Add suggestion as related information if available
108                if let Some(suggestion) = &issue.suggestion {
109                    diagnostic.message =
110                        format!("{}\nSuggestion: {}", diagnostic.message, suggestion);
111                }
112
113                diagnostic
114            })
115            .collect()
116    }
117
118    /// Get validation statistics for all files.
119    pub async fn get_validation_stats(&self) -> ValidationStats {
120        let diag_map = self.diagnostics.read().await;
121
122        let mut total_files = 0;
123        let mut files_with_errors = 0;
124        let mut files_with_warnings = 0;
125        let mut total_diagnostics = 0;
126        let mut total_errors = 0;
127        let mut total_warnings = 0;
128        let mut total_info = 0;
129
130        for diagnostics in diag_map.values() {
131            total_files += 1;
132            total_diagnostics += diagnostics.len();
133
134            let mut has_errors = false;
135            let mut has_warnings = false;
136
137            for diagnostic in diagnostics {
138                match diagnostic.severity {
139                    Some(DiagnosticSeverity::ERROR) => {
140                        total_errors += 1;
141                        has_errors = true;
142                    }
143                    Some(DiagnosticSeverity::WARNING) => {
144                        total_warnings += 1;
145                        has_warnings = true;
146                    }
147                    Some(DiagnosticSeverity::INFORMATION) => {
148                        total_info += 1;
149                    }
150                    _ => {}
151                }
152            }
153
154            if has_errors {
155                files_with_errors += 1;
156            } else if has_warnings {
157                files_with_warnings += 1;
158            }
159        }
160
161        ValidationStats {
162            total_files,
163            files_with_errors,
164            files_with_warnings,
165            total_diagnostics,
166            total_errors,
167            total_warnings,
168            total_info,
169        }
170    }
171}
172
173/// Statistics about validation across all files.
174#[derive(Debug, Clone)]
175pub struct ValidationStats {
176    /// Total number of files with diagnostics
177    pub total_files: usize,
178    /// Number of files with errors
179    pub files_with_errors: usize,
180    /// Number of files with warnings (but no errors)
181    pub files_with_warnings: usize,
182    /// Total number of diagnostics
183    pub total_diagnostics: usize,
184    /// Total number of errors
185    pub total_errors: usize,
186    /// Total number of warnings
187    pub total_warnings: usize,
188    /// Total number of info messages
189    pub total_info: usize,
190}
191
192impl ValidationStats {
193    /// Check if there are any errors.
194    pub fn has_errors(&self) -> bool {
195        self.total_errors > 0
196    }
197
198    /// Check if there are any diagnostics.
199    pub fn has_diagnostics(&self) -> bool {
200        self.total_diagnostics > 0
201    }
202
203    /// Get the percentage of files with issues.
204    pub fn error_rate(&self) -> f64 {
205        if self.total_files > 0 {
206            (self.files_with_errors as f64 / self.total_files as f64) * 100.0
207        } else {
208            0.0
209        }
210    }
211}