Skip to main content

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 serde_json::{Value, json};
197use tauri::Emitter;
198
199use super::{MountainEnvironment::MountainEnvironment, Utility};
200use crate::{ApplicationState::DTO::MarkerDataDTO::MarkerDataDTO, dev_log};
201
202#[async_trait]
203impl DiagnosticManager for MountainEnvironment {
204	/// Sets or updates diagnostics for multiple resources from a specific
205	/// owner. Empty marker arrays are treated as clearing diagnostics for that
206	/// URI.
207	async fn SetDiagnostics(&self, Owner:String, EntriesDTOValue:Value) -> Result<(), CommonError> {
208		dev_log!("extensions", "[DiagnosticProvider] Setting diagnostics for owner: {}", Owner);
209
210		let DeserializedEntries:Vec<(Value, Option<Vec<MarkerDataDTO>>)> = serde_json::from_value(EntriesDTOValue)
211			.map_err(|Error| {
212				CommonError::InvalidArgument {
213					ArgumentName:"EntriesDTOValue".to_string(),
214					Reason:format!("Failed to deserialize diagnostic entries: {}", Error),
215				}
216			})?;
217
218		let mut DiagnosticsMapGuard = self
219			.ApplicationState
220			.Feature
221			.Diagnostics
222			.DiagnosticsMap
223			.lock()
224			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
225
226		let OwnerMap = DiagnosticsMapGuard.entry(Owner.clone()).or_default();
227
228		let mut ChangedURIKeys = Vec::new();
229
230		for (URIComponentsValue, MarkersOption) in DeserializedEntries {
231			let URIKey = Utility::GetURLFromURIComponentsDTO(&URIComponentsValue)?.to_string();
232
233			ChangedURIKeys.push(URIKey.clone());
234
235			if let Some(Markers) = MarkersOption {
236				if Markers.is_empty() {
237					OwnerMap.remove(&URIKey);
238				} else {
239					OwnerMap.insert(URIKey, Markers);
240				}
241			} else {
242				OwnerMap.remove(&URIKey);
243			}
244		}
245
246		drop(DiagnosticsMapGuard);
247
248		// Notify the frontend that diagnostics have changed for specific URIs.
249		// Include both added/cleared URIs so UI can update accurately.
250		let EventPayload = json!({ "Owner": Owner, "Uris": ChangedURIKeys });
251
252		if let Err(Error) = self.ApplicationHandle.emit("sky://diagnostics/changed", EventPayload) {
253			dev_log!(
254				"extensions",
255				"error: [DiagnosticProvider] Failed to emit 'diagnostics_changed': {}",
256				Error
257			);
258		}
259
260		dev_log!(
261			"extensions",
262			"[DiagnosticProvider] Emitted diagnostics changed for {} URI(s)",
263			ChangedURIKeys.len()
264		);
265
266		Ok(())
267	}
268
269	/// Clears all diagnostics from a specific owner.
270	async fn ClearDiagnostics(&self, Owner:String) -> Result<(), CommonError> {
271		dev_log!(
272			"extensions",
273			"[DiagnosticProvider] Clearing all diagnostics for owner: {}",
274			Owner
275		);
276
277		let (ClearedCount, ChangedURIKeys):(usize, Vec<String>) = {
278			let mut DiagnosticsMapGuard = self
279				.ApplicationState
280				.Feature
281				.Diagnostics
282				.DiagnosticsMap
283				.lock()
284				.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
285
286			DiagnosticsMapGuard
287				.remove(&Owner)
288				.map(|OwnerMap| {
289					let keys:Vec<String> = OwnerMap.keys().cloned().collect();
290					(keys.len(), keys)
291				})
292				.unwrap_or((0, vec![]))
293		};
294
295		if !ChangedURIKeys.is_empty() {
296			dev_log!(
297				"extensions",
298				"[DiagnosticProvider] Cleared {} diagnostics across {} URI(s)",
299				ClearedCount,
300				ChangedURIKeys.len()
301			);
302
303			let EventPayload = json!({ "Owner": Owner, "Uris": ChangedURIKeys });
304
305			if let Err(Error) = self.ApplicationHandle.emit("sky://diagnostics/changed", EventPayload) {
306				dev_log!(
307					"extensions",
308					"error: [DiagnosticProvider] Failed to emit 'diagnostics_changed' on clear: {}",
309					Error
310				);
311			}
312		}
313
314		Ok(())
315	}
316
317	/// Retrieves all diagnostics, optionally filtered by a resource URI.
318	/// Returns diagnostics aggregated from all owners for the specified
319	/// resource(s).
320	async fn GetAllDiagnostics(&self, ResourceURIFilterOption:Option<Value>) -> Result<Value, CommonError> {
321		dev_log!(
322			"extensions",
323			"[DiagnosticProvider] Getting all diagnostics with filter: {:?}",
324			ResourceURIFilterOption
325		);
326
327		let DiagnosticsMapGuard = self
328			.ApplicationState
329			.Feature
330			.Diagnostics
331			.DiagnosticsMap
332			.lock()
333			.map_err(Utility::MapApplicationStateLockErrorToCommonError)?;
334
335		let mut ResultMap:std::collections::HashMap<String, Vec<MarkerDataDTO>> = std::collections::HashMap::new();
336
337		if let Some(FilterURIValue) = ResourceURIFilterOption {
338			let FilterURIKey = Utility::GetURLFromURIComponentsDTO(&FilterURIValue)?.to_string();
339
340			for OwnerMap in DiagnosticsMapGuard.values() {
341				if let Some(Markers) = OwnerMap.get(&FilterURIKey) {
342					ResultMap.entry(FilterURIKey.clone()).or_default().extend(Markers.clone());
343				}
344			}
345		} else {
346			// Aggregate all diagnostics from all owners for all files.
347			for OwnerMap in DiagnosticsMapGuard.values() {
348				for (URIKey, Markers) in OwnerMap.iter() {
349					ResultMap.entry(URIKey.clone()).or_default().extend(Markers.clone());
350				}
351			}
352		}
353
354		let ResultList:Vec<(String, Vec<MarkerDataDTO>)> = ResultMap.into_iter().collect();
355
356		dev_log!(
357			"extensions",
358			"[DiagnosticProvider] Returning {} diagnostic collection(s)",
359			ResultList.len()
360		);
361
362		serde_json::to_value(ResultList).map_err(|Error| CommonError::from(Error))
363	}
364}