Skip to main content

lspkit_server/
diagnostics.rs

1//! Diagnostics fan-out bus.
2//!
3//! A single source of diagnostics (the engine) fans out to multiple sinks
4//! atomically per generation. The LSP sink calls `publishDiagnostics`; the
5//! MCP sink emits resource-update notifications.
6
7use std::sync::{Arc, Mutex};
8
9use async_trait::async_trait;
10use lspkit::Generation;
11use lspkit_vfs::DocumentUri;
12
13use crate::uri::path_to_uri;
14
15/// Severity ladder matching the LSP convention.
16#[non_exhaustive]
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum Severity {
19    /// Error.
20    Error,
21    /// Warning.
22    Warning,
23    /// Informational.
24    Information,
25    /// Hint.
26    Hint,
27}
28
29/// A single diagnostic finding.
30#[non_exhaustive]
31#[derive(Debug, Clone)]
32pub struct Diagnostic {
33    /// Severity ladder.
34    pub severity: Severity,
35    /// Human-readable message.
36    pub message: String,
37    /// Zero-based `(start_line, start_char, end_line, end_char)` range.
38    pub range: (u32, u32, u32, u32),
39    /// Optional diagnostic code (e.g. lint rule name).
40    pub code: Option<String>,
41    /// Optional source identifier (e.g. linter name).
42    pub source: Option<String>,
43}
44
45impl Diagnostic {
46    /// Construct a diagnostic with required fields. Optional fields default to `None`.
47    #[must_use]
48    pub fn new(
49        severity: Severity,
50        message: impl Into<String>,
51        range: (u32, u32, u32, u32),
52    ) -> Self {
53        Self {
54            severity,
55            message: message.into(),
56            range,
57            code: None,
58            source: None,
59        }
60    }
61
62    /// Attach a diagnostic `code`.
63    #[must_use]
64    pub fn with_code(mut self, code: impl Into<String>) -> Self {
65        self.code = Some(code.into());
66        self
67    }
68
69    /// Attach a `source` identifier.
70    #[must_use]
71    pub fn with_source(mut self, source: impl Into<String>) -> Self {
72        self.source = Some(source.into());
73        self
74    }
75}
76
77/// Diagnostics for a single document at a specific engine generation.
78#[non_exhaustive]
79#[derive(Debug, Clone)]
80pub struct DiagnosticsBatch {
81    /// The document the diagnostics apply to.
82    pub uri: DocumentUri,
83    /// The engine generation at which they were computed.
84    pub generation: Generation,
85    /// The findings.
86    pub diagnostics: Vec<Diagnostic>,
87}
88
89impl DiagnosticsBatch {
90    /// Construct a batch tagging `diagnostics` for `uri` at `generation`.
91    #[must_use]
92    pub fn new(uri: DocumentUri, generation: Generation, diagnostics: Vec<Diagnostic>) -> Self {
93        Self {
94            uri,
95            generation,
96            diagnostics,
97        }
98    }
99}
100
101/// A consumer of diagnostic batches.
102#[async_trait]
103pub trait DiagnosticsSink: Send + Sync + 'static {
104    /// Receive a batch of diagnostics. Implementations MUST NOT block.
105    async fn publish(&self, batch: DiagnosticsBatch);
106}
107
108/// Fan-out bus connecting one diagnostics source to many sinks.
109#[derive(Clone, Default)]
110pub struct DiagnosticsBus {
111    sinks: Arc<Mutex<Vec<Arc<dyn DiagnosticsSink>>>>,
112}
113
114impl DiagnosticsBus {
115    /// New bus with no sinks attached.
116    #[must_use]
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    /// Attach a sink. Returns the new sink count.
122    pub fn attach(&self, sink: Arc<dyn DiagnosticsSink>) -> usize {
123        if let Ok(mut guard) = self.sinks.lock() {
124            guard.push(sink);
125            guard.len()
126        } else {
127            0
128        }
129    }
130
131    /// Publish a batch to every attached sink concurrently.
132    pub async fn publish(&self, batch: DiagnosticsBatch) {
133        let snapshot: Vec<Arc<dyn DiagnosticsSink>> = self
134            .sinks
135            .lock()
136            .map(|guard| guard.clone())
137            .unwrap_or_default();
138        for sink in snapshot {
139            let sink_batch = batch.clone();
140            sink.publish(sink_batch).await;
141        }
142    }
143}
144
145impl std::fmt::Debug for DiagnosticsBus {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        let count = self.sinks.lock().map_or(0, |g| g.len());
148        f.debug_struct("DiagnosticsBus")
149            .field("sinks", &count)
150            .finish()
151    }
152}
153
154/// Convenience: build a `DocumentUri` from an absolute path.
155///
156/// # Errors
157/// Returns [`crate::uri::UriError`] if the path cannot be expressed as a URI.
158pub fn document_uri_from_path(path: &std::path::Path) -> Result<DocumentUri, crate::uri::UriError> {
159    Ok(DocumentUri::new(path_to_uri(path)?))
160}