use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
pub fn highlight_code_line(line: &str) -> Vec<Span<'static>> {
let mut spans = Vec::new();
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
let n = chars.len();
let peek_next_non_ws = |mut idx: usize| -> Option<char> {
while idx < n {
if !chars[idx].is_whitespace() {
return Some(chars[idx]);
}
idx += 1;
}
None
};
while i < n {
if chars[i] == '/' && i + 1 < n && chars[i + 1] == '*' {
let mut s = String::new();
s.push('/');
s.push('*');
i += 2;
while i < n {
s.push(chars[i]);
if chars[i] == '*' && i + 1 < n && chars[i + 1] == '/' {
s.push('/');
i += 2;
break;
}
i += 1;
}
spans.push(Span::styled(s, Style::default().fg(Color::DarkGray)));
continue;
}
if chars[i] == '#' {
let mut is_directive = false;
let mut j = i + 1;
while j < n && chars[j].is_whitespace() {
j += 1;
}
if j < n && chars[j].is_alphabetic() {
let mut word = String::new();
while j < n && chars[j].is_alphabetic() {
word.push(chars[j]);
j += 1;
}
if matches!(
word.as_str(),
"include"
| "define"
| "undef"
| "ifdef"
| "ifndef"
| "if"
| "else"
| "elif"
| "endif"
| "error"
| "pragma"
| "import"
) {
is_directive = true;
}
}
if is_directive {
let mut directive_text = String::new();
directive_text.push('#');
i += 1;
while i < n && (chars[i].is_alphanumeric() || chars[i].is_whitespace()) {
directive_text.push(chars[i]);
i += 1;
}
spans.push(Span::styled(
directive_text,
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
continue;
} else {
let comment_text: String = chars[i..].iter().collect();
spans.push(Span::styled(
comment_text,
Style::default().fg(Color::DarkGray),
));
break;
}
}
if chars[i] == '/' && i + 1 < n && chars[i + 1] == '/' {
let comment_text: String = chars[i..].iter().collect();
spans.push(Span::styled(
comment_text,
Style::default().fg(Color::DarkGray),
));
break;
}
if chars[i] == '"' || chars[i] == '\'' || chars[i] == '`' {
let quote = chars[i];
let mut s = String::new();
s.push(quote);
i += 1;
let mut escaped = false;
while i < n {
let c = chars[i];
s.push(c);
i += 1;
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == quote {
break;
}
}
if quote == '"' && peek_next_non_ws(i) == Some(':') {
spans.push(Span::styled(s, Style::default().fg(Color::Blue)));
} else {
spans.push(Span::styled(s, Style::default().fg(Color::Green)));
}
continue;
}
if chars[i] == '@' {
let mut decorator = String::new();
decorator.push('@');
i += 1;
while i < n && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '.') {
decorator.push(chars[i]);
i += 1;
}
spans.push(Span::styled(decorator, Style::default().fg(Color::Magenta)));
continue;
}
if chars[i].is_alphabetic() || chars[i] == '_' {
let mut word = String::new();
while i < n && (chars[i].is_alphanumeric() || chars[i] == '_') {
word.push(chars[i]);
i += 1;
}
if is_keyword(&word) {
spans.push(Span::styled(
word,
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
} else if is_built_in_type(&word) {
spans.push(Span::styled(word, Style::default().fg(Color::Blue)));
} else {
spans.push(Span::styled(word, Style::default().fg(Color::Cyan)));
}
continue;
}
if chars[i].is_ascii_digit() {
let mut num = String::new();
while i < n
&& (chars[i].is_ascii_digit() || chars[i] == '.' || chars[i].is_alphabetic())
{
num.push(chars[i]);
i += 1;
}
spans.push(Span::styled(num, Style::default().fg(Color::Yellow)));
continue;
}
let mut other = String::new();
while i < n {
let c = chars[i];
if c.is_alphabetic()
|| c == '_'
|| c.is_ascii_digit()
|| c == '"'
|| c == '\''
|| c == '`'
|| c == '#'
|| c == '@'
|| (c == '/' && i + 1 < n && (chars[i + 1] == '/' || chars[i + 1] == '*'))
{
break;
}
other.push(c);
i += 1;
}
if !other.is_empty() {
spans.push(Span::styled(other, Style::default().fg(Color::Cyan)));
}
}
spans
}
fn is_keyword(word: &str) -> bool {
matches!(
word,
"fn" | "let"
| "mut"
| "pub"
| "struct"
| "impl"
| "enum"
| "use"
| "mod"
| "match"
| "if"
| "else"
| "for"
| "while"
| "loop"
| "in"
| "return"
| "break"
| "continue"
| "const"
| "static"
| "class"
| "def"
| "import"
| "as"
| "from"
| "try"
| "except"
| "finally"
| "with"
| "self"
| "true"
| "false"
| "None"
| "null"
| "var"
| "function"
| "new"
| "typeof"
| "instanceof"
| "switch"
| "case"
| "default"
| "type"
| "interface"
| "package"
| "func"
| "go"
| "select"
| "chan"
| "nil"
| "then"
| "fi"
| "done"
| "do"
| "elif"
| "until"
| "local"
| "export"
| "echo"
| "printf"
| "exit"
| "alias"
| "read"
| "map"
| "range"
)
}
fn is_built_in_type(word: &str) -> bool {
matches!(
word,
"i8" | "i16"
| "i32"
| "i64"
| "i128"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "isize"
| "usize"
| "f32"
| "f64"
| "str"
| "String"
| "Option"
| "Result"
| "Some"
| "None"
| "Ok"
| "Err"
| "bool"
| "char"
| "int"
| "float"
| "double"
| "void"
| "string"
| "vector"
| "map"
| "list"
| "set"
| "Vec"
| "HashMap"
| "BTreeMap"
| "HashSet"
| "BTreeSet"
| "Box"
| "Rc"
| "Arc"
| "nil"
| "null"
| "undefined"
| "error"
| "int64"
| "float64"
)
}
pub fn parse_markdown_lines(text: &str) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let mut in_code_block = false;
let mut is_diff = false;
for raw_line in text.split('\n') {
let trimmed = raw_line.trim_start();
if trimmed.starts_with("```") {
in_code_block = !in_code_block;
if in_code_block && trimmed.ends_with("diff") {
is_diff = true;
} else if !in_code_block {
is_diff = false;
}
lines.push(Line::from(Span::styled(
raw_line.to_owned(),
Style::default().fg(Color::DarkGray),
)));
continue;
}
if in_code_block {
if is_diff {
let mut style = Style::default();
let mut prefix = "";
if raw_line.starts_with('+') {
style = Style::default().fg(Color::Black).bg(Color::Green);
prefix = " ";
} else if raw_line.starts_with('-') {
style = Style::default().fg(Color::Black).bg(Color::Red);
prefix = " ";
} else if raw_line.starts_with("@@") {
style = Style::default().fg(Color::DarkGray);
}
lines.push(Line::from(Span::styled(
format!("{prefix}{raw_line}"),
style,
)));
} else {
lines.push(Line::from(highlight_code_line(raw_line)));
}
continue;
}
if trimmed.starts_with("# ") {
lines.push(Line::from(Span::styled(
raw_line.to_owned(),
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED)
.fg(Color::Yellow),
)));
} else if trimmed.starts_with("## ") || trimmed.starts_with("### ") {
lines.push(Line::from(Span::styled(
raw_line.to_owned(),
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Yellow),
)));
} else if trimmed.starts_with("* ")
|| trimmed.starts_with("- ")
|| (trimmed.chars().next().is_some_and(|c| c.is_ascii_digit())
&& trimmed.contains(". "))
{
let mut spans = parse_inline_markdown(raw_line);
if !spans.is_empty() {
spans[0].style = spans[0]
.style
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
}
lines.push(Line::from(spans));
} else {
lines.push(Line::from(parse_inline_markdown(raw_line)));
}
}
lines
}
fn parse_inline_markdown(line: &str) -> Vec<Span<'static>> {
let mut spans = Vec::new();
let mut current = String::new();
let mut chars = line.chars().peekable();
let mut is_bold = false;
let mut is_code = false;
while let Some(c) = chars.next() {
if c == '`' {
if !current.is_empty() {
spans.push(Span::styled(current.clone(), style_for(is_bold, is_code)));
current.clear();
}
is_code = !is_code;
} else if c == '*' && chars.peek() == Some(&'*') {
chars.next(); if !current.is_empty() {
spans.push(Span::styled(current.clone(), style_for(is_bold, is_code)));
current.clear();
}
is_bold = !is_bold;
} else {
current.push(c);
}
}
if !current.is_empty() {
spans.push(Span::styled(current, style_for(is_bold, is_code)));
}
spans
}
fn style_for(is_bold: bool, is_code: bool) -> Style {
let mut style = Style::default();
if is_bold {
style = style.add_modifier(Modifier::BOLD);
}
if is_code {
style = style.fg(Color::Cyan);
}
style
}
pub fn wrap_spans(spans: Vec<Span<'static>>, max_width: usize) -> Vec<Vec<Span<'static>>> {
let mut wrapped_lines = Vec::new();
let mut current_line = Vec::new();
let mut current_width = 0;
for span in spans {
let text = span.content.as_ref();
let mut char_idx = 0;
let chars: Vec<char> = text.chars().collect();
while char_idx < chars.len() {
let remaining_width = max_width.saturating_sub(current_width);
if remaining_width == 0 {
wrapped_lines.push(current_line);
current_line = Vec::new();
current_width = 0;
continue;
}
let take = (chars.len() - char_idx).min(remaining_width);
let chunk: String = chars[char_idx..char_idx + take].iter().collect();
current_line.push(Span::styled(chunk, span.style));
current_width += take;
char_idx += take;
}
}
if !current_line.is_empty() {
wrapped_lines.push(current_line);
}
if wrapped_lines.is_empty() {
wrapped_lines.push(Vec::new());
}
wrapped_lines
}
pub fn wrap_line(line: Line<'static>, max_width: usize) -> Vec<Line<'static>> {
let spans: Vec<Span<'static>> = line.spans.into_iter().collect();
wrap_spans(spans, max_width)
.into_iter()
.map(Line::from)
.collect()
}
pub fn wrap_lines(lines: Vec<Line<'static>>, max_width: usize) -> Vec<Line<'static>> {
let mut wrapped = Vec::new();
for line in lines {
wrapped.extend(wrap_line(line, max_width));
}
wrapped
}
pub fn wrap_text_to_lines(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
for line in text.split('\n') {
if line.is_empty() {
lines.push(String::new());
} else {
let chars: Vec<char> = line.chars().collect();
for chunk in chars.chunks(width) {
lines.push(chunk.iter().collect());
}
}
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rich_highlighting() {
let spans = highlight_code_line("\"key\": \"value\"");
assert_eq!(spans[0].content, "\"key\"");
assert_eq!(spans[0].style.fg, Some(Color::Blue));
assert_eq!(spans[2].content, "\"value\"");
assert_eq!(spans[2].style.fg, Some(Color::Green));
let spans_types = highlight_code_line("let x: usize = 42;");
let usize_span = spans_types.iter().find(|s| s.content == "usize").unwrap();
assert_eq!(usize_span.style.fg, Some(Color::Blue));
let spans_decorator = highlight_code_line("@app.route('/')");
assert_eq!(spans_decorator[0].content, "@app.route");
assert_eq!(spans_decorator[0].style.fg, Some(Color::Magenta)); }
#[test]
fn test_wrap_text_to_lines() {
let text = "hello\nworld";
let wrapped = wrap_text_to_lines(text, 3);
assert_eq!(wrapped, vec!["hel", "lo", "wor", "ld"]);
}
#[test]
fn test_parse_inline_markdown_bold() {
let spans = parse_inline_markdown("This is **bold** text");
let bold_span = spans.iter().find(|s| s.content == "bold").unwrap();
assert!(bold_span.style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn test_parse_inline_markdown_bold_and_code() {
let spans = parse_inline_markdown("This is **bold and `code`** text");
let code_span = spans.iter().find(|s| s.content == "code").unwrap();
assert!(code_span.style.add_modifier.contains(Modifier::BOLD));
assert_eq!(code_span.style.fg, Some(Color::Cyan));
}
#[test]
fn test_parse_inline_markdown_code() {
let spans = parse_inline_markdown("This is `code` text");
let code_span = spans.iter().find(|s| s.content == "code").unwrap();
assert_eq!(code_span.style.fg, Some(Color::Cyan));
}
}