Skip to main content

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}