use blinc_core::Color;
use regex::Regex;
use crate::styled_text::{StyledLine, StyledText, TextSpan};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum TokenType {
Keyword,
String,
Comment,
Number,
Type,
Function,
Variable,
Macro,
Operator,
Lifetime,
Custom(String),
}
impl TokenType {
pub fn custom(name: impl Into<String>) -> Self {
TokenType::Custom(name.into())
}
}
#[derive(Clone, Debug)]
pub struct TokenHit {
pub text: String,
pub token_type: TokenType,
pub line: usize,
pub start_column: usize,
pub end_column: usize,
}
impl TokenHit {
pub fn new(
text: impl Into<String>,
token_type: TokenType,
line: usize,
start_column: usize,
end_column: usize,
) -> Self {
Self {
text: text.into(),
token_type,
line,
start_column,
end_column,
}
}
}
#[derive(Clone)]
pub struct TokenRule {
pattern: Regex,
pub color: Color,
pub bold: bool,
pub token_type: TokenType,
}
impl TokenRule {
pub fn new(pattern: &str, color: Color) -> Self {
Self {
pattern: Regex::new(pattern).expect("Invalid regex pattern"),
color,
bold: false,
token_type: TokenType::Custom("unknown".to_string()),
}
}
pub fn try_new(pattern: &str, color: Color) -> Option<Self> {
Regex::new(pattern).ok().map(|pattern| Self {
pattern,
color,
bold: false,
token_type: TokenType::Custom("unknown".to_string()),
})
}
pub fn token_type(mut self, token_type: TokenType) -> Self {
self.token_type = token_type;
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn pattern(&self) -> &Regex {
&self.pattern
}
}
impl std::fmt::Debug for TokenRule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenRule")
.field("pattern", &self.pattern.as_str())
.field("color", &self.color)
.field("bold", &self.bold)
.finish()
}
}
pub trait SyntaxHighlighter: Send + Sync {
fn token_rules(&self) -> &[TokenRule];
fn default_color(&self) -> Color {
Color::rgba(0.9, 0.9, 0.9, 1.0)
}
fn background_color(&self) -> Color {
Color::rgba(0.12, 0.12, 0.14, 1.0)
}
fn line_number_color(&self) -> Color {
Color::rgba(0.45, 0.45, 0.5, 1.0)
}
fn highlight(&self, text: &str) -> StyledText {
let default_color = self.default_color();
let rules = self.token_rules();
if text.is_empty() {
return StyledText::from_lines(vec![highlight_line("", rules, default_color)]);
}
let lines = text
.lines()
.map(|line| highlight_line(line, rules, default_color))
.collect();
StyledText::from_lines(lines)
}
}
pub struct SyntaxConfig {
highlighter: Box<dyn SyntaxHighlighter>,
}
impl SyntaxConfig {
pub fn new(highlighter: impl SyntaxHighlighter + 'static) -> Self {
Self {
highlighter: Box::new(highlighter),
}
}
pub fn highlighter(&self) -> &dyn SyntaxHighlighter {
self.highlighter.as_ref()
}
pub fn into_arc(self) -> std::sync::Arc<dyn SyntaxHighlighter> {
std::sync::Arc::from(self.highlighter)
}
}
#[derive(Debug, Clone)]
struct TokenMatch {
start: usize,
end: usize,
color: Color,
bold: bool,
token_type: TokenType,
}
fn highlight_line(line: &str, rules: &[TokenRule], default_color: Color) -> StyledLine {
if line.is_empty() {
return StyledLine::new(line, vec![]);
}
let mut matches: Vec<TokenMatch> = Vec::new();
for rule in rules {
for m in rule.pattern.find_iter(line) {
matches.push(TokenMatch {
start: m.start(),
end: m.end(),
color: rule.color,
bold: rule.bold,
token_type: rule.token_type.clone(),
});
}
}
matches.sort_by_key(|m| m.start);
let mut spans: Vec<TextSpan> = Vec::new();
let mut current_pos = 0;
for m in matches {
if m.start < current_pos {
continue;
}
if m.start > current_pos {
spans.push(TextSpan::new(current_pos, m.start, default_color, false));
}
let mut span = TextSpan::new(m.start, m.end, m.color, m.bold);
span.token_type = Some(m.token_type.clone());
spans.push(span);
current_pos = m.end;
}
if current_pos < line.len() {
spans.push(TextSpan::new(current_pos, line.len(), default_color, false));
}
StyledLine::new(line, spans)
}
pub struct PlainHighlighter {
text_color: Color,
bg_color: Color,
}
impl PlainHighlighter {
pub fn new() -> Self {
Self {
text_color: Color::rgba(0.9, 0.9, 0.9, 1.0),
bg_color: Color::rgba(0.12, 0.12, 0.14, 1.0),
}
}
pub fn text_color(mut self, color: Color) -> Self {
self.text_color = color;
self
}
pub fn background(mut self, color: Color) -> Self {
self.bg_color = color;
self
}
}
impl Default for PlainHighlighter {
fn default() -> Self {
Self::new()
}
}
impl SyntaxHighlighter for PlainHighlighter {
fn token_rules(&self) -> &[TokenRule] {
&[]
}
fn default_color(&self) -> Color {
self.text_color
}
fn background_color(&self) -> Color {
self.bg_color
}
}
pub struct RustHighlighter {
rules: Vec<TokenRule>,
bg_color: Color,
}
impl RustHighlighter {
pub fn new() -> Self {
let keyword_color = Color::rgba(0.77, 0.56, 0.82, 1.0); let string_color = Color::rgba(0.81, 0.54, 0.44, 1.0); let comment_color = Color::rgba(0.42, 0.54, 0.35, 1.0); let number_color = Color::rgba(0.71, 0.82, 0.57, 1.0); let function_color = Color::rgba(0.86, 0.82, 0.65, 1.0); let type_color = Color::rgba(0.31, 0.76, 0.77, 1.0); let macro_color = Color::rgba(0.31, 0.76, 0.77, 1.0); let lifetime_color = Color::rgba(0.77, 0.56, 0.82, 1.0);
let rules = vec![
TokenRule::new(r"//.*$", comment_color).token_type(TokenType::Comment),
TokenRule::new(r#""[^"]*""#, string_color).token_type(TokenType::String),
TokenRule::new(r"'[^']{1,2}'", string_color).token_type(TokenType::String),
TokenRule::new(r"'[a-zA-Z_][a-zA-Z0-9_]*", lifetime_color).token_type(TokenType::Lifetime),
TokenRule::new(r"\b[a-z_][a-zA-Z0-9_]*!", macro_color).token_type(TokenType::Macro),
TokenRule::new(
r"\b(fn|let|mut|const|static|pub|use|mod|struct|enum|trait|impl|for|while|loop|if|else|match|return|break|continue|async|await|move|ref|self|Self|super|crate|where|type|dyn|unsafe|extern)\b",
keyword_color,
).bold().token_type(TokenType::Keyword),
TokenRule::new(r"\b[A-Z][a-zA-Z0-9_]*\b", type_color).token_type(TokenType::Type),
TokenRule::new(r"\b\d+(\.\d+)?([eE][+-]?\d+)?[fiu]?(8|16|32|64|128|size)?\b", number_color).token_type(TokenType::Number),
TokenRule::new(r"\b0x[0-9a-fA-F_]+\b", number_color).token_type(TokenType::Number),
TokenRule::new(r"\b0b[01_]+\b", number_color).token_type(TokenType::Number),
TokenRule::new(r"\b0o[0-7_]+\b", number_color).token_type(TokenType::Number),
TokenRule::new(r"\b([a-z_][a-zA-Z0-9_]*)\s*\(", function_color).token_type(TokenType::Function),
];
Self {
rules,
bg_color: Color::rgba(0.12, 0.12, 0.14, 1.0),
}
}
}
impl Default for RustHighlighter {
fn default() -> Self {
Self::new()
}
}
impl SyntaxHighlighter for RustHighlighter {
fn token_rules(&self) -> &[TokenRule] {
&self.rules
}
fn background_color(&self) -> Color {
self.bg_color
}
}
pub struct JsonHighlighter {
rules: Vec<TokenRule>,
}
impl JsonHighlighter {
pub fn new() -> Self {
let string_color = Color::rgba(0.81, 0.54, 0.44, 1.0); let number_color = Color::rgba(0.71, 0.82, 0.57, 1.0); let keyword_color = Color::rgba(0.77, 0.56, 0.82, 1.0); let key_color = Color::rgba(0.61, 0.78, 0.92, 1.0);
let rules = vec![
TokenRule::new(r#""[^"]*"\s*:"#, key_color),
TokenRule::new(r#""(?:[^"\\]|\\.)*""#, string_color),
TokenRule::new(r"-?\b\d+(\.\d+)?([eE][+-]?\d+)?\b", number_color),
TokenRule::new(r"\b(true|false|null)\b", keyword_color),
];
Self { rules }
}
}
impl Default for JsonHighlighter {
fn default() -> Self {
Self::new()
}
}
impl SyntaxHighlighter for JsonHighlighter {
fn token_rules(&self) -> &[TokenRule] {
&self.rules
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plain_highlighter() {
let highlighter = PlainHighlighter::new();
let styled = highlighter.highlight("Hello, World!");
assert_eq!(styled.line_count(), 1);
assert_eq!(styled.lines[0].spans.len(), 1);
assert_eq!(styled.lines[0].spans[0].start, 0);
assert_eq!(styled.lines[0].spans[0].end, 13);
}
#[test]
fn test_rust_keywords() {
let highlighter = RustHighlighter::new();
let styled = highlighter.highlight("fn main() {}");
assert_eq!(styled.line_count(), 1);
assert!(!styled.lines[0].spans.is_empty());
}
#[test]
fn test_multiline() {
let highlighter = RustHighlighter::new();
let code = "fn main() {\n println!(\"Hello\");\n}";
let styled = highlighter.highlight(code);
assert_eq!(styled.line_count(), 3);
}
#[test]
fn test_json_highlighter() {
let highlighter = JsonHighlighter::new();
let styled = highlighter.highlight(r#"{"key": "value", "num": 42}"#);
assert_eq!(styled.line_count(), 1);
assert!(!styled.lines[0].spans.is_empty());
}
#[test]
fn test_empty_line() {
let highlighter = PlainHighlighter::new();
let styled = highlighter.highlight("");
assert_eq!(styled.line_count(), 1);
assert!(styled.lines[0].spans.is_empty());
}
}