Skip to main content

rusty_rich/
syntax.rs

1//! Syntax highlighting — equivalent to Rich's `syntax.py`.
2//!
3//! Uses `syntect` for syntax highlighting (Rust equivalent of Pygments).
4
5use syntect::easy::HighlightLines;
6use syntect::highlighting::{ThemeSet, Style as SyntectStyle};
7use syntect::parsing::SyntaxSet;
8use syntect::util::LinesWithEndings;
9
10use crate::console::{ConsoleOptions, RenderResult, Renderable};
11use crate::segment::Segment;
12use crate::style::Style;
13
14/// A syntax-highlighted source code renderable.
15#[derive(Debug, Clone)]
16pub struct Syntax {
17    /// The source code.
18    pub code: String,
19    /// The language name (e.g. "rust", "python", "javascript").
20    pub language: String,
21    /// Optional theme name.
22    pub theme: String,
23    /// Starting line number (for line numbers).
24    pub start_line: usize,
25    /// If true, show line numbers.
26    pub line_numbers: bool,
27    /// If true, highlight the code.
28    pub highlight: bool,
29    /// Optional background color.
30    pub background_color: Option<crate::color::Color>,
31    /// Tab size.
32    pub tab_size: usize,
33}
34
35impl Syntax {
36    /// Create a new Syntax renderable for the given code and language.
37    pub fn new(code: impl Into<String>, language: impl Into<String>) -> Self {
38        Self {
39            code: code.into(),
40            language: language.into(),
41            theme: "base16-ocean.dark".to_string(),
42            start_line: 1,
43            line_numbers: false,
44            highlight: true,
45            background_color: None,
46            tab_size: 4,
47        }
48    }
49
50    /// Builder: set the syntect theme name (e.g. `"base16-ocean.dark"`, `"monokai"`).
51    pub fn theme(mut self, theme: impl Into<String>) -> Self { self.theme = theme.into(); self }
52
53    /// Builder: enable line numbers in the rendered output.
54    pub fn line_numbers(mut self) -> Self { self.line_numbers = true; self }
55
56    /// Builder: set the starting line number for display (default 1).
57    pub fn start_line(mut self, n: usize) -> Self { self.start_line = n; self }
58
59    /// Builder: set a background color for the code block.
60    pub fn background(mut self, color: crate::color::Color) -> Self { self.background_color = Some(color); self }
61}
62
63impl Renderable for Syntax {
64    fn render(&self, _options: &ConsoleOptions) -> RenderResult {
65        if !self.highlight || self.language.is_empty() {
66            // No highlighting — just render as plain text
67            let lines: Vec<Vec<Segment>> = self
68                .code
69                .lines()
70                .map(|line| vec![Segment::new(line), Segment::line()])
71                .collect();
72            return RenderResult { lines, items: Vec::new() };
73        }
74
75        let ss = SyntaxSet::load_defaults_newlines();
76        let ts = ThemeSet::load_defaults();
77
78        let syntax = ss
79            .find_syntax_by_name(&self.language)
80            .or_else(|| ss.find_syntax_by_extension(&self.language))
81            .unwrap_or_else(|| ss.find_syntax_plain_text());
82
83        let theme = &ts.themes[&self.theme];
84
85        let mut highlighter = HighlightLines::new(syntax, theme);
86
87        let mut lines: Vec<Vec<Segment>> = Vec::new();
88        let line_num_width = if self.line_numbers {
89            (self.code.lines().count().saturating_add(self.start_line))
90                .to_string()
91                .len()
92        } else {
93            0
94        };
95
96        for (i, line) in LinesWithEndings::from(&self.code).enumerate() {
97            let mut line_segments: Vec<Segment> = Vec::new();
98
99            // Line number
100            if self.line_numbers {
101                let num = i + self.start_line;
102                let num_str = format!("{:>width$} │ ", num, width = line_num_width);
103                line_segments.push(Segment::new(num_str));
104            }
105
106            // Highlight the line
107            match highlighter.highlight_line(line, &ss) {
108                Ok(highlighted) => {
109                    for (syntect_style, text) in &highlighted {
110                        let style = syntect_to_rich_style(syntect_style);
111                        line_segments.push(Segment::styled(
112                            text.to_string(),
113                            style,
114                        ));
115                    }
116                }
117                Err(_) => {
118                    line_segments.push(Segment::new(line));
119                }
120            }
121
122            lines.push(line_segments);
123        }
124
125        RenderResult { lines, items: Vec::new() }
126    }
127}
128
129/// Convert a syntect `Style` to our `Style`.
130fn syntect_to_rich_style(ss: &SyntectStyle) -> Style {
131    let mut style = Style::new();
132    let fg = ss.foreground;
133    style = style.color(crate::color::Color::from_rgb(fg.r, fg.g, fg.b));
134
135    if ss.font_style.contains(syntect::highlighting::FontStyle::BOLD) {
136        style = style.bold(true);
137    }
138    if ss.font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
139        style = style.italic(true);
140    }
141    if ss.font_style.contains(syntect::highlighting::FontStyle::UNDERLINE) {
142        style = style.underline(true);
143    }
144    style
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_syntax_no_highlight() {
153        let s = Syntax::new("fn main() {}", "rust");
154        let opts = ConsoleOptions::default();
155        let result = s.render(&opts);
156        let ansi = result.to_ansi();
157        assert!(ansi.contains("fn main"));
158    }
159
160    #[test]
161    fn test_syntax_line_numbers() {
162        let s = Syntax::new("line1\nline2\nline3", "").line_numbers();
163        let opts = ConsoleOptions::default();
164        let result = s.render(&opts);
165        let ansi = result.to_ansi();
166        assert!(ansi.contains("1"));
167    }
168}