Skip to content

Commit e7dd18e

Browse files
feat(Mountain): add diagnostics RPC handlers for extension problem reporting
- Implements `handle_change_many`, `handle_clear`, and `handle_get_diagnostics` RPC handlers in new `diagnostics.rs` - Stores diagnostic markers from extensions in `AppState.diagnostics_map` using nested HashMaps (owner → URI → markers) - Mirrors VS Code's `IMarkerData` and `IRelatedInformation` structures for compatibility with Cocoon shim layer - Emits `diagnostics_changed` Tauri events to Sky frontend when markers update, enabling real-time Problems panel updates - Handles URI key normalization through `get_uri_key_from_components` to match VS Code URI component serialization - Integrates with Track dispatcher to handle RPC calls from Cocoon's diagnostic shim implementation - Implements thread-safe Mutex access patterns for concurrent extension host operations This enables VS Code extensions running in Cocoon to report diagnostics (errors/warnings) that appear in the Land editor UI, a critical requirement for maintaining VS Code compatibility in the MVP architecture.
1 parent d7adb5b commit e7dd18e

1 file changed

Lines changed: 350 additions & 0 deletions

File tree

Source/handlers/diagnostics.rs

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
// ---------------------------------------------------------------------------------------------
2+
// Mountain Diagnostics Handlers (handlers/diagnostics.rs)
3+
// --------------------------------------------------------------------------------------------
4+
// Manages diagnostic information (problems/markers) reported by extensions
5+
// running in sidecars (like Cocoon). Handles RPC calls from Cocoon's
6+
// diagnostics shim, updates AppState, and emits events to notify the frontend
7+
// (Sky).
8+
//
9+
// Responsibilities:
10+
// - Handling `$changeMany` RPC calls: Receives diagnostic updates (as
11+
// `MarkerData` arrays or `undefined`/`null` for clearing) for specific URIs
12+
// from a specific owner (collection). Updates the central diagnostics store
13+
// in `AppState`.
14+
// - Handling `$clear` RPC calls: Removes all diagnostics associated with a
15+
// specific owner.
16+
// - Storing diagnostics state (e.g., Map<Owner: String, Map<UriString: String,
17+
18+
// Vec<MarkerData>>>) in `AppState`.
19+
// - Emitting Tauri events (`diagnostics_changed`) to notify the frontend (Sky)
20+
// when diagnostics for specific URIs have changed, allowing UI updates (e.g.,
21+
22+
// Problems panel).
23+
// - Handling `$getDiagnostics` RPC calls (optional): Aggregates and returns
24+
// diagnostics from the store, potentially filtered by resource.
25+
//
26+
// Key Interactions:
27+
// - Called by `track::dispatch_sidecar_request` for RPC methods.
28+
// - Interacts with `AppState` via Mutex to read/write the `diagnostics_map`.
29+
// - Uses `serde_json` to deserialize `MarkerData` based on `IMarkerData`
30+
// structure.
31+
// - Emits Tauri events (`diagnostics_changed`) via `AppHandle::emit_all`.
32+
// --------------------------------------------------------------------------------------------
33+
34+
use std::{
35+
collections::HashMap,
36+
37+
// Use StdMutex if used directly in AppState
38+
sync::{Arc, Mutex as StdMutex, MutexGuard},
39+
};
40+
41+
// Use log crate
42+
use log;
43+
use serde::{Deserialize, Serialize};
44+
use serde_json::{Value, json};
45+
// Added Manager trait for emit_all
46+
use tauri::{AppHandle, Manager, Runtime};
47+
48+
// Not strictly needed if using string keys from get_uri_key_from_components
49+
// use url::Url;
50+
51+
// Use AppState for storing diagnostics
52+
use crate::app_state::AppState;
53+
54+
// --- Helper Functions ---
55+
56+
/// Creates a structured error JSON string for RPC error responses.
57+
fn create_handler_error_string(message:String, code:Option<&str>) -> String {
58+
json!({ "message": message, "code": code.unwrap_or("EUNKNOWN") }).to_string()
59+
}
60+
61+
/// Helper to map Mutex lock poisoning errors to the handler's error string
62+
/// format.
63+
fn map_lock_error<T>(e:std::sync::PoisonError<MutexGuard<'_, T>>) -> String {
64+
create_handler_error_string(format!("Failed to acquire lock on diagnostics state: {}", e), Some("ELOCKED"))
65+
}
66+
67+
/// Helper to get a consistent string key from UriComponents Value received via
68+
/// JSON RPC. Tries to use the 'external' property first, then reconstructs from
69+
/// components.
70+
fn get_uri_key_from_components(uri_components:&Value) -> Option<String> {
71+
// Prioritize 'external' if present
72+
if let Some(ext) = uri_components.get("external").and_then(|v| v.as_str()) {
73+
return Some(ext.to_string());
74+
}
75+
// Fallback to scheme://authority/path structure
76+
let scheme = uri_components.get("scheme").and_then(|v| v.as_str())?;
77+
78+
let path = uri_components.get("path").and_then(|v| v.as_str())?;
79+
80+
let authority = uri_components.get("authority").and_then(|v| v.as_str()).unwrap_or("");
81+
82+
Some(format!("{}://{}{}", scheme, authority, path))
83+
}
84+
85+
// --- Data Structures ---
86+
87+
// Structure matching vs/platform/markers/common/markers.ts:IMarkerData
88+
#[derive(Serialize, Deserialize, Debug, Clone)]
89+
pub struct MarkerData {
90+
// Can be string or { value: string, target: UriComponents }
91+
pub code:Option<Value>,
92+
93+
// Error=8, Warn=4, Info=2, Hint=1
94+
pub severity:u32,
95+
96+
pub message:String,
97+
98+
pub source:Option<String>,
99+
100+
#[serde(rename = "startLineNumber")]
101+
pub start_line_number:u32,
102+
103+
#[serde(rename = "startColumn")]
104+
pub start_column:u32,
105+
106+
#[serde(rename = "endLineNumber")]
107+
pub end_line_number:u32,
108+
109+
#[serde(rename = "endColumn")]
110+
pub end_column:u32,
111+
112+
#[serde(rename = "modelVersionId")]
113+
pub model_version_id:Option<u64>,
114+
115+
#[serde(rename = "relatedInformation")]
116+
pub related_information:Option<Vec<RelatedInformation>>,
117+
118+
// Unnecessary=1, Deprecated=2
119+
pub tags:Option<Vec<u32>>,
120+
}
121+
122+
// Structure matching vs/platform/markers/common/markers.ts:IRelatedInformation
123+
#[derive(Serialize, Deserialize, Debug, Clone)]
124+
pub struct RelatedInformation {
125+
// UriComponents JSON Value
126+
pub resource:Value,
127+
128+
pub message:String,
129+
130+
#[serde(rename = "startLineNumber")]
131+
pub start_line_number:u32,
132+
133+
#[serde(rename = "startColumn")]
134+
pub start_column:u32,
135+
136+
#[serde(rename = "endLineNumber")]
137+
pub end_line_number:u32,
138+
139+
#[serde(rename = "endColumn")]
140+
pub end_column:u32,
141+
}
142+
143+
// --- RPC Handlers ---
144+
145+
/// Handles the `$changeMany` RPC call from a diagnostics provider.
146+
/// Updates diagnostics state for multiple URIs for a given owner.
147+
/// Args: `[owner: string, entries: [uriComponents: Value, markers: MarkerData[]
148+
/// | null][]]`
149+
pub async fn handle_change_many<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
150+
let owner = args
151+
.get(0)
152+
.and_then(Value::as_str)
153+
.ok_or_else(|| create_handler_error_string("Missing 'owner' (string) argument".to_string(), Some("EBADARG")))?
154+
.to_string();
155+
156+
let entries_val = args.get(1).ok_or_else(|| {
157+
create_handler_error_string("Missing 'entries' (array) argument".to_string(), Some("EBADARG"))
158+
})?;
159+
160+
let entries:Vec<(Value, Option<Vec<MarkerData>>)> = serde_json::from_value(entries_val.clone()).map_err(|e| {
161+
create_handler_error_string(format!("Failed to parse 'entries' argument: {}", e), Some("EBADMSG"))
162+
})?;
163+
164+
// Keep: Log owner and number of entries summary
165+
log::info!("[Diag Handler] changeMany owner='{}', {} entries", owner, entries.len());
166+
167+
let app_state = app.state::<AppState>();
168+
169+
let mut changed_uris:Vec<String> = Vec::new();
170+
171+
{
172+
// Scope for the mutex lock
173+
let mut all_owner_diags = app_state.diagnostics_map.lock().map_err(map_lock_error)?;
174+
175+
let resource_map = all_owner_diags.entry(owner.clone()).or_default();
176+
177+
for (uri_components_val, markers_opt) in entries {
178+
let uri_str = match get_uri_key_from_components(&uri_components_val) {
179+
Some(s) => s,
180+
181+
None => {
182+
log::warn!(
183+
"[Diag Handler] changeMany: Skipping entry for owner '{}' with invalid URI components: {:?}",
184+
owner,
185+
uri_components_val
186+
);
187+
188+
continue;
189+
},
190+
};
191+
192+
// Track affected URI
193+
changed_uris.push(uri_str.clone());
194+
195+
match markers_opt {
196+
Some(markers) if !markers.is_empty() => {
197+
// Set/replace markers for this URI
198+
// Reduce logging verbosity: log::trace!("[Diag Handler] Setting {} markers for
199+
// owner '{}', URI '{}'", markers.len(), owner, uri_str);
200+
201+
resource_map.insert(uri_str, markers);
202+
},
203+
204+
_ => {
205+
// Clear markers (received null, undefined, or empty array)
206+
// Reduce logging verbosity: log::trace!("[Diag Handler] Clearing markers for
207+
// owner '{}', URI '{}'", owner, uri_str);
208+
209+
resource_map.remove(&uri_str);
210+
},
211+
}
212+
}
213+
// Clean up owner entry if they have no diagnostics left
214+
if resource_map.is_empty() {
215+
log::info!(
216+
"[Diag Handler] Owner '{}' has no more diagnostics, removing owner entry.",
217+
owner
218+
);
219+
220+
all_owner_diags.remove(&owner);
221+
}
222+
// Lock released here
223+
}
224+
225+
// Notify frontend about the changes for the affected URIs of this owner
226+
if !changed_uris.is_empty() {
227+
let event_payload = json!({ "owner": owner, "uris": changed_uris });
228+
229+
// Keep: Log the event being emitted
230+
log::debug!("[Diag Handler] Emitting diagnostics_changed event: {:?}", event_payload);
231+
232+
if let Err(e) = app.emit_all("diagnostics_changed", event_payload) {
233+
log::error!("[Diag Handler] Failed to emit diagnostics_changed event: {}", e);
234+
}
235+
}
236+
237+
// Void operation success
238+
Ok(Value::Null)
239+
}
240+
241+
/// Handles the `$clear` RPC call from a diagnostics provider.
242+
/// Removes all diagnostics associated with the specified owner.
243+
/// Args: `[owner: string]`
244+
pub async fn handle_clear<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
245+
let owner = args
246+
.get(0)
247+
.and_then(Value::as_str)
248+
.ok_or_else(|| create_handler_error_string("Missing 'owner' (string) argument".to_string(), Some("EBADARG")))?
249+
.to_string();
250+
251+
// Keep: Clearing all is significant
252+
log::info!("[Diag Handler] clear owner='{}'", owner);
253+
254+
let app_state = app.state::<AppState>();
255+
256+
let mut cleared_uris:Vec<String> = Vec::new();
257+
258+
let mut owner_existed = false;
259+
260+
{
261+
// Scope lock
262+
let mut all_owner_diags = app_state.diagnostics_map.lock().map_err(map_lock_error)?;
263+
264+
if let Some(resource_map) = all_owner_diags.get(&owner) {
265+
// Get URIs before removal
266+
cleared_uris = resource_map.keys().cloned().collect();
267+
}
268+
// Attempt to remove the owner entry and check if it was present
269+
owner_existed = all_owner_diags.remove(&owner).is_some();
270+
271+
// Lock released here
272+
}
273+
274+
if owner_existed {
275+
// Keep: Confirmation log
276+
log::info!("[Diag Handler] Cleared all diagnostics for owner '{}'.", owner);
277+
278+
// Notify frontend about the cleared URIs for this owner, if any existed
279+
if !cleared_uris.is_empty() {
280+
let event_payload = json!({ "owner": owner, "uris": cleared_uris });
281+
282+
// Keep: Log the event being emitted
283+
log::debug!("[Diag Handler] Emitting diagnostics_changed event (clear): {:?}", event_payload);
284+
285+
if let Err(e) = app.emit_all("diagnostics_changed", event_payload) {
286+
log::error!("[Diag Handler] Failed to emit diagnostics_changed event after clear: {}", e);
287+
}
288+
}
289+
} else {
290+
// Keep: Warning log
291+
log::warn!("[Diag Handler] Owner '{}' not found for clearing.", owner);
292+
}
293+
294+
// Void operation success
295+
Ok(Value::Null)
296+
}
297+
298+
/// Handles the `$getDiagnostics` RPC call. Aggregates diagnostics, optionally
299+
/// filtered. Args: `[resource?: UriComponents]`
300+
pub async fn handle_get_diagnostics<R:Runtime>(app:AppHandle<R>, args:Value) -> Result<Value, String> {
301+
let resource_filter_val = args.get(0);
302+
303+
// Reduced logging: log::debug!("[Diag Handler] getDiagnostics filter='{:?}'",
304+
305+
// resource_filter_val);
306+
307+
let app_state = app.state::<AppState>();
308+
309+
let owner_diags = app_state.diagnostics_map.lock().map_err(map_lock_error)?;
310+
311+
let target_uri_str_opt:Option<String> = resource_filter_val
312+
// Treat null as no filter
313+
.filter(|v| !v.is_null())
314+
.and_then(get_uri_key_from_components);
315+
316+
let mut aggregated_map:HashMap<String, Vec<MarkerData>> = HashMap::new();
317+
318+
for (_owner, resource_map) in owner_diags.iter() {
319+
for (uri_str, markers) in resource_map.iter() {
320+
if let Some(target_uri) = &target_uri_str_opt {
321+
if uri_str != target_uri {
322+
continue;
323+
324+
// Apply filter
325+
}
326+
}
327+
aggregated_map
328+
.entry(uri_str.clone())
329+
.or_default()
330+
.extend(markers.iter().cloned());
331+
}
332+
}
333+
// Release lock
334+
drop(owner_diags);
335+
336+
// Convert aggregated map to expected result format: `[UriComponents,
337+
338+
// MarkerData[]][]`
339+
let result_list:Vec<(Value, Vec<MarkerData>)> = aggregated_map
340+
.into_iter()
341+
// Use external form for URI components
342+
.map(|(uri_str, markers)| (json!({ "external": uri_str }), markers))
343+
.collect();
344+
345+
serde_json::to_value(result_list).map_err(|e| {
346+
log::error!("Failed to serialize getDiagnostics result: {}", e);
347+
348+
create_handler_error_string(format!("Failed to serialize diagnostics result: {}", e), None)
349+
})
350+
}

0 commit comments

Comments
 (0)