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 (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}