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}