1use crate::style::Style;
9
10#[derive(Clone, Debug, PartialEq)]
15pub struct HighlightSpan {
16 pub start_col: usize,
18 pub end_col: usize,
20 pub style: Style,
22}
23
24pub trait Highlighter {
30 fn highlight_line(&self, line_idx: usize, text: &str) -> Vec<HighlightSpan>;
35
36 fn on_edit(&mut self, line_idx: usize);
41}
42
43#[derive(Clone, Debug, Default)]
48pub struct NoHighlighter;
49
50impl Highlighter for NoHighlighter {
51 fn highlight_line(&self, _line_idx: usize, _text: &str) -> Vec<HighlightSpan> {
52 Vec::new()
53 }
54
55 fn on_edit(&mut self, _line_idx: usize) {}
56}
57
58#[derive(Clone, Debug)]
63pub struct SimpleKeywordHighlighter {
64 keywords: Vec<(String, Style)>,
65}
66
67impl SimpleKeywordHighlighter {
68 pub fn new(keywords: Vec<(String, Style)>) -> Self {
70 Self { keywords }
71 }
72}
73
74impl Highlighter for SimpleKeywordHighlighter {
75 fn highlight_line(&self, _line_idx: usize, text: &str) -> Vec<HighlightSpan> {
76 let mut spans = Vec::new();
77
78 for (keyword, style) in &self.keywords {
79 let mut search_start = 0;
80 while let Some(byte_idx) = text[search_start..].find(keyword.as_str()) {
81 let abs_byte_idx = search_start + byte_idx;
82 let start_col = text[..abs_byte_idx].chars().count();
84 let end_col = start_col + keyword.chars().count();
85 spans.push(HighlightSpan {
86 start_col,
87 end_col,
88 style: style.clone(),
89 });
90 search_start = abs_byte_idx + keyword.len();
91 }
92 }
93
94 spans.sort_by_key(|s| s.start_col);
96 spans
97 }
98
99 fn on_edit(&mut self, _line_idx: usize) {
100 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn no_highlighter_returns_empty() {
110 let h = NoHighlighter;
111 let spans = h.highlight_line(0, "hello world");
112 assert!(spans.is_empty());
113 }
114
115 #[test]
116 fn keyword_highlighter_finds_keyword() {
117 let h = SimpleKeywordHighlighter::new(vec![("fn".to_string(), Style::new().bold(true))]);
118 let spans = h.highlight_line(0, "fn main() {}");
119 assert!(spans.len() == 1);
120 assert!(spans[0].start_col == 0);
121 assert!(spans[0].end_col == 2);
122 assert!(spans[0].style.bold);
123 }
124
125 #[test]
126 fn multiple_keywords_same_line() {
127 let h = SimpleKeywordHighlighter::new(vec![
128 ("let".to_string(), Style::new().bold(true)),
129 ("mut".to_string(), Style::new().italic(true)),
130 ]);
131 let spans = h.highlight_line(0, "let mut x = 5;");
132 assert!(spans.len() == 2);
133 assert!(spans[0].start_col == 0);
135 assert!(spans[0].end_col == 3);
136 assert!(spans[1].start_col == 4);
137 assert!(spans[1].end_col == 7);
138 }
139
140 #[test]
141 fn no_match_returns_empty() {
142 let h = SimpleKeywordHighlighter::new(vec![("class".to_string(), Style::new().bold(true))]);
143 let spans = h.highlight_line(0, "fn main() {}");
144 assert!(spans.is_empty());
145 }
146
147 #[test]
148 fn partial_match_not_highlighted() {
149 let h = SimpleKeywordHighlighter::new(vec![("fn".to_string(), Style::new().bold(true))]);
151 let spans = h.highlight_line(0, "function");
152 assert!(spans.is_empty());
153 }
154
155 #[test]
156 fn unicode_keyword_matching() {
157 let h = SimpleKeywordHighlighter::new(vec![("日本".to_string(), Style::new().bold(true))]);
158 let spans = h.highlight_line(0, "hello 日本語 world");
159 assert!(spans.len() == 1);
160 assert!(spans[0].start_col == 6);
161 assert!(spans[0].end_col == 8);
162 }
163
164 #[test]
165 fn multiple_occurrences_of_keyword() {
166 let h = SimpleKeywordHighlighter::new(vec![("ab".to_string(), Style::new().bold(true))]);
167 let spans = h.highlight_line(0, "ab cd ab");
168 assert!(spans.len() == 2);
169 assert!(spans[0].start_col == 0);
170 assert!(spans[1].start_col == 6);
171 }
172
173 #[test]
174 fn on_edit_no_panic() {
175 let mut h = NoHighlighter;
176 h.on_edit(0);
177 let mut kh =
178 SimpleKeywordHighlighter::new(vec![("x".to_string(), Style::new().bold(true))]);
179 kh.on_edit(5);
180 }
181}