use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
};
pub fn detect_language(file_path: &str) -> Option<Language> {
let extension = file_path.split('.').next_back()?;
match extension {
"rs" => Some(Language::Rust),
"js" | "jsx" | "ts" | "tsx" => Some(Language::JavaScript),
"py" => Some(Language::Python),
"go" => Some(Language::Go),
"java" => Some(Language::Java),
"c" | "h" => Some(Language::C),
"cpp" | "cc" | "cxx" | "hpp" => Some(Language::Cpp),
"json" => Some(Language::Json),
"toml" => Some(Language::Toml),
"yaml" | "yml" => Some(Language::Yaml),
"md" => Some(Language::Markdown),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
pub enum Language {
Rust,
JavaScript,
Python,
Go,
Java,
C,
Cpp,
Json,
Toml,
Yaml,
Markdown,
}
impl Language {
fn keywords(&self) -> &[&str] {
match self {
Language::Rust => &[
"fn", "let", "mut", "const", "static", "impl", "trait", "struct", "enum", "pub",
"use", "mod", "crate", "super", "self", "if", "else", "match", "for", "while",
"loop", "break", "continue", "return", "async", "await",
],
Language::JavaScript => &[
"function", "const", "let", "var", "if", "else", "for", "while", "return", "class",
"extends", "import", "export", "async", "await", "try", "catch",
],
Language::Python => &[
"def", "class", "if", "elif", "else", "for", "while", "return", "import", "from",
"as", "try", "except", "with", "lambda", "async", "await",
],
Language::Go => &[
"func",
"var",
"const",
"if",
"else",
"for",
"range",
"return",
"struct",
"interface",
"type",
"package",
"import",
"defer",
"go",
"chan",
],
_ => &[],
}
}
fn types(&self) -> &[&str] {
match self {
Language::Rust => &[
"String", "str", "i32", "i64", "u32", "u64", "f32", "f64", "bool", "usize",
"Option", "Result", "Vec",
],
Language::JavaScript | Language::Python => {
&["string", "number", "boolean", "object", "array"]
}
Language::Go => &["string", "int", "int64", "float64", "bool", "map", "slice"],
_ => &[],
}
}
}
pub fn highlight_code(code: &str, language: Option<Language>) -> Vec<Line<'static>> {
let mut lines = vec![];
for line in code.lines() {
if let Some(lang) = language {
lines.push(highlight_line(line, lang));
} else {
lines.push(Line::from(line.to_string()));
}
}
lines
}
fn highlight_line(line: &str, language: Language) -> Line<'static> {
let keywords = language.keywords();
let types = language.types();
let mut spans = vec![];
let mut current = String::new();
let mut in_string = false;
let mut string_char = ' ';
for ch in line.chars() {
if ch == '"' || ch == '\'' {
if !in_string {
if !current.is_empty() {
spans.push(word_to_span(¤t, keywords, types));
current.clear();
}
in_string = true;
string_char = ch;
current.push(ch);
} else if ch == string_char {
current.push(ch);
spans.push(Span::styled(
current.clone(),
Style::default().fg(Color::Green),
));
current.clear();
in_string = false;
} else {
current.push(ch);
}
} else if in_string {
current.push(ch);
} else if ch.is_whitespace()
|| ch == '('
|| ch == ')'
|| ch == '{'
|| ch == '}'
|| ch == '['
|| ch == ']'
|| ch == ';'
|| ch == ','
|| ch == ':'
{
if !current.is_empty() {
spans.push(word_to_span(¤t, keywords, types));
current.clear();
}
spans.push(Span::raw(ch.to_string()));
} else {
current.push(ch);
}
}
if !current.is_empty() {
if in_string {
spans.push(Span::styled(current, Style::default().fg(Color::Green)));
} else {
spans.push(word_to_span(¤t, keywords, types));
}
}
Line::from(spans)
}
fn word_to_span(word: &str, keywords: &[&str], types: &[&str]) -> Span<'static> {
if keywords.contains(&word) {
Span::styled(
word.to_string(),
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
)
} else if types.contains(&word) {
Span::styled(word.to_string(), Style::default().fg(Color::Cyan))
} else if word.starts_with("//") {
Span::styled(word.to_string(), Style::default().fg(Color::DarkGray))
} else if word.chars().all(|c| c.is_numeric() || c == '.') {
Span::styled(word.to_string(), Style::default().fg(Color::Yellow))
} else {
Span::raw(word.to_string())
}
}
pub fn render_edit_result(content: &str) -> Option<Vec<Line<'static>>> {
let json: serde_json::Value = serde_json::from_str(content).ok()?;
let mut lines = vec![];
if let Some(file_path) = json.get("file_path").and_then(|v| v.as_str()) {
lines.push(Line::from(vec![
Span::styled(
" File: ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
), Span::styled(file_path.to_string(), Style::default().fg(Color::Yellow)),
]));
lines.push(Line::from(""));
let language = detect_language(file_path);
if let Some(old_string) = json.get("old_string").and_then(|v| v.as_str()) {
lines.push(Line::from(Span::styled(
" Removed:", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
for line in highlight_code(old_string, language) {
let mut spans = vec![Span::styled(" - ", Style::default().fg(Color::Red))];
spans.extend(line.spans);
lines.push(Line::from(spans));
}
lines.push(Line::from(""));
}
if let Some(new_string) = json.get("new_string").and_then(|v| v.as_str()) {
lines.push(Line::from(Span::styled(
" Added:", Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
for line in highlight_code(new_string, language) {
let mut spans = vec![Span::styled(" + ", Style::default().fg(Color::Green))];
spans.extend(line.spans);
lines.push(Line::from(spans));
}
}
return Some(lines);
}
None
}