reovim_tui_mod_diagnostics/
lib.rs1#![cfg_attr(coverage_nightly, allow(unused_features))]
2#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3use std::collections::HashMap;
13
14use reovim_client_driver::{
15 BufferId, ClientModule, ClientModuleError, InlineDecoration, ModuleContext, ProbeResult, Style,
16 Version, VirtualLine, VirtualLinePosition,
17};
18
19use {reovim_arch::Color, serde::Deserialize};
20
21#[derive(Debug, Deserialize)]
23#[serde(rename_all = "camelCase")]
24struct DiagnosticPayload {
25 active: bool,
26 #[serde(default)]
27 diagnostics: Vec<BufferDiagnostics>,
28}
29
30#[derive(Debug, Clone, Deserialize)]
32#[serde(rename_all = "camelCase")]
33struct BufferDiagnostics {
34 buffer_id: u64,
35 items: Vec<DiagnosticItemPayload>,
36}
37
38#[derive(Debug, Clone, Deserialize)]
40#[serde(rename_all = "camelCase")]
41struct DiagnosticItemPayload {
42 start_line: u32,
43 start_col: u32,
44 end_line: u32,
45 end_col: u32,
46 severity: String,
47 message: String,
48 #[serde(default)]
49 #[allow(dead_code)]
50 source: Option<String>,
51}
52
53const fn severity_color(severity: &str) -> Color {
55 match severity.as_bytes() {
56 b"error" => Color::Red,
57 b"warning" => Color::Yellow,
58 b"information" => Color::Cyan,
59 b"hint" => Color::Green,
60 _ => Color::Grey,
61 }
62}
63
64const fn severity_prefix(severity: &str) -> &str {
66 match severity.as_bytes() {
67 b"error" => "E",
68 b"warning" => "W",
69 b"information" => "I",
70 b"hint" => "H",
71 _ => "?",
72 }
73}
74
75pub struct DiagnosticsModule {
82 active: bool,
83 buffers: Vec<BufferDiagnostics>,
84 active_buffer_id: Option<u64>,
85 decorations_by_line: HashMap<usize, Vec<InlineDecoration>>,
87 cached_virtual_lines: Vec<VirtualLine>,
89}
90
91impl DiagnosticsModule {
92 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 active: false,
96 buffers: Vec::new(),
97 active_buffer_id: None,
98 decorations_by_line: HashMap::new(),
99 cached_virtual_lines: Vec::new(),
100 }
101 }
102
103 #[allow(clippy::cast_possible_truncation)]
105 fn rebuild_caches(&mut self) {
106 self.decorations_by_line.clear();
107 self.cached_virtual_lines.clear();
108
109 if !self.active {
110 return;
111 }
112
113 let Some(buf_id) = self.active_buffer_id else {
114 return;
115 };
116
117 let Some(buffer_diags) = self.buffers.iter().find(|b| b.buffer_id == buf_id) else {
118 return;
119 };
120
121 for diag in &buffer_diags.items {
122 let color = severity_color(&diag.severity);
123
124 if diag.start_line == diag.end_line {
126 let style = Style::new().fg(color);
127 self.decorations_by_line
128 .entry(diag.start_line as usize)
129 .or_default()
130 .push(InlineDecoration {
131 col_start: diag.start_col as u16,
132 col_end: diag.end_col as u16,
133 style,
134 });
135 }
136
137 let prefix = severity_prefix(&diag.severity);
139 let content = format!(" {prefix}: {}", diag.message);
140 let style = Style::new().fg(color);
141
142 self.cached_virtual_lines.push(VirtualLine {
143 buffer_line: diag.start_line as usize,
144 position: VirtualLinePosition::After,
145 content,
146 style,
147 });
148 }
149 }
150}
151
152impl Default for DiagnosticsModule {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158#[cfg_attr(coverage_nightly, coverage(off))]
159impl ClientModule for DiagnosticsModule {
160 fn id(&self) -> &'static str {
161 "diagnostics"
162 }
163
164 fn kind(&self) -> &'static str {
165 "diagnostics"
166 }
167
168 fn name(&self) -> &'static str {
169 "Diagnostics"
170 }
171
172 fn version(&self) -> Version {
173 Version::new(0, 1, 0)
174 }
175
176 fn init(&mut self, _ctx: &ModuleContext) -> ProbeResult {
177 ProbeResult::Success
178 }
179
180 fn exit(&mut self) -> Result<(), ClientModuleError> {
181 Ok(())
182 }
183
184 fn has_buffer_contrib(&self) -> bool {
185 self.active
186 }
187
188 fn on_notification(&mut self, data: &str) {
189 let Ok(payload) = serde_json::from_str::<DiagnosticPayload>(data) else {
190 return;
191 };
192
193 self.active = payload.active;
194
195 if self.active {
196 self.buffers = payload.diagnostics;
197 } else {
198 self.buffers.clear();
199 }
200
201 self.rebuild_caches();
202 }
203
204 #[allow(clippy::cast_possible_truncation)]
205 fn on_buffer_focus(&mut self, buffer_id: BufferId) {
206 self.active_buffer_id = Some(buffer_id.0 as u64);
207 self.rebuild_caches();
208 }
209
210 fn inline_decorations(&self, line: usize) -> &[InlineDecoration] {
211 self.decorations_by_line
212 .get(&line)
213 .map_or(&[], Vec::as_slice)
214 }
215
216 fn virtual_lines(&self) -> &[VirtualLine] {
217 &self.cached_virtual_lines
218 }
219}
220
221#[cfg(test)]
222mod tests;