Mountain/Environment/DiagnosticProvider.rs
1//! # DiagnosticProvider (Environment)
2//!
3//! Implements the `DiagnosticManager` trait, managing diagnostic information
4//! from multiple sources (language servers, extensions, built-in providers). It
5//! aggregates diagnostics by owner, file URI, and severity, notifying the UI
6//! when changes occur.
7//!
8//! ## RESPONSIBILITIES
9//!
10//! ### 1. Diagnostic Collection
11//! - Maintain collections of diagnostics organized by owner (TypeScript, Rust,
12//! ESLint)
13//! - Store diagnostics per resource URI for efficient lookup
14//! - Support multiple severity levels (Error, Warning, Info, Hint)
15//! - Track diagnostic source and code for quick fixes
16//!
17//! ### 2. Diagnostic Aggregation
18//! - Combine diagnostics from multiple sources into unified view
19//! - Merge diagnostics for same location from different owners
20//! - Sort diagnostics by severity and position
21//! - De-duplicate identical diagnostics
22//!
23//! ### 3. Change Notification
24//! - Emit events to UI (Sky) when diagnostics change
25//! - Identify changed URIs efficiently for incremental updates
26//! - Format diagnostic collections for IPC transmission
27//! - Support diagnostic refresh requests
28//!
29//! ### 4. Owner Management
30//! - Allow independent language servers to manage their diagnostics
31//! - Support adding/removing diagnostic owners
32//! - Prevent interference between different diagnostic sources
33//! - Track owner metadata (name, version, etc.)
34//!
35//! ### 5. Diagnostic Lifecycle
36//! - `SetDiagnostics(owner, uri, entries)`: Set diagnostics for owner+URI
37//! - `ClearDiagnostics(owner, uri)`: Remove diagnostics
38//! - `RemoveOwner(owner)`: Remove all diagnostics from an owner
39//! - `GetDiagnostics(uri)`: Retrieve all diagnostics for a URI
40//!
41//! ## ARCHITECTURAL ROLE
42//!
43//! DiagnosticProvider is the **diagnostic aggregation hub**:
44//!
45//! ```text
46//! Language Server ──► SetDiagnostics ──► DiagnosticProvider ──► UI Event ──► Sky
47//! Extension ──► SetDiagnostics ──► DiagnosticProvider ──► UI Event ──► Sky
48//! ```
49//!
50//! ### Position in Mountain
51//! - `Environment` module: Error and diagnostic management
52//! - Implements `CommonLibrary::Diagnostic::DiagnosticManager` trait
53//! - Accessible via `Environment.Require<dyn DiagnosticManager>()`
54//!
55//! ### Data Storage
56//! - `ApplicationState.Feature.Diagnostics`: HashMap<String, HashMap<String,
57//! `Vec<MarkerDataDTO>`>>
58//! - Outer key: Owner (e.g., "typescript", "rust-analyzer")
59//! - Inner key: URI string
60//! - Value: Vector of diagnostic markers
61//!
62//! ### Dependencies
63//! - `ApplicationState`: Diagnostic storage
64//! - `Log`: Diagnostic change logging
65//! - `IPCProvider`: To emit diagnostic change events
66//!
67//! ### Dependents
68//! - Language servers: Report diagnostics via provider
69//! - `DispatchLogic`: Route diagnostic-related commands
70//! - UI components: Display diagnostics in editor
71//!
72//! ## DIAGNOSTIC DATA MODEL
73//!
74//! Each diagnostic is a `MarkerDataDTO`:
75//! - `Severity`: Error(8), Warning(4), Information(2), Hint(1)
76//! - `Message`: Human-readable description
77//! - `StartLineNumber`/`StartColumn`: Start position (0-based)
78//! - `EndLineNumber`/`EndColumn`: End position
79//! - `Source`: Diagnostic source string (e.g., "tslint")
80//! - `Code`: Diagnostic code for quick fix lookup
81//! - `ModelVersionIdentifier`: Document version for tracking
82//!
83//! ## NOTIFICATION FLOW
84//!
85//! 1. Language server calls `SetDiagnostics(owner, uri, entries)`
86//! 2. Provider validates and stores in `ApplicationState.Feature.Diagnostics`
87//! 3. Provider identifies which URIs changed in this update
88//! 4. Provider emits `sky://diagnostics/changed` event with:
89//! - `owner`: Diagnostic source
90//! - `uris`: List of changed file URIs
91//! 5. Sky receives event and requests updated diagnostics for those URIs
92//! 6. Sky updates UI (squiggles, Problems panel, etc.)
93//!
94//! ## ERROR HANDLING
95//!
96//! - Invalid owner/uri: Logged but operation continues
97//! - Empty diagnostic list: Treated as "clear" operation
98//! - Serialization errors: Logged and skipped
99//! - State lock errors: `CommonError::StateLockPoisoned`
100//!
101//! ## PERFORMANCE
102//!
103//! - Diagnostic storage uses nested HashMaps for O(1) lookup
104//! - Change detection compares old vs new URI sets
105//! - Events are debounced to prevent spam (configurable)
106//! - Large diagnostic sets may impact UI responsiveness (consider paging)
107//!
108//! ## VS CODE REFERENCE
109//!
110//! Patterns from VS Code:
111//! - `vs/workbench/services/diagnostic/common/diagnosticCollection.ts` -
112//! Collection management
113//! - `vs/platform/diagnostics/common/diagnostics.ts` - Diagnostic data model
114//! - `vs/workbench/services/diagnostic/common/diagnosticService.ts` -
115//! Aggregation and events
116//!
117//! ## TODO
118//!
119//! - [ ] Implement diagnostic severity filtering (hide certain levels)
120//! - [ ] Add diagnostic code actions/quick fixes integration
121//! - [ ] Support diagnostic inline messages and hover
122//! - [ ] Implement diagnostic history and undo/redo
123//! - [ ] Add diagnostic export (to file, clipboard)
124//! - [ ] Support diagnostic linting and rule configuration
125//! - [ ] Implement diagnostic suppression comments
126//! - [ ] Add diagnostic telemetry (frequency, severity distribution)
127//! - [ ] Support remote diagnostics (from cloud services)
128//! - [ ] Implement diagnostic caching for offline scenarios
129//!
130//! ## MODULE CONTENTS
131//!
132//! - `DiagnosticProvider`: Main struct implementing `DiagnosticManager`
133//! - Diagnostic storage and retrieval methods
134//! - Change notification and event emission
135//! - Owner management functions
136//! - Diagnostic validation helpers
137
138// 1. **Diagnostic Collection**: Maintains collections of diagnostics organized
139// by owner (e.g., TypeScript, Rust, ESLint) and resource URI.
140//
141// 2. **Diagnostic Aggregation**: Combines diagnostics from multiple sources
142// into a unified view for the user interface.
143//
144// 3. **Change Notification**: Emits events to the UI (Sky) when diagnostics
145// change, enabling real-time feedback.
146//
147// 4. **Owner Management**: Allows independent language servers and tools to
148// manage their own diagnostic collections without interference.
149//
150// 5. **Diagnostic Lifecycle**: Handles setting, updating, and clearing
151// diagnostics for specific resources or entire owner collections.
152//
153// # Diagnostic Data Model
154//
155// Diagnostics are stored in ApplicationState.Feature.Diagnostics as:
156// - Outer map: Owner (String) -> Inner map
157// - Inner map: URI String -> Vector of MarkerDataDTO
158// - Each MarkerDataDTO represents a single diagnostic with severity, message,
159// range, etc.
160//
161// # Notification Flow
162//
163// 1. Language server or extension calls SetDiagnostics(owner, entries)
164// 2. Mountain validates and stores diagnostics in ApplicationState
165// 3. Mountain identifies changed URIs in this update
166// 4. Mountain emits "sky://diagnostics/changed" event with owner and changed
167// URIs
168// 5. UI (Sky) receives event and updates diagnostic display
169//
170// # Patterns Borrowed from VSCode
171//
172// - **Diagnostic Collections**: Inspired by VSCode's DiagnosticCollection
173// pattern where each language service manages its own collection.
174//
175// - **Owner Model**: Similar to VSCode's owner concept for distinguishing
176// diagnostic sources (e.g., cs, tslint, eslint).
177//
178// - **Batch Updates**: Like VSCode, supports setting multiple diagnostics at
179// once for efficiency.
180//
181// # TODOs
182//
183// - [ ] Implement diagnostic severity filtering
184// - [ ] Add diagnostic code and code description support
185// - - [ ] Implement related information support
186// - [ ] Add diagnostic tags (deprecated, unnecessary)
187// - [ ] Implement diagnostic source tracking
188// - [ ] Add support for diagnostic suppression comments
189// - [ ] Implement diagnostic cleanup for closed resources
190// - [ ] Add diagnostic statistics and metrics
191// - [ ] Consider implementing diagnostic versioning for change detection
192// - [ ] Add support for diagnostic workspace-wide filtering (exclude files)
193
194use CommonLibrary::{Diagnostic::DiagnosticManager::DiagnosticManager, Error::CommonError::CommonError};
195use async_trait::async_trait;
196use log::{debug, error, info};
197use serde_json::{Value, json};
198use tauri::Emitter;
199
200use super::{MountainEnvironment::MountainEnvironment, Utility};
201use crate::ApplicationState::DTO::MarkerDataDTO::MarkerDataDTO;
202
203#[async_trait]
204impl DiagnosticManager for MountainEnvironment {
205 /// Sets or updates diagnostics for multiple resources from a specific
206 /// owner. Empty marker arrays are treated as clearing diagnostics for that
207 /// URI.
208 async fn SetDiagnostics(&self, Owner:String, EntriesDTOValue:Value) -> Result<(), CommonError> {
209 info!("[DiagnosticProvider] Setting diagnostics for owner: {}", Owner);
210
211 let DeserializedEntries:Vec<(Value, Option<Vec<MarkerDataDTO>>)> = serde_json::from_value(EntriesDTOValue)
212 .map_err(|Error| {
213 CommonError::InvalidArgument {
214 ArgumentName:"EntriesDTOValue".to_string(),
215 Reason:format!("Failed to deserialize diagnostic entries: {}", Error),
216 }
217 })?;
218
219 let mut DiagnosticsMapGuard = self
220 .ApplicationState
221 .Feature
222 .Diagnostics
223 .DiagnosticsMap
224 .lock()
225 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
226
227 let OwnerMap = DiagnosticsMapGuard.entry(Owner.clone()).or_default();
228
229 let mut ChangedURIKeys = Vec::new();
230
231 for (URIComponentsValue, MarkersOption) in DeserializedEntries {
232 let URIKey = Utility::GetURLFromURIComponentsDTO(&URIComponentsValue)?.to_string();
233
234 ChangedURIKeys.push(URIKey.clone());
235
236 if let Some(Markers) = MarkersOption {
237 if Markers.is_empty() {
238 OwnerMap.remove(&URIKey);
239 } else {
240 OwnerMap.insert(URIKey, Markers);
241 }
242 } else {
243 OwnerMap.remove(&URIKey);
244 }
245 }
246
247 drop(DiagnosticsMapGuard);
248
249 // Notify the frontend that diagnostics have changed for specific URIs.
250 // Include both added/cleared URIs so UI can update accurately.
251 let EventPayload = json!({ "Owner": Owner, "Uris": ChangedURIKeys });
252
253 if let Err(Error) = self.ApplicationHandle.emit("sky://diagnostics/changed", EventPayload) {
254 error!("[DiagnosticProvider] Failed to emit 'diagnostics_changed': {}", Error);
255 }
256
257 info!(
258 "[DiagnosticProvider] Emitted diagnostics changed for {} URI(s)",
259 ChangedURIKeys.len()
260 );
261
262 Ok(())
263 }
264
265 /// Clears all diagnostics from a specific owner.
266 async fn ClearDiagnostics(&self, Owner:String) -> Result<(), CommonError> {
267 info!("[DiagnosticProvider] Clearing all diagnostics for owner: {}", Owner);
268
269 let (ClearedCount, ChangedURIKeys):(usize, Vec<String>) = {
270 let mut DiagnosticsMapGuard = self
271 .ApplicationState
272 .Feature
273 .Diagnostics
274 .DiagnosticsMap
275 .lock()
276 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
277
278 DiagnosticsMapGuard
279 .remove(&Owner)
280 .map(|OwnerMap| {
281 let keys:Vec<String> = OwnerMap.keys().cloned().collect();
282 (keys.len(), keys)
283 })
284 .unwrap_or((0, vec![]))
285 };
286
287 if !ChangedURIKeys.is_empty() {
288 info!(
289 "[DiagnosticProvider] Cleared {} diagnostics across {} URI(s)",
290 ClearedCount,
291 ChangedURIKeys.len()
292 );
293
294 let EventPayload = json!({ "Owner": Owner, "Uris": ChangedURIKeys });
295
296 if let Err(Error) = self.ApplicationHandle.emit("sky://diagnostics/changed", EventPayload) {
297 error!("[DiagnosticProvider] Failed to emit 'diagnostics_changed' on clear: {}", Error);
298 }
299 }
300
301 Ok(())
302 }
303
304 /// Retrieves all diagnostics, optionally filtered by a resource URI.
305 /// Returns diagnostics aggregated from all owners for the specified
306 /// resource(s).
307 async fn GetAllDiagnostics(&self, ResourceURIFilterOption:Option<Value>) -> Result<Value, CommonError> {
308 debug!(
309 "[DiagnosticProvider] Getting all diagnostics with filter: {:?}",
310 ResourceURIFilterOption
311 );
312
313 let DiagnosticsMapGuard = self
314 .ApplicationState
315 .Feature
316 .Diagnostics
317 .DiagnosticsMap
318 .lock()
319 .map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
320
321 let mut ResultMap:std::collections::HashMap<String, Vec<MarkerDataDTO>> = std::collections::HashMap::new();
322
323 if let Some(FilterURIValue) = ResourceURIFilterOption {
324 let FilterURIKey = Utility::GetURLFromURIComponentsDTO(&FilterURIValue)?.to_string();
325
326 for OwnerMap in DiagnosticsMapGuard.values() {
327 if let Some(Markers) = OwnerMap.get(&FilterURIKey) {
328 ResultMap.entry(FilterURIKey.clone()).or_default().extend(Markers.clone());
329 }
330 }
331 } else {
332 // Aggregate all diagnostics from all owners for all files.
333 for OwnerMap in DiagnosticsMapGuard.values() {
334 for (URIKey, Markers) in OwnerMap.iter() {
335 ResultMap.entry(URIKey.clone()).or_default().extend(Markers.clone());
336 }
337 }
338 }
339
340 let ResultList:Vec<(String, Vec<MarkerDataDTO>)> = ResultMap.into_iter().collect();
341
342 debug!("[DiagnosticProvider] Returning {} diagnostic collection(s)", ResultList.len());
343
344 serde_json::to_value(ResultList).map_err(|Error| CommonError::from(Error))
345 }
346}