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, and notifies the
6//! UI when changes occur.
7//!
8//! Diagnostics are stored in `ApplicationState.Feature.Diagnostics` as a
9//! nested `HashMap<owner, HashMap<uri, Vec<MarkerDataDTO>>>`. Each owner (e.g.
10//! `"typescript"`, `"rust-analyzer"`) manages its collection independently.
11//!
12//! ## Data model
13//!
14//! Each `MarkerDataDTO` carries:
15//! - `Severity` - Error(8), Warning(4), Information(2), Hint(1)
16//! - `Message` - human-readable description
17//! - `StartLineNumber` / `StartColumn` - 1-based (Cocoon's
18//!   `NormaliseDiagnostic` adds `+1`)
19//! - `EndLineNumber` / `EndColumn` - 1-based, same convention
20//! - `Source` - diagnostic source string (e.g. `"tslint"`)
21//! - `Code` - diagnostic code for quick-fix lookup
22//! - `ModelVersionIdentifier` - document version for change tracking
23//!
24//! ## Notification flow
25//!
26//! 1. Language server or extension calls `SetDiagnostics(owner, entries)`.
27//! 2. Provider validates and stores in `ApplicationState.Feature.Diagnostics`.
28//! 3. Provider identifies which URIs changed in this update.
29//! 4. Provider emits `sky://diagnostics/changed` with `owner`, `uris` (string
30//!    array for back-compat), and `changedURIs` (per-URI marker payload for the
31//!    SkyBridge marker bridge).
32//! 5. Sky receives the event and updates squiggles and the Problems panel.
33//!
34//! ## VS Code reference
35//!
36//! - `vs/workbench/services/diagnostic/common/diagnosticCollection.ts`
37//! - `vs/platform/diagnostics/common/diagnostics.ts`
38//! - `vs/workbench/services/diagnostic/common/diagnosticService.ts`
39
40use CommonLibrary::{
41	Diagnostic::DiagnosticManager::DiagnosticManager,
42	Error::CommonError::CommonError,
43	IPC::SkyEvent::SkyEvent,
44};
45use async_trait::async_trait;
46use serde_json::{Value, json};
47
48// `tauri::Emitter` is no longer used directly here - all emits
49// route through `LogSkyEmit` which carries the trait import. The
50// import was previously here for the direct `.emit()` calls now
51// replaced. Removed to keep the file warning-clean.
52use super::{MountainEnvironment::MountainEnvironment, Utility};
53use crate::{ApplicationState::DTO::MarkerDataDTO::MarkerDataDTO, IPC::SkyEmit::LogSkyEmit, dev_log};
54
55// TODO: severity filtering, code actions/quick-fix integration, diagnostic
56// inline messages, history/undo-redo, export, suppression comments,
57// telemetry, remote diagnostics, caching, workspace-wide filtering.
58#[async_trait]
59impl DiagnosticManager for MountainEnvironment {
60	/// Sets or updates diagnostics for multiple resources from a specific
61	/// owner. Empty marker arrays are treated as clearing diagnostics for that
62	/// URI.
63	async fn SetDiagnostics(&self, Owner:String, EntriesDTOValue:Value) -> Result<(), CommonError> {
64		dev_log!("extensions", "[DiagnosticProvider] Setting diagnostics for owner: {}", Owner);
65
66		let DeserializedEntries:Vec<(Value, Option<Vec<MarkerDataDTO>>)> = serde_json::from_value(EntriesDTOValue)
67			.map_err(|Error| {
68				CommonError::InvalidArgument {
69					ArgumentName:"EntriesDTOValue".to_string(),
70					Reason:format!("Failed to deserialize diagnostic entries: {}", Error),
71				}
72			})?;
73
74		let mut DiagnosticsMapGuard = self
75			.ApplicationState
76			.Feature
77			.Diagnostics
78			.DiagnosticsMap
79			.lock()
80			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
81
82		let OwnerMap = DiagnosticsMapGuard.entry(Owner.clone()).or_default();
83
84		let mut ChangedURIKeys = Vec::new();
85
86		// `ChangedEntries` carries the post-update marker set per URI so the
87		// Sky-side `cel:diagnostics:changed` listener can call
88		// `IMarkerService.changeOne(owner, uri, markers)` without an extra
89		// IPC round-trip per change. URIs whose markers were cleared still
90		// appear here with an empty array, so the workbench replaces the
91		// previous owner-set rather than leaving stale red squiggles.
92		let mut ChangedEntries:Vec<serde_json::Value> = Vec::new();
93
94		for (URIComponentsValue, MarkersOption) in DeserializedEntries {
95			// Per-entry tolerance: a single malformed URI (extension
96			// passed an empty `.path`, exotic scheme, or non-string
97			// authority) used to fail the entire batch via `?`-prop -
98			// dropping every well-formed diagnostic in the same call
99			// because of one bad sibling. Mirror VS Code's
100			// `MarkerService._toMarker` which returns `undefined` for
101			// bad entries instead of throwing: skip the offender, log
102			// once, keep going so the rest of the batch reaches the
103			// renderer.
104			let URIKey = match Utility::UriParsing::GetURLFromURIComponentsDTO(&URIComponentsValue) {
105				Ok(Url) => Url.to_string(),
106
107				Err(Error) => {
108					dev_log!(
109						"extensions",
110						"warn: [DiagnosticProvider] skipping diagnostic entry with bad URI: {} (raw={:?})",
111						Error,
112						URIComponentsValue
113					);
114
115					continue;
116				},
117			};
118
119			if URIKey.is_empty() {
120				dev_log!(
121					"extensions",
122					"warn: [DiagnosticProvider] skipping diagnostic entry with empty URI string"
123				);
124
125				continue;
126			}
127
128			ChangedURIKeys.push(URIKey.clone());
129
130			let MarkersForEvent = match MarkersOption {
131				Some(Markers) => {
132					if Markers.is_empty() {
133						OwnerMap.remove(&URIKey);
134
135						Vec::new()
136					} else {
137						let MarkersClone = Markers.clone();
138
139						OwnerMap.insert(URIKey.clone(), Markers);
140
141						MarkersClone
142					}
143				},
144
145				None => {
146					OwnerMap.remove(&URIKey);
147
148					Vec::new()
149				},
150			};
151
152			ChangedEntries.push(json!({
153				"uri": URIKey,
154				"markers": MarkersForEvent,
155			}));
156		}
157
158		drop(DiagnosticsMapGuard);
159
160		// Notify the frontend that diagnostics have changed. Both keys are
161		// included for backward compatibility - older listeners read `Uris`
162		// (string-array) while the new SkyBridge marker bridge reads
163		// `changedURIs` (per-URI marker payload) to push directly into
164		// the workbench's `IMarkerService`.
165		let EventPayload = json!({
166			"Owner": Owner,
167			"owner": Owner,
168			"Uris": ChangedURIKeys,
169			"changedURIs": ChangedEntries,
170		});
171
172		// Route through `LogSkyEmit` so the channel + payload size lands
173		// in the `[DEV:SKY-EMIT]` histogram alongside SCM / tree-view /
174		// terminal emits. Diagnostic emit volume is one of the easiest
175		// signals to over- or under-count when triaging "Problems panel
176		// shows count but no items"; without LogSkyEmit the channel was
177		// invisible.
178		if let Err(Error) = LogSkyEmit(&self.ApplicationHandle, SkyEvent::DiagnosticsChanged.AsStr(), EventPayload) {
179			dev_log!(
180				"extensions",
181				"error: [DiagnosticProvider] Failed to emit 'diagnostics_changed': {}",
182				Error
183			);
184		}
185
186		dev_log!(
187			"extensions",
188			"[DiagnosticProvider] Emitted diagnostics changed for {} URI(s)",
189			ChangedURIKeys.len()
190		);
191
192		Ok(())
193	}
194
195	/// Clears all diagnostics from a specific owner.
196	async fn ClearDiagnostics(&self, Owner:String) -> Result<(), CommonError> {
197		dev_log!(
198			"extensions",
199			"[DiagnosticProvider] Clearing all diagnostics for owner: {}",
200			Owner
201		);
202
203		let (ClearedCount, ChangedURIKeys):(usize, Vec<String>) = {
204			let mut DiagnosticsMapGuard = self
205				.ApplicationState
206				.Feature
207				.Diagnostics
208				.DiagnosticsMap
209				.lock()
210				.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
211
212			DiagnosticsMapGuard
213				.remove(&Owner)
214				.map(|OwnerMap| {
215					let keys:Vec<String> = OwnerMap.keys().cloned().collect();
216					(keys.len(), keys)
217				})
218				.unwrap_or((0, vec![]))
219		};
220
221		if !ChangedURIKeys.is_empty() {
222			dev_log!(
223				"extensions",
224				"[DiagnosticProvider] Cleared {} diagnostics across {} URI(s)",
225				ClearedCount,
226				ChangedURIKeys.len()
227			);
228
229			// Clear path - every URI's marker set goes to empty so the
230			// SkyBridge listener can wipe them via
231			// `IMarkerService.changeOne(owner, uri, [])`.
232			let ChangedEntries:Vec<serde_json::Value> =
233				ChangedURIKeys.iter().map(|Uri| json!({ "uri": Uri, "markers": [] })).collect();
234
235			let EventPayload = json!({
236				"Owner": Owner,
237				"owner": Owner,
238				"Uris": ChangedURIKeys,
239				"changedURIs": ChangedEntries,
240			});
241
242			if let Err(Error) = LogSkyEmit(&self.ApplicationHandle, SkyEvent::DiagnosticsChanged.AsStr(), EventPayload)
243			{
244				dev_log!(
245					"extensions",
246					"error: [DiagnosticProvider] Failed to emit 'diagnostics_changed' on clear: {}",
247					Error
248				);
249			}
250		}
251
252		Ok(())
253	}
254
255	/// Retrieves all diagnostics, optionally filtered by a resource URI.
256	/// Returns diagnostics aggregated from all owners for the specified
257	/// resource(s).
258	async fn GetAllDiagnostics(&self, ResourceURIFilterOption:Option<Value>) -> Result<Value, CommonError> {
259		dev_log!(
260			"extensions",
261			"[DiagnosticProvider] Getting all diagnostics with filter: {:?}",
262			ResourceURIFilterOption
263		);
264
265		let DiagnosticsMapGuard = self
266			.ApplicationState
267			.Feature
268			.Diagnostics
269			.DiagnosticsMap
270			.lock()
271			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
272
273		let mut ResultMap:std::collections::HashMap<String, Vec<MarkerDataDTO>> = std::collections::HashMap::new();
274
275		if let Some(FilterURIValue) = ResourceURIFilterOption {
276			let FilterURIKey = Utility::UriParsing::GetURLFromURIComponentsDTO(&FilterURIValue)?.to_string();
277
278			for OwnerMap in DiagnosticsMapGuard.values() {
279				if let Some(Markers) = OwnerMap.get(&FilterURIKey) {
280					ResultMap.entry(FilterURIKey.clone()).or_default().extend(Markers.clone());
281				}
282			}
283		} else {
284			// Aggregate all diagnostics from all owners for all files.
285			for OwnerMap in DiagnosticsMapGuard.values() {
286				for (URIKey, Markers) in OwnerMap.iter() {
287					ResultMap.entry(URIKey.clone()).or_default().extend(Markers.clone());
288				}
289			}
290		}
291
292		let ResultList:Vec<(String, Vec<MarkerDataDTO>)> = ResultMap.into_iter().collect();
293
294		dev_log!(
295			"extensions",
296			"[DiagnosticProvider] Returning {} diagnostic collection(s)",
297			ResultList.len()
298		);
299
300		serde_json::to_value(ResultList).map_err(|Error| CommonError::from(Error))
301	}
302}