use crate::core::palette::{Attr, TvColor};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
Normal,
Keyword,
String,
Comment,
Number,
Operator,
Identifier,
Type,
Preprocessor,
Function,
Special,
}
impl TokenType {
pub fn default_color(&self) -> Attr {
match self {
TokenType::Normal => Attr::new(TvColor::LightGray, TvColor::Blue),
TokenType::Keyword => Attr::new(TvColor::Yellow, TvColor::Blue),
TokenType::String => Attr::new(TvColor::LightRed, TvColor::Blue),
TokenType::Comment => Attr::new(TvColor::LightCyan, TvColor::Blue),
TokenType::Number => Attr::new(TvColor::LightMagenta, TvColor::Blue),
TokenType::Operator => Attr::new(TvColor::White, TvColor::Blue),
TokenType::Identifier => Attr::new(TvColor::LightGray, TvColor::Blue),
TokenType::Type => Attr::new(TvColor::LightGreen, TvColor::Blue),
TokenType::Preprocessor => Attr::new(TvColor::LightCyan, TvColor::Blue),
TokenType::Function => Attr::new(TvColor::Cyan, TvColor::Blue),
TokenType::Special => Attr::new(TvColor::White, TvColor::Blue),
}
}
}
#[derive(Debug, Clone)]
pub struct Token {
pub start: usize,
pub end: usize,
pub token_type: TokenType,
}
impl Token {
pub fn new(start: usize, end: usize, token_type: TokenType) -> Self {
Token {
start,
end,
token_type,
}
}
}
pub trait SyntaxHighlighter: Send + Sync {
fn language(&self) -> &str;
fn highlight_line(&self, line: &str, line_number: usize) -> Vec<Token>;
fn is_multiline_context(&self, _line_number: usize) -> bool {
false
}
fn update_multiline_state(&mut self, _line: &str, _line_number: usize) {}
}
pub struct PlainTextHighlighter;
impl SyntaxHighlighter for PlainTextHighlighter {
fn language(&self) -> &str {
"text"
}
fn highlight_line(&self, line: &str, _line_number: usize) -> Vec<Token> {
if line.is_empty() {
vec![]
} else {
vec![Token::new(0, line.len(), TokenType::Normal)]
}
}
}
pub struct RustHighlighter {
in_block_comment: bool,
}
impl RustHighlighter {
pub fn new() -> Self {
RustHighlighter {
in_block_comment: false,
}
}
fn is_rust_keyword(word: &str) -> bool {
matches!(
word,
"as" | "async" | "await" | "break" | "const" | "continue" | "crate"
| "dyn" | "else" | "enum" | "extern" | "false" | "fn" | "for"
| "if" | "impl" | "in" | "let" | "loop" | "match" | "mod" | "move"
| "mut" | "pub" | "ref" | "return" | "self" | "Self" | "static"
| "struct" | "super" | "trait" | "true" | "type" | "unsafe" | "use"
| "where" | "while"
)
}
fn is_rust_type(word: &str) -> bool {
matches!(
word,
"i8" | "i16" | "i32" | "i64" | "i128" | "isize"
| "u8" | "u16" | "u32" | "u64" | "u128" | "usize"
| "f32" | "f64" | "bool" | "char" | "str"
| "String" | "Vec" | "Option" | "Result" | "Box" | "Rc" | "Arc"
) || word.chars().next().map_or(false, |c| c.is_uppercase())
}
}
impl SyntaxHighlighter for RustHighlighter {
fn language(&self) -> &str {
"rust"
}
fn highlight_line(&self, line: &str, _line_number: usize) -> Vec<Token> {
let mut tokens = Vec::new();
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
if self.in_block_comment {
if let Some(end_pos) = line.find("*/") {
tokens.push(Token::new(0, end_pos + 2, TokenType::Comment));
i = end_pos + 2;
} else {
tokens.push(Token::new(0, line.len(), TokenType::Comment));
return tokens;
}
}
while i < chars.len() {
let ch = chars[i];
if i + 1 < chars.len() && ch == '/' && chars[i + 1] == '/' {
tokens.push(Token::new(i, chars.len(), TokenType::Comment));
break;
}
if i + 1 < chars.len() && ch == '/' && chars[i + 1] == '*' {
let start = i;
i += 2;
let mut found_end = false;
while i + 1 < chars.len() {
if chars[i] == '*' && chars[i + 1] == '/' {
i += 2;
found_end = true;
break;
}
i += 1;
}
if !found_end {
i = chars.len();
}
tokens.push(Token::new(start, i, TokenType::Comment));
continue;
}
if ch == '"' {
let start = i;
i += 1;
while i < chars.len() {
if chars[i] == '\\' && i + 1 < chars.len() {
i += 2; } else if chars[i] == '"' {
i += 1;
break;
} else {
i += 1;
}
}
tokens.push(Token::new(start, i, TokenType::String));
continue;
}
if ch == '\'' {
let start = i;
i += 1;
while i < chars.len() {
if chars[i] == '\\' && i + 1 < chars.len() {
i += 2; } else if chars[i] == '\'' {
i += 1;
break;
} else {
i += 1;
}
}
tokens.push(Token::new(start, i, TokenType::String));
continue;
}
if ch.is_ascii_digit() {
let start = i;
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '.') {
i += 1;
}
tokens.push(Token::new(start, i, TokenType::Number));
continue;
}
if matches!(ch, '+' | '-' | '*' | '/' | '%' | '=' | '<' | '>' | '!' | '&' | '|' | '^' | '~') {
let start = i;
i += 1;
while i < chars.len() && matches!(chars[i], '=' | '&' | '|' | '<' | '>') {
i += 1;
}
tokens.push(Token::new(start, i, TokenType::Operator));
continue;
}
if ch.is_alphabetic() || ch == '_' {
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
let word: String = chars[start..i].iter().collect();
let token_type = if Self::is_rust_keyword(&word) {
TokenType::Keyword
} else if Self::is_rust_type(&word) {
TokenType::Type
} else {
TokenType::Identifier
};
tokens.push(Token::new(start, i, token_type));
continue;
}
i += 1;
}
tokens
}
fn is_multiline_context(&self, _line_number: usize) -> bool {
self.in_block_comment
}
fn update_multiline_state(&mut self, line: &str, _line_number: usize) {
let mut chars = line.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '/' && chars.peek() == Some(&'*') {
self.in_block_comment = true;
chars.next();
} else if ch == '*' && chars.peek() == Some(&'/') {
self.in_block_comment = false;
chars.next();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_type_default_colors() {
assert_ne!(TokenType::Keyword.default_color(), Attr::new(TvColor::Black, TvColor::Black));
assert_ne!(TokenType::String.default_color(), Attr::new(TvColor::Black, TvColor::Black));
}
#[test]
fn test_plain_text_highlighter() {
let highlighter = PlainTextHighlighter;
let tokens = highlighter.highlight_line("Hello, world!", 0);
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Normal);
}
#[test]
fn test_rust_highlighter_keywords() {
let highlighter = RustHighlighter::new();
let tokens = highlighter.highlight_line("fn main() {", 0);
assert!(!tokens.is_empty());
let fn_token = tokens.iter().find(|t| t.token_type == TokenType::Keyword);
assert!(fn_token.is_some(), "Should find 'fn' keyword");
}
#[test]
fn test_rust_highlighter_strings() {
let highlighter = RustHighlighter::new();
let tokens = highlighter.highlight_line(r#"let s = "hello";"#, 0);
let string_token = tokens.iter().find(|t| t.token_type == TokenType::String);
assert!(string_token.is_some(), "Should find string literal");
}
#[test]
fn test_rust_highlighter_comments() {
let highlighter = RustHighlighter::new();
let tokens = highlighter.highlight_line("// This is a comment", 0);
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Comment);
}
#[test]
fn test_rust_highlighter_numbers() {
let highlighter = RustHighlighter::new();
let tokens = highlighter.highlight_line("let x = 42;", 0);
let number_token = tokens.iter().find(|t| t.token_type == TokenType::Number);
assert!(number_token.is_some(), "Should find number literal");
}
#[test]
fn test_rust_highlighter_types() {
let highlighter = RustHighlighter::new();
let tokens = highlighter.highlight_line("let x: i32 = 0;", 0);
let type_token = tokens.iter().find(|t| t.token_type == TokenType::Type);
assert!(type_token.is_some(), "Should find type name");
}
}