use egui::{text::LayoutJob, Color32, FontId, TextFormat};
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, Theme, ThemeSet};
use syntect::parsing::{SyntaxSet, SyntaxSetBuilder};
use syntect::util::LinesWithEndings;
pub struct SyntaxHighlighter {
syntax_set: SyntaxSet,
theme: Theme,
}
impl SyntaxHighlighter {
pub fn new() -> Self {
let mut builder = SyntaxSetBuilder::new();
builder.add_plain_text_syntax();
let syntax_set = builder.build();
let theme = Self::load_theme();
Self { syntax_set, theme }
}
fn load_theme() -> Theme {
let ts = ThemeSet::load_defaults();
ts.themes["base16-ocean.dark"].clone()
}
pub fn load_syntax(&self, file_path: &str) -> &syntect::parsing::SyntaxReference {
let extension = std::path::Path::new(file_path)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
match extension {
"rs" => self
.syntax_set
.find_syntax_by_extension("rs")
.or_else(|| self.syntax_set.find_syntax_by_name("Rust"))
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()),
"md" => self
.syntax_set
.find_syntax_by_extension("md")
.or_else(|| self.syntax_set.find_syntax_by_name("Markdown"))
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()),
_ => self.syntax_set.find_syntax_plain_text(),
}
}
pub fn highlight(&self, text: &str, file_path: Option<&str>) -> LayoutJob {
let syntax = if let Some(path) = file_path {
self.load_syntax(path)
} else {
self.syntax_set.find_syntax_plain_text()
};
let mut layout_job = LayoutJob::default();
let mut highlighter = HighlightLines::new(syntax, &self.theme);
for line in LinesWithEndings::from(text) {
let ranges = highlighter
.highlight_line(line, &self.syntax_set)
.unwrap_or_default();
for (style, text_segment) in ranges {
let color = Self::style_to_color(style);
layout_job.append(
text_segment,
0.0,
TextFormat {
font_id: FontId::monospace(14.0),
color,
..Default::default()
},
);
}
}
layout_job
}
fn style_to_color(style: Style) -> Color32 {
Color32::from_rgb(style.foreground.r, style.foreground.g, style.foreground.b)
}
#[allow(dead_code)]
pub fn supported_extensions() -> Vec<&'static str> {
vec!["rs", "md", "txt"]
}
}
impl Default for SyntaxHighlighter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_syntax_rust() {
let highlighter = SyntaxHighlighter::new();
let syntax = highlighter.load_syntax("foo.rs");
assert!(syntax.name == "Plain Text" || syntax.name.contains("Rust"));
}
#[test]
fn test_load_syntax_markdown() {
let highlighter = SyntaxHighlighter::new();
let syntax = highlighter.load_syntax("foo.md");
assert!(syntax.name == "Plain Text" || syntax.name.contains("Markdown"));
}
#[test]
fn test_load_syntax_unknown() {
let highlighter = SyntaxHighlighter::new();
let syntax = highlighter.load_syntax("foo.xyz");
assert_eq!(syntax.name, "Plain Text");
}
#[test]
fn test_highlight_rust_code() {
let highlighter = SyntaxHighlighter::new();
let code = "let a = 1;";
let layout_job = highlighter.highlight(code, Some("test.rs"));
assert!(layout_job.text.contains("let"));
assert!(layout_job.text.contains("a"));
assert!(layout_job.text.contains("="));
assert!(layout_job.text.contains("1"));
}
#[test]
fn test_highlight_markdown() {
let highlighter = SyntaxHighlighter::new();
let markdown = "# Header\n\n**bold**";
let layout_job = highlighter.highlight(markdown, Some("test.md"));
assert!(layout_job.text.contains("# Header"));
assert!(layout_job.text.contains("**bold**"));
}
#[test]
fn test_highlight_plaintext() {
let highlighter = SyntaxHighlighter::new();
let text = "Hello, World!";
let layout_job = highlighter.highlight(text, Some("test.txt"));
assert_eq!(layout_job.text, text);
}
#[test]
fn test_supported_extensions() {
let extensions = SyntaxHighlighter::supported_extensions();
assert!(extensions.contains(&"rs"));
assert!(extensions.contains(&"md"));
assert!(extensions.contains(&"txt"));
}
}