hjkl_syntax_tui/lib.rs
1//! Ratatui adapter for `hjkl-syntax`.
2//!
3//! Converts [`hjkl_syntax::RenderOutput`] (renderer-agnostic
4//! [`hjkl_theme::StyleSpec`] spans) into `ratatui::style::Style`-typed row
5//! tables and routes [`hjkl_syntax::DiagSign`]s to [`hjkl_buffer_tui::Sign`]
6//! values for gutter rendering.
7//!
8//! # Quick-start
9//!
10//! ```rust
11//! use hjkl_syntax::{DiagSign, RenderOutput, PerfBreakdown};
12//! use hjkl_syntax_tui::{to_ratatui_spans, diag_signs_to_buffer_signs};
13//!
14//! // An empty output with no spans and no signs.
15//! let out = RenderOutput::new(0, vec![], vec![], (0, 0, 10), PerfBreakdown::new());
16//! let rows = to_ratatui_spans(&out.spans);
17//! assert!(rows.is_empty());
18//!
19//! let signs = diag_signs_to_buffer_signs(&out.signs);
20//! assert!(signs.is_empty());
21//! ```
22
23use hjkl_buffer_tui::Sign;
24use hjkl_syntax::{DiagSign, RenderOutput, StyleSpec};
25use hjkl_theme_tui::ToRatatui;
26use ratatui::style::{Color, Style};
27
28// ---------------------------------------------------------------------------
29// Public conversion functions
30// ---------------------------------------------------------------------------
31
32/// Convert a per-row [`StyleSpec`] span table (as produced by
33/// [`hjkl_syntax::RenderOutput::spans`]) into the equivalent
34/// `ratatui::style::Style`-typed table consumed by
35/// `hjkl_editor_tui::install_ratatui_syntax_spans`.
36///
37/// Each inner `(byte_start, byte_end, StyleSpec)` triple becomes
38/// `(byte_start, byte_end, ratatui::style::Style)`.
39///
40/// # Examples
41///
42/// ```rust
43/// use hjkl_syntax::StyleSpec;
44/// use hjkl_syntax_tui::to_ratatui_spans;
45///
46/// let spans: Vec<Vec<(usize, usize, StyleSpec)>> = vec![
47/// vec![(0, 5, StyleSpec::default())],
48/// vec![],
49/// ];
50/// let rows = to_ratatui_spans(&spans);
51/// assert_eq!(rows.len(), 2);
52/// assert_eq!(rows[0].len(), 1);
53/// assert!(rows[1].is_empty());
54/// ```
55pub fn to_ratatui_spans(
56 spans: &[Vec<(usize, usize, StyleSpec)>],
57) -> Vec<Vec<(usize, usize, Style)>> {
58 spans
59 .iter()
60 .map(|row| {
61 row.iter()
62 .map(|(start, end, spec)| (*start, *end, spec.to_ratatui()))
63 .collect()
64 })
65 .collect()
66}
67
68/// Convert a single [`StyleSpec`] to a `ratatui::style::Style`.
69///
70/// Convenience wrapper around the [`ToRatatui`] trait for callers that work
71/// with individual styles rather than the whole span table.
72///
73/// # Examples
74///
75/// ```rust
76/// use hjkl_syntax::StyleSpec;
77/// use hjkl_syntax_tui::spec_to_ratatui;
78///
79/// let style = spec_to_ratatui(&StyleSpec::default());
80/// // Default StyleSpec has no fg/bg and no modifiers; should round-trip.
81/// let _ = style;
82/// ```
83pub fn spec_to_ratatui(spec: &StyleSpec) -> Style {
84 spec.to_ratatui()
85}
86
87/// Convert [`DiagSign`]s (renderer-agnostic) into [`hjkl_buffer_tui::Sign`]s
88/// (ratatui-styled) using the canonical error colour (red foreground).
89///
90/// Higher-priority signs take precedence when multiple signs land on the
91/// same gutter row. The priority from [`DiagSign::priority`] is preserved.
92///
93/// # Examples
94///
95/// ```rust
96/// use hjkl_syntax::DiagSign;
97/// use hjkl_syntax_tui::diag_signs_to_buffer_signs;
98///
99/// let diags = vec![DiagSign::new(3, 'E', 100)];
100/// let signs = diag_signs_to_buffer_signs(&diags);
101/// assert_eq!(signs.len(), 1);
102/// assert_eq!(signs[0].row, 3);
103/// assert_eq!(signs[0].ch, 'E');
104/// assert_eq!(signs[0].priority, 100);
105/// ```
106pub fn diag_signs_to_buffer_signs(signs: &[DiagSign]) -> Vec<Sign> {
107 let err_style = Style::default().fg(Color::Red);
108 signs
109 .iter()
110 .map(|s| Sign {
111 row: s.row,
112 ch: s.ch,
113 style: err_style,
114 priority: s.priority,
115 })
116 .collect()
117}
118
119/// Convert a full [`RenderOutput`] into the ratatui-typed pair
120/// `(spans, signs)` ready for installation into an editor slot.
121///
122/// Returns the converted span table and the [`hjkl_buffer_tui::Sign`] vec.
123/// The order of operations matches the install path in `syntax_glue.rs`.
124///
125/// # Examples
126///
127/// ```rust
128/// use hjkl_syntax::{RenderOutput, PerfBreakdown};
129/// use hjkl_syntax_tui::render_output_to_tui;
130///
131/// let out = RenderOutput::new(
132/// 0,
133/// vec![vec![]],
134/// vec![],
135/// (1, 0, 30),
136/// PerfBreakdown::new(),
137/// );
138/// let (spans, signs) = render_output_to_tui(&out);
139/// assert_eq!(spans.len(), 1);
140/// assert!(signs.is_empty());
141/// ```
142#[allow(clippy::type_complexity)]
143pub fn render_output_to_tui(out: &RenderOutput) -> (Vec<Vec<(usize, usize, Style)>>, Vec<Sign>) {
144 let spans = to_ratatui_spans(&out.spans);
145 let signs = diag_signs_to_buffer_signs(&out.signs);
146 (spans, signs)
147}
148
149// ---------------------------------------------------------------------------
150// Tests
151// ---------------------------------------------------------------------------
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use hjkl_syntax::{
157 Color as ThemeColor, DiagSign, Modifiers, PerfBreakdown, RenderOutput, StyleSpec,
158 };
159 use ratatui::style::Modifier;
160
161 fn red_spec() -> StyleSpec {
162 StyleSpec {
163 fg: Some(ThemeColor {
164 r: 255,
165 g: 0,
166 b: 0,
167 a: 255,
168 }),
169 bg: None,
170 modifiers: Modifiers::default(),
171 }
172 }
173
174 fn bold_spec() -> StyleSpec {
175 StyleSpec {
176 fg: None,
177 bg: None,
178 modifiers: Modifiers {
179 bold: true,
180 ..Default::default()
181 },
182 }
183 }
184
185 // --- to_ratatui_spans ---
186
187 #[test]
188 fn to_ratatui_spans_empty_input() {
189 let result = to_ratatui_spans(&[]);
190 assert!(result.is_empty());
191 }
192
193 #[test]
194 fn to_ratatui_spans_empty_rows() {
195 let input: Vec<Vec<(usize, usize, StyleSpec)>> = vec![vec![], vec![]];
196 let result = to_ratatui_spans(&input);
197 assert_eq!(result.len(), 2);
198 assert!(result[0].is_empty());
199 assert!(result[1].is_empty());
200 }
201
202 #[test]
203 fn to_ratatui_spans_preserves_byte_offsets() {
204 let input = vec![vec![(3, 8, StyleSpec::default())]];
205 let result = to_ratatui_spans(&input);
206 assert_eq!(result.len(), 1);
207 assert_eq!(result[0][0].0, 3);
208 assert_eq!(result[0][0].1, 8);
209 }
210
211 #[test]
212 fn to_ratatui_spans_converts_fg_colour() {
213 let input = vec![vec![(0, 5, red_spec())]];
214 let result = to_ratatui_spans(&input);
215 let style = result[0][0].2;
216 assert_eq!(style.fg, Some(ratatui::style::Color::Rgb(255, 0, 0)));
217 }
218
219 #[test]
220 fn to_ratatui_spans_converts_bold_modifier() {
221 let input = vec![vec![(0, 3, bold_spec())]];
222 let result = to_ratatui_spans(&input);
223 let style = result[0][0].2;
224 assert!(style.add_modifier.contains(Modifier::BOLD));
225 }
226
227 // --- spec_to_ratatui ---
228
229 #[test]
230 fn spec_to_ratatui_default_is_plain() {
231 let style = spec_to_ratatui(&StyleSpec::default());
232 assert_eq!(style.fg, None);
233 assert_eq!(style.bg, None);
234 }
235
236 // --- diag_signs_to_buffer_signs ---
237
238 #[test]
239 fn diag_signs_empty() {
240 let result = diag_signs_to_buffer_signs(&[]);
241 assert!(result.is_empty());
242 }
243
244 #[test]
245 fn diag_signs_row_and_ch_preserved() {
246 let diags = vec![DiagSign::new(5, 'E', 100), DiagSign::new(12, 'W', 50)];
247 let result = diag_signs_to_buffer_signs(&diags);
248 assert_eq!(result.len(), 2);
249 assert_eq!(result[0].row, 5);
250 assert_eq!(result[0].ch, 'E');
251 assert_eq!(result[0].priority, 100);
252 assert_eq!(result[1].row, 12);
253 assert_eq!(result[1].ch, 'W');
254 assert_eq!(result[1].priority, 50);
255 }
256
257 #[test]
258 fn diag_signs_use_red_style() {
259 let diags = vec![DiagSign::new(0, 'E', 100)];
260 let result = diag_signs_to_buffer_signs(&diags);
261 assert_eq!(result[0].style.fg, Some(ratatui::style::Color::Red));
262 }
263
264 // --- render_output_to_tui ---
265
266 #[test]
267 fn render_output_to_tui_empty() {
268 let out = RenderOutput::new(0, vec![], vec![], (0, 0, 10), PerfBreakdown::new());
269 let (spans, signs) = render_output_to_tui(&out);
270 assert!(spans.is_empty());
271 assert!(signs.is_empty());
272 }
273
274 #[test]
275 fn render_output_to_tui_routes_spans_and_signs() {
276 let out = RenderOutput::new(
277 1,
278 vec![vec![(0, 4, red_spec())]],
279 vec![DiagSign::new(0, 'E', 100)],
280 (3, 0, 10),
281 PerfBreakdown::new(),
282 );
283 let (spans, signs) = render_output_to_tui(&out);
284 assert_eq!(spans.len(), 1);
285 assert_eq!(
286 spans[0][0].2.fg,
287 Some(ratatui::style::Color::Rgb(255, 0, 0))
288 );
289 assert_eq!(signs.len(), 1);
290 assert_eq!(signs[0].row, 0);
291 assert_eq!(signs[0].style.fg, Some(ratatui::style::Color::Red));
292 }
293}