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 (1-based, matches
78//! workbench `IMarkerData` - Cocoon's `LanguagesNamespace.ts`
79//! `NormaliseDiagnostic` adds the `+ 1` from vscode.Position 0-based before
80//! sending to Mountain)
81//! - `EndLineNumber`/`EndColumn`: End position (1-based, same convention)
82//! - `Source`: Diagnostic source string (e.g., "tslint")
83//! - `Code`: Diagnostic code for quick fix lookup
84//! - `ModelVersionIdentifier`: Document version for tracking
85//!
86//! ## NOTIFICATION FLOW
87//!
88//! 1. Language server calls `SetDiagnostics(owner, uri, entries)`
89//! 2. Provider validates and stores in `ApplicationState.Feature.Diagnostics`
90//! 3. Provider identifies which URIs changed in this update
91//! 4. Provider emits `sky://diagnostics/changed` event with:
92//! - `owner`: Diagnostic source
93//! - `uris`: List of changed file URIs
94//! 5. Sky receives event and requests updated diagnostics for those URIs
95//! 6. Sky updates UI (squiggles, Problems panel, etc.)
96//!
97//! ## ERROR HANDLING
98//!
99//! - Invalid owner/uri: Logged but operation continues
100//! - Empty diagnostic list: Treated as "clear" operation
101//! - Serialization errors: Logged and skipped
102//! - State lock errors: `CommonError::StateLockPoisoned`
103//!
104//! ## PERFORMANCE
105//!
106//! - Diagnostic storage uses nested HashMaps for O(1) lookup
107//! - Change detection compares old vs new URI sets
108//! - Events are debounced to prevent spam (configurable)
109//! - Large diagnostic sets may impact UI responsiveness (consider paging)
110//!
111//! ## VS CODE REFERENCE
112//!
113//! Patterns from VS Code:
114//! - `vs/workbench/services/diagnostic/common/diagnosticCollection.ts` -
115//! Collection management
116//! - `vs/platform/diagnostics/common/diagnostics.ts` - Diagnostic data model
117//! - `vs/workbench/services/diagnostic/common/diagnosticService.ts` -
118//! Aggregation and events
119//!
120//! ## TODO
121//!
122//! - [ ] Implement diagnostic severity filtering (hide certain levels)
123//! - [ ] Add diagnostic code actions/quick fixes integration
124//! - [ ] Support diagnostic inline messages and hover
125//! - [ ] Implement diagnostic history and undo/redo
126//! - [ ] Add diagnostic export (to file, clipboard)
127//! - [ ] Support diagnostic linting and rule configuration
128//! - [ ] Implement diagnostic suppression comments
129//! - [ ] Add diagnostic telemetry (frequency, severity distribution)
130//! - [ ] Support remote diagnostics (from cloud services)
131//! - [ ] Implement diagnostic caching for offline scenarios
132//!
133//! ## MODULE CONTENTS
134//!
135//! - `DiagnosticProvider`: Main struct implementing `DiagnosticManager`
136//! - Diagnostic storage and retrieval methods
137//! - Change notification and event emission
138//! - Owner management functions
139//! - Diagnostic validation helpers
140
141// 1. **Diagnostic Collection**: Maintains collections of diagnostics organized
142// by owner (e.g., TypeScript, Rust, ESLint) and resource URI.
143//
144// 2. **Diagnostic Aggregation**: Combines diagnostics from multiple sources
145// into a unified view for the user interface.
146//
147// 3. **Change Notification**: Emits events to the UI (Sky) when diagnostics
148// change, enabling real-time feedback.
149//
150// 4. **Owner Management**: Allows independent language servers and tools to
151// manage their own diagnostic collections without interference.
152//
153// 5. **Diagnostic Lifecycle**: Handles setting, updating, and clearing
154// diagnostics for specific resources or entire owner collections.
155//
156// # Diagnostic Data Model
157//
158// Diagnostics are stored in ApplicationState.Feature.Diagnostics as:
159// - Outer map: Owner (String) -> Inner map
160// - Inner map: URI String -> Vector of MarkerDataDTO
161// - Each MarkerDataDTO represents a single diagnostic with severity, message,
162// range, etc.
163//
164// # Notification Flow
165//
166// 1. Language server or extension calls SetDiagnostics(owner, entries)
167// 2. Mountain validates and stores diagnostics in ApplicationState
168// 3. Mountain identifies changed URIs in this update
169// 4. Mountain emits "sky://diagnostics/changed" event with owner and changed
170// URIs
171// 5. UI (Sky) receives event and updates diagnostic display
172//
173// # Patterns Borrowed from VSCode
174//
175// - **Diagnostic Collections**: Inspired by VSCode's DiagnosticCollection
176// pattern where each language service manages its own collection.
177//
178// - **Owner Model**: Similar to VSCode's owner concept for distinguishing
179// diagnostic sources (e.g., cs, tslint, eslint).
180//
181// - **Batch Updates**: Like VSCode, supports setting multiple diagnostics at
182// once for efficiency.
183//
184// # TODOs
185//
186// - [ ] Implement diagnostic severity filtering
187// - [ ] Add diagnostic code and code description support
188// - - [ ] Implement related information support
189// - [ ] Add diagnostic tags (deprecated, unnecessary)
190// - [ ] Implement diagnostic source tracking
191// - [ ] Add support for diagnostic suppression comments
192// - [ ] Implement diagnostic cleanup for closed resources
193// - [ ] Add diagnostic statistics and metrics
194// - [ ] Consider implementing diagnostic versioning for change detection
195// - [ ] Add support for diagnostic workspace-wide filtering (exclude files)
196
197use CommonLibrary::{
198 Diagnostic::DiagnosticManager::DiagnosticManager,
199 Error::CommonError::CommonError,
200 IPC::SkyEvent::SkyEvent,
201};
202use async_trait::async_trait;
203use serde_json::{Value, json};
204
205// `tauri::Emitter` is no longer used directly here - all emits
206// route through `LogSkyEmit` which carries the trait import. The
207// import was previously here for the direct `.emit()` calls now
208// replaced. Removed to keep the file warning-clean.
209use super::{MountainEnvironment::MountainEnvironment, Utility};
210use crate::{ApplicationState::DTO::MarkerDataDTO::MarkerDataDTO, IPC::SkyEmit::LogSkyEmit, dev_log};
211
212#[async_trait]
213impl DiagnosticManager for MountainEnvironment {
214 /// Sets or updates diagnostics for multiple resources from a specific
215 /// owner. Empty marker arrays are treated as clearing diagnostics for that
216 /// URI.
217 async fn SetDiagnostics(&self, Owner:String, EntriesDTOValue:Value) -> Result<(), CommonError> {
218 dev_log!("extensions", "[DiagnosticProvider] Setting diagnostics for owner: {}", Owner);
219
220 let DeserializedEntries:Vec<(Value, Option<Vec<MarkerDataDTO>>)> = serde_json::from_value(EntriesDTOValue)
221 .map_err(|Error| {
222 CommonError::InvalidArgument {
223 ArgumentName:"EntriesDTOValue".to_string(),
224 Reason:format!("Failed to deserialize diagnostic entries: {}", Error),
225 }
226 })?;
227
228 let mut DiagnosticsMapGuard = self
229 .ApplicationState
230 .Feature
231 .Diagnostics
232 .DiagnosticsMap
233 .lock()
234 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
235
236 let OwnerMap = DiagnosticsMapGuard.entry(Owner.clone()).or_default();
237
238 let mut ChangedURIKeys = Vec::new();
239 // `ChangedEntries` carries the post-update marker set per URI so the
240 // Sky-side `cel:diagnostics:changed` listener can call
241 // `IMarkerService.changeOne(owner, uri, markers)` without an extra
242 // IPC round-trip per change. URIs whose markers were cleared still
243 // appear here with an empty array, so the workbench replaces the
244 // previous owner-set rather than leaving stale red squiggles.
245 let mut ChangedEntries:Vec<serde_json::Value> = Vec::new();
246
247 for (URIComponentsValue, MarkersOption) in DeserializedEntries {
248 // Per-entry tolerance: a single malformed URI (extension
249 // passed an empty `.path`, exotic scheme, or non-string
250 // authority) used to fail the entire batch via `?`-prop -
251 // dropping every well-formed diagnostic in the same call
252 // because of one bad sibling. Mirror VS Code's
253 // `MarkerService._toMarker` which returns `undefined` for
254 // bad entries instead of throwing: skip the offender, log
255 // once, keep going so the rest of the batch reaches the
256 // renderer.
257 let URIKey = match Utility::UriParsing::GetURLFromURIComponentsDTO(&URIComponentsValue) {
258 Ok(Url) => Url.to_string(),
259 Err(Error) => {
260 dev_log!(
261 "extensions",
262 "warn: [DiagnosticProvider] skipping diagnostic entry with bad URI: {} (raw={:?})",
263 Error,
264 URIComponentsValue
265 );
266 continue;
267 },
268 };
269 if URIKey.is_empty() {
270 dev_log!(
271 "extensions",
272 "warn: [DiagnosticProvider] skipping diagnostic entry with empty URI string"
273 );
274 continue;
275 }
276
277 ChangedURIKeys.push(URIKey.clone());
278
279 let MarkersForEvent = match MarkersOption {
280 Some(Markers) => {
281 if Markers.is_empty() {
282 OwnerMap.remove(&URIKey);
283 Vec::new()
284 } else {
285 let MarkersClone = Markers.clone();
286 OwnerMap.insert(URIKey.clone(), Markers);
287 MarkersClone
288 }
289 },
290 None => {
291 OwnerMap.remove(&URIKey);
292 Vec::new()
293 },
294 };
295
296 ChangedEntries.push(json!({
297 "uri": URIKey,
298 "markers": MarkersForEvent,
299 }));
300 }
301
302 drop(DiagnosticsMapGuard);
303
304 // Notify the frontend that diagnostics have changed. Both keys are
305 // included for backward compatibility - older listeners read `Uris`
306 // (string-array) while the new SkyBridge marker bridge reads
307 // `changedURIs` (per-URI marker payload) to push directly into
308 // the workbench's `IMarkerService`.
309 let EventPayload = json!({
310 "Owner": Owner,
311 "owner": Owner,
312 "Uris": ChangedURIKeys,
313 "changedURIs": ChangedEntries,
314 });
315
316 // Route through `LogSkyEmit` so the channel + payload size lands
317 // in the `[DEV:SKY-EMIT]` histogram alongside SCM / tree-view /
318 // terminal emits. Diagnostic emit volume is one of the easiest
319 // signals to over- or under-count when triaging "Problems panel
320 // shows count but no items"; without LogSkyEmit the channel was
321 // invisible.
322 if let Err(Error) = LogSkyEmit(&self.ApplicationHandle, SkyEvent::DiagnosticsChanged.AsStr(), EventPayload) {
323 dev_log!(
324 "extensions",
325 "error: [DiagnosticProvider] Failed to emit 'diagnostics_changed': {}",
326 Error
327 );
328 }
329
330 dev_log!(
331 "extensions",
332 "[DiagnosticProvider] Emitted diagnostics changed for {} URI(s)",
333 ChangedURIKeys.len()
334 );
335
336 Ok(())
337 }
338
339 /// Clears all diagnostics from a specific owner.
340 async fn ClearDiagnostics(&self, Owner:String) -> Result<(), CommonError> {
341 dev_log!(
342 "extensions",
343 "[DiagnosticProvider] Clearing all diagnostics for owner: {}",
344 Owner
345 );
346
347 let (ClearedCount, ChangedURIKeys):(usize, Vec<String>) = {
348 let mut DiagnosticsMapGuard = self
349 .ApplicationState
350 .Feature
351 .Diagnostics
352 .DiagnosticsMap
353 .lock()
354 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
355
356 DiagnosticsMapGuard
357 .remove(&Owner)
358 .map(|OwnerMap| {
359 let keys:Vec<String> = OwnerMap.keys().cloned().collect();
360 (keys.len(), keys)
361 })
362 .unwrap_or((0, vec![]))
363 };
364
365 if !ChangedURIKeys.is_empty() {
366 dev_log!(
367 "extensions",
368 "[DiagnosticProvider] Cleared {} diagnostics across {} URI(s)",
369 ClearedCount,
370 ChangedURIKeys.len()
371 );
372
373 // Clear path - every URI's marker set goes to empty so the
374 // SkyBridge listener can wipe them via
375 // `IMarkerService.changeOne(owner, uri, [])`.
376 let ChangedEntries:Vec<serde_json::Value> =
377 ChangedURIKeys.iter().map(|Uri| json!({ "uri": Uri, "markers": [] })).collect();
378 let EventPayload = json!({
379 "Owner": Owner,
380 "owner": Owner,
381 "Uris": ChangedURIKeys,
382 "changedURIs": ChangedEntries,
383 });
384
385 if let Err(Error) = LogSkyEmit(&self.ApplicationHandle, SkyEvent::DiagnosticsChanged.AsStr(), EventPayload)
386 {
387 dev_log!(
388 "extensions",
389 "error: [DiagnosticProvider] Failed to emit 'diagnostics_changed' on clear: {}",
390 Error
391 );
392 }
393 }
394
395 Ok(())
396 }
397
398 /// Retrieves all diagnostics, optionally filtered by a resource URI.
399 /// Returns diagnostics aggregated from all owners for the specified
400 /// resource(s).
401 async fn GetAllDiagnostics(&self, ResourceURIFilterOption:Option<Value>) -> Result<Value, CommonError> {
402 dev_log!(
403 "extensions",
404 "[DiagnosticProvider] Getting all diagnostics with filter: {:?}",
405 ResourceURIFilterOption
406 );
407
408 let DiagnosticsMapGuard = self
409 .ApplicationState
410 .Feature
411 .Diagnostics
412 .DiagnosticsMap
413 .lock()
414 .map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
415
416 let mut ResultMap:std::collections::HashMap<String, Vec<MarkerDataDTO>> = std::collections::HashMap::new();
417
418 if let Some(FilterURIValue) = ResourceURIFilterOption {
419 let FilterURIKey = Utility::UriParsing::GetURLFromURIComponentsDTO(&FilterURIValue)?.to_string();
420
421 for OwnerMap in DiagnosticsMapGuard.values() {
422 if let Some(Markers) = OwnerMap.get(&FilterURIKey) {
423 ResultMap.entry(FilterURIKey.clone()).or_default().extend(Markers.clone());
424 }
425 }
426 } else {
427 // Aggregate all diagnostics from all owners for all files.
428 for OwnerMap in DiagnosticsMapGuard.values() {
429 for (URIKey, Markers) in OwnerMap.iter() {
430 ResultMap.entry(URIKey.clone()).or_default().extend(Markers.clone());
431 }
432 }
433 }
434
435 let ResultList:Vec<(String, Vec<MarkerDataDTO>)> = ResultMap.into_iter().collect();
436
437 dev_log!(
438 "extensions",
439 "[DiagnosticProvider] Returning {} diagnostic collection(s)",
440 ResultList.len()
441 );
442
443 serde_json::to_value(ResultList).map_err(|Error| CommonError::from(Error))
444 }
445}