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}