ailloli_ui_editor/code/
diagnostics.rs

1//! Version-aware diagnostic value types and byte hit testing.
2
3use std::ops::Range;
4
5/// Diagnostic attached to a byte range in a code document.
6///
7/// Ranges are UTF-8 byte offsets and are not normalized or clamped here. Empty
8/// and reversed ranges are representable; hit testing uses inclusive endpoints.
9///
10/// # Examples
11///
12/// ```
13/// use ailloli_ui_editor::{Diagnostic, DiagnosticSeverity, DiagnosticSource};
14/// let diagnostic = Diagnostic::new(2..5, DiagnosticSeverity::Warning, "unused");
15/// assert_eq!(diagnostic.source, DiagnosticSource::Local);
16/// assert_eq!(diagnostic.range, 2..5);
17/// ```
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct Diagnostic {
20    /// Unvalidated UTF-8 byte range.
21    pub range: Range<usize>,
22    /// Error, warning, information, or hint level.
23    pub severity: DiagnosticSeverity,
24    /// Exact owned human-readable message; empty is allowed.
25    pub message: String,
26    /// Local or language-server provenance.
27    pub source: DiagnosticSource,
28    /// Source document revision for LSP diagnostics, otherwise `None` by default.
29    pub document_version: Option<crate::code::DocumentVersion>,
30}
31
32/// Diagnostic importance used for semantic coloring.
33///
34/// # Examples
35///
36/// ```
37/// use ailloli_ui_editor::DiagnosticSeverity;
38/// assert_ne!(DiagnosticSeverity::Error, DiagnosticSeverity::Hint);
39/// ```
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
41pub enum DiagnosticSeverity {
42    /// Error.
43    Error,
44    /// Warning.
45    Warning,
46    /// Informational notice.
47    Info,
48    /// Low-priority hint.
49    Hint,
50}
51
52/// Diagnostic producer category.
53///
54/// # Examples
55///
56/// ```
57/// use ailloli_ui_editor::DiagnosticSource;
58/// assert_ne!(DiagnosticSource::Local, DiagnosticSource::Lsp);
59/// ```
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
61pub enum DiagnosticSource {
62    /// Generated locally by the editor/application.
63    Local,
64    /// Produced by an LSP backend.
65    Lsp,
66}
67
68/// Diagnostic constructors.
69impl Diagnostic {
70    /// Creates a local diagnostic with no document version.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use ailloli_ui_editor::{Diagnostic, DiagnosticSeverity, DiagnosticSource};
76    /// let diagnostic = Diagnostic::new(0..0, DiagnosticSeverity::Hint, "");
77    /// assert_eq!(diagnostic.source, DiagnosticSource::Local);
78    /// assert_eq!(diagnostic.document_version, None);
79    /// ```
80    pub fn new(
81        range: Range<usize>,
82        severity: DiagnosticSeverity,
83        message: impl Into<String>,
84    ) -> Self {
85        Self {
86            range,
87            severity,
88            message: message.into(),
89            source: DiagnosticSource::Local,
90            document_version: None,
91        }
92    }
93
94    /// Creates an LSP diagnostic tied to an exact document version.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// use ailloli_ui_editor::{Diagnostic, DiagnosticSeverity, DiagnosticSource, DocumentVersion};
100    /// let diagnostic = Diagnostic::lsp(1..3, DiagnosticSeverity::Error, "bad", DocumentVersion(4));
101    /// assert_eq!(diagnostic.source, DiagnosticSource::Lsp);
102    /// assert_eq!(diagnostic.document_version, Some(DocumentVersion(4)));
103    /// ```
104    pub fn lsp(
105        range: Range<usize>,
106        severity: DiagnosticSeverity,
107        message: impl Into<String>,
108        document_version: crate::code::DocumentVersion,
109    ) -> Self {
110        Self {
111            range,
112            severity,
113            message: message.into(),
114            source: DiagnosticSource::Lsp,
115            document_version: Some(document_version),
116        }
117    }
118}
119
120/// Owned diagnostic plus its original slice index.
121///
122/// # Examples
123///
124/// ```
125/// use ailloli_ui_editor::{Diagnostic, DiagnosticHit, DiagnosticSeverity};
126/// let hit = DiagnosticHit { index: 2, diagnostic: Diagnostic::new(0..1, DiagnosticSeverity::Info, "note") };
127/// assert_eq!(hit.index, 2);
128/// ```
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct DiagnosticHit {
131    /// Index in the searched diagnostics slice.
132    pub index: usize,
133    /// Cloned matching diagnostic.
134    pub diagnostic: Diagnostic,
135}
136
137/// Returns the first diagnostic whose inclusive range contains `byte`.
138///
139/// Both start and end are inclusive, unlike standard Rust ranges; overlapping
140/// diagnostics select lowest slice index. No match returns `None`.
141///
142/// # Examples
143///
144/// ```
145/// use ailloli_ui_editor::{code::diagnostic_at_byte, Diagnostic, DiagnosticSeverity};
146/// let diagnostics = [Diagnostic::new(2..4, DiagnosticSeverity::Error, "bad")];
147/// assert_eq!(diagnostic_at_byte(&diagnostics, 4).unwrap().index, 0);
148/// assert!(diagnostic_at_byte(&diagnostics, 5).is_none());
149/// ```
150pub fn diagnostic_at_byte(diagnostics: &[Diagnostic], byte: usize) -> Option<DiagnosticHit> {
151    diagnostics
152        .iter()
153        .enumerate()
154        .find(|(_, diagnostic)| diagnostic.range.start <= byte && byte <= diagnostic.range.end)
155        .map(|(index, diagnostic)| DiagnosticHit {
156            index,
157            diagnostic: diagnostic.clone(),
158        })
159}