use crate::style::Style;
#[derive(Clone, Debug, PartialEq)]
pub struct HighlightSpan {
pub start_col: usize,
pub end_col: usize,
pub style: Style,
}
pub trait Highlighter {
fn highlight_line(&self, line_idx: usize, text: &str) -> Vec<HighlightSpan>;
fn on_edit(&mut self, line_idx: usize);
}
#[derive(Clone, Debug, Default)]
pub struct NoHighlighter;
impl Highlighter for NoHighlighter {
fn highlight_line(&self, _line_idx: usize, _text: &str) -> Vec<HighlightSpan> {
Vec::new()
}
fn on_edit(&mut self, _line_idx: usize) {}
}
#[derive(Clone, Debug)]
pub struct SimpleKeywordHighlighter {
keywords: Vec<(String, Style)>,
}
impl SimpleKeywordHighlighter {
pub fn new(keywords: Vec<(String, Style)>) -> Self {
Self { keywords }
}
}
impl Highlighter for SimpleKeywordHighlighter {
fn highlight_line(&self, _line_idx: usize, text: &str) -> Vec<HighlightSpan> {
let mut spans = Vec::new();
for (keyword, style) in &self.keywords {
let mut search_start = 0;
while let Some(byte_idx) = text[search_start..].find(keyword.as_str()) {
let abs_byte_idx = search_start + byte_idx;
let start_col = text[..abs_byte_idx].chars().count();
let end_col = start_col + keyword.chars().count();
spans.push(HighlightSpan {
start_col,
end_col,
style: style.clone(),
});
search_start = abs_byte_idx + keyword.len();
}
}
spans.sort_by_key(|s| s.start_col);
spans
}
fn on_edit(&mut self, _line_idx: usize) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_highlighter_returns_empty() {
let h = NoHighlighter;
let spans = h.highlight_line(0, "hello world");
assert!(spans.is_empty());
}
#[test]
fn keyword_highlighter_finds_keyword() {
let h = SimpleKeywordHighlighter::new(vec![("fn".to_string(), Style::new().bold(true))]);
let spans = h.highlight_line(0, "fn main() {}");
assert!(spans.len() == 1);
assert!(spans[0].start_col == 0);
assert!(spans[0].end_col == 2);
assert!(spans[0].style.bold);
}
#[test]
fn multiple_keywords_same_line() {
let h = SimpleKeywordHighlighter::new(vec![
("let".to_string(), Style::new().bold(true)),
("mut".to_string(), Style::new().italic(true)),
]);
let spans = h.highlight_line(0, "let mut x = 5;");
assert!(spans.len() == 2);
assert!(spans[0].start_col == 0);
assert!(spans[0].end_col == 3);
assert!(spans[1].start_col == 4);
assert!(spans[1].end_col == 7);
}
#[test]
fn no_match_returns_empty() {
let h = SimpleKeywordHighlighter::new(vec![("class".to_string(), Style::new().bold(true))]);
let spans = h.highlight_line(0, "fn main() {}");
assert!(spans.is_empty());
}
#[test]
fn partial_match_not_highlighted() {
let h = SimpleKeywordHighlighter::new(vec![("fn".to_string(), Style::new().bold(true))]);
let spans = h.highlight_line(0, "function");
assert!(spans.is_empty());
}
#[test]
fn unicode_keyword_matching() {
let h = SimpleKeywordHighlighter::new(vec![("日本".to_string(), Style::new().bold(true))]);
let spans = h.highlight_line(0, "hello 日本語 world");
assert!(spans.len() == 1);
assert!(spans[0].start_col == 6);
assert!(spans[0].end_col == 8);
}
#[test]
fn multiple_occurrences_of_keyword() {
let h = SimpleKeywordHighlighter::new(vec![("ab".to_string(), Style::new().bold(true))]);
let spans = h.highlight_line(0, "ab cd ab");
assert!(spans.len() == 2);
assert!(spans[0].start_col == 0);
assert!(spans[1].start_col == 6);
}
#[test]
fn on_edit_no_panic() {
let mut h = NoHighlighter;
h.on_edit(0);
let mut kh =
SimpleKeywordHighlighter::new(vec![("x".to_string(), Style::new().bold(true))]);
kh.on_edit(5);
}
}