Skip to main content

rl_lang/repl/
syntax_highlighting.rs

1//! Token-based syntax highlighting for the REPL input bar and output.
2//!
3//! Lexes the input with the real rl lexer and maps each [`TokenType`] to a
4//! ratatui [`Style`]. Gaps between token spans are emitted as unstyled raw spans
5//! to preserve whitespace exactly. On lex failure the entire input is returned
6//! as a single red span.
7//!
8//! # Color scheme
9//!
10//! | Token group         | Color / modifier         |
11//! |---------------------|--------------------------|
12//! | Control flow        | Cyan bold                |
13//! | Declarations        | Cyan italic              |
14//! | Import keywords     | Cyan dim                 |
15//! | Type keywords       | Light blue italic        |
16//! | Logical operators   | Yellow bold              |
17//! | Number literals     | Magenta                  |
18//! | String literals     | Yellow                   |
19//! | Char literals       | Light yellow             |
20//! | Bool literals       | Magenta italic           |
21//! | `null`              | Dark gray italic         |
22//! | Comparison ops      | Light cyan               |
23//! | Punctuation/braces  | Dark gray                |
24//! | Identifiers         | White                    |
25
26use ratatui::{
27    style::{Color, Modifier, Style},
28    text::Span,
29};
30
31use crate::{
32    lexer::{tokenizer::Tokenizer, tokentypes::TokenType},
33    utils::source::SourceFile,
34};
35
36/// Returns the ratatui [`Style`] for a given [`TokenType`].
37fn token_color(tt: &TokenType) -> Style {
38    match tt {
39        // control flow - cyan bold
40        TokenType::If
41        | TokenType::Else
42        | TokenType::While
43        | TokenType::For
44        | TokenType::Break
45        | TokenType::Continue
46        | TokenType::Return => Style::default()
47            .fg(Color::Cyan)
48            .add_modifier(Modifier::BOLD),
49
50        // declarations cyan italic
51        TokenType::Dec | TokenType::Const | TokenType::Fn | TokenType::Array => Style::default()
52            .fg(Color::Cyan)
53            .add_modifier(Modifier::ITALIC),
54
55        // import keywords cyan dim
56        TokenType::Get | TokenType::From => {
57            Style::default().fg(Color::Cyan).add_modifier(Modifier::DIM)
58        }
59
60        // types light blue italic
61        TokenType::Int
62        | TokenType::Float
63        | TokenType::Bool
64        | TokenType::String
65        | TokenType::Char
66        | TokenType::Byte
67        | TokenType::Error => Style::default()
68            .fg(Color::LightBlue)
69            .add_modifier(Modifier::ITALIC),
70
71        // logic operators yellow
72        TokenType::Or | TokenType::And => Style::default()
73            .fg(Color::Yellow)
74            .add_modifier(Modifier::BOLD),
75
76        // number literals magenta
77        TokenType::NumberLiteral(_) | TokenType::FloatLiteral(_) => {
78            Style::default().fg(Color::Magenta)
79        }
80
81        // string literals yellow
82        TokenType::StringLiteral(_) => Style::default().fg(Color::Yellow),
83
84        // char literals light yellow
85        TokenType::CharacterLiteral(_) => Style::default().fg(Color::LightYellow),
86
87        // bool literals magenta italic
88        TokenType::BoolLiteral(_) => Style::default()
89            .fg(Color::Magenta)
90            .add_modifier(Modifier::ITALIC),
91
92        // null dark gray italic
93        TokenType::Null => Style::default()
94            .fg(Color::DarkGray)
95            .add_modifier(Modifier::ITALIC),
96
97        // arrow white dim (->)
98        TokenType::Arrow => Style::default()
99            .fg(Color::White)
100            .add_modifier(Modifier::DIM),
101
102        // operators white
103        TokenType::Plus
104        | TokenType::Minus
105        | TokenType::Star
106        | TokenType::Slash
107        | TokenType::Bang
108        | TokenType::PlusEqual
109        | TokenType::MinusEqual
110        | TokenType::StarEqual
111        | TokenType::SlashEqual => Style::default().fg(Color::White),
112
113        // comparison operators light cyan
114        TokenType::Compare
115        | TokenType::BangEqual
116        | TokenType::Greater
117        | TokenType::GreaterEqual
118        | TokenType::Less
119        | TokenType::LessEqual => Style::default().fg(Color::LightCyan),
120
121        // assignment white
122        TokenType::Assign => Style::default().fg(Color::White),
123
124        // punctuation dark gray
125        TokenType::Comma
126        | TokenType::Semicolon
127        | TokenType::Colon
128        | TokenType::ColonColon
129        | TokenType::Dot
130        | TokenType::DotDot
131        | TokenType::Hash
132        | TokenType::BangHash => Style::default().fg(Color::DarkGray),
133
134        // braces/parens/brackets dark gray
135        TokenType::LeftBrace
136        | TokenType::RightBrace
137        | TokenType::LeftParen
138        | TokenType::RightParen
139        | TokenType::LeftBracket
140        | TokenType::RightBracket => Style::default().fg(Color::DarkGray),
141
142        // identifiers white
143        TokenType::Identifier(_) => Style::default().fg(Color::White),
144
145        _ => Style::default().fg(Color::White),
146    }
147}
148
149/// Lexes `input` and returns a vec of syntax-highlighted [`Span`]s.
150///
151/// On lex error returns a single red span containing the raw input.
152pub fn highlight(input: &str) -> Vec<Span<'static>> {
153    let source = SourceFile::new("<hl>", input.to_string());
154    let tokens = match Tokenizer::lex(source) {
155        Ok(t) => t,
156        Err(_) => {
157            return vec![Span::styled(
158                input.to_string(),
159                Style::default().fg(Color::Red),
160            )];
161        }
162    };
163
164    let mut spans = Vec::new();
165    let mut last = 0usize;
166    let chars: Vec<char> = input.chars().collect();
167
168    for tok in &tokens {
169        let start = tok.span.start;
170        let end = tok.span.end;
171
172        if start > last {
173            let gap: String = chars[last..start].iter().collect();
174            spans.push(Span::raw(gap));
175        }
176
177        let text: String = chars[start..end].iter().collect();
178        let style = token_color(&tok.token); // ← Style now, not Color
179        spans.push(Span::styled(text, style));
180        last = end;
181    }
182
183    if last < chars.len() {
184        let tail: String = chars[last..].iter().collect();
185        spans.push(Span::raw(tail));
186    }
187
188    spans
189}