ghostscope_ui/components/command_panel/
syntax_highlighter.rs1use ratatui::{
2 style::{Color, Style},
3 text::Span,
4};
5use regex::Regex;
6
7pub struct SyntaxHighlighter {
9 keywords_regex: Regex,
11 string_regex: Regex,
12 comment_regex: Regex,
13 number_regex: Regex,
14 hex_address_regex: Regex,
15 special_var_regex: Regex,
16 operator_regex: Regex,
17}
18
19impl SyntaxHighlighter {
20 pub fn new() -> Self {
21 Self {
22 keywords_regex: Regex::new(r"\b(trace|print|backtrace|bt|if|else|let)\b").unwrap(),
24
25 string_regex: Regex::new(r#""([^"\\]|\\.)*""#).unwrap(),
27
28 comment_regex: Regex::new(r"(//.*)|(/\*[\s\S]*?\*/)").unwrap(),
30
31 number_regex: Regex::new(r"\b\d+(\.\d+)?\b").unwrap(),
33
34 hex_address_regex: Regex::new(r"\b0x[0-9a-fA-F]+\b").unwrap(),
36
37 special_var_regex: Regex::new(r"\$[a-zA-Z_][a-zA-Z0-9_]*").unwrap(),
39
40 operator_regex: Regex::new(r"(==|!=|<=|>=|[+\-*/=<>])").unwrap(),
42 }
43 }
44
45 pub fn highlight_line(&self, line: &str) -> Vec<Span<'static>> {
47 let mut spans = Vec::new();
48 let mut last_end = 0;
49
50 let mut matches = Vec::new();
52
53 self.collect_matches(line, &mut matches);
55
56 matches.sort_by_key(|m| m.start);
58
59 let mut filtered_matches = Vec::new();
61 let mut last_match_end = 0;
62
63 for m in matches {
64 if m.start >= last_match_end {
65 filtered_matches.push(m.clone());
66 last_match_end = m.end;
67 }
68 }
69
70 for m in filtered_matches {
72 if m.start > last_end {
74 let text = &line[last_end..m.start];
75 if !text.is_empty() {
76 spans.push(Span::styled(
77 text.to_string(),
78 Style::default().fg(Color::White),
79 ));
80 }
81 }
82
83 let text = &line[m.start..m.end];
85 spans.push(Span::styled(text.to_string(), m.style));
86
87 last_end = m.end;
88 }
89
90 if last_end < line.len() {
92 let text = &line[last_end..];
93 spans.push(Span::styled(
94 text.to_string(),
95 Style::default().fg(Color::White),
96 ));
97 }
98
99 if spans.is_empty() {
101 spans.push(Span::styled(
102 line.to_string(),
103 Style::default().fg(Color::White),
104 ));
105 }
106
107 spans
108 }
109
110 fn collect_matches(&self, line: &str, matches: &mut Vec<Match>) {
112 for m in self.comment_regex.find_iter(line) {
114 matches.push(Match {
115 start: m.start(),
116 end: m.end(),
117 style: Style::default().fg(Color::DarkGray),
118 });
119 }
120
121 for m in self.string_regex.find_iter(line) {
123 matches.push(Match {
124 start: m.start(),
125 end: m.end(),
126 style: Style::default().fg(Color::Green),
127 });
128 }
129
130 for m in self.hex_address_regex.find_iter(line) {
132 matches.push(Match {
133 start: m.start(),
134 end: m.end(),
135 style: Style::default().fg(Color::Magenta),
136 });
137 }
138
139 for m in self.number_regex.find_iter(line) {
141 matches.push(Match {
142 start: m.start(),
143 end: m.end(),
144 style: Style::default().fg(Color::Magenta),
145 });
146 }
147
148 for m in self.keywords_regex.find_iter(line) {
150 matches.push(Match {
151 start: m.start(),
152 end: m.end(),
153 style: Style::default().fg(Color::Blue),
154 });
155 }
156
157 for m in self.special_var_regex.find_iter(line) {
159 matches.push(Match {
160 start: m.start(),
161 end: m.end(),
162 style: Style::default().fg(Color::Cyan),
163 });
164 }
165
166 for m in self.operator_regex.find_iter(line) {
168 matches.push(Match {
169 start: m.start(),
170 end: m.end(),
171 style: Style::default().fg(Color::Yellow),
172 });
173 }
174 }
175}
176
177impl Default for SyntaxHighlighter {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183#[derive(Clone)]
185struct Match {
186 start: usize,
187 end: usize,
188 style: Style,
189}
190
191pub fn highlight_line(line: &str) -> Vec<Span<'static>> {
193 use std::sync::OnceLock;
195 static HIGHLIGHTER: OnceLock<SyntaxHighlighter> = OnceLock::new();
196
197 let highlighter = HIGHLIGHTER.get_or_init(SyntaxHighlighter::new);
198 highlighter.highlight_line(line)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_keyword_highlighting() {
207 let highlighter = SyntaxHighlighter::new();
208 let spans = highlighter.highlight_line("trace calculate_something {");
209
210 assert!(spans.iter().any(|span| span.style.fg == Some(Color::Blue)));
212 }
213
214 #[test]
215 fn test_string_highlighting() {
216 let highlighter = SyntaxHighlighter::new();
217 let spans = highlighter.highlight_line(r#"print "Hello, world!";"#);
218
219 assert!(spans.iter().any(|span| span.style.fg == Some(Color::Green)));
221 assert!(spans.iter().any(|span| span.style.fg == Some(Color::Blue)));
223 }
224
225 #[test]
226 fn test_comment_highlighting() {
227 let highlighter = SyntaxHighlighter::new();
228 let spans = highlighter.highlight_line("// This is a comment");
229
230 assert!(spans
232 .iter()
233 .any(|span| span.style.fg == Some(Color::DarkGray)));
234 }
235
236 #[test]
237 fn test_number_highlighting() {
238 let highlighter = SyntaxHighlighter::new();
239 let spans = highlighter.highlight_line("let x = 42;");
240
241 assert!(spans
243 .iter()
244 .any(|span| span.style.fg == Some(Color::Magenta)));
245 }
246
247 #[test]
248 fn test_hex_address_highlighting() {
249 let highlighter = SyntaxHighlighter::new();
250 let spans = highlighter.highlight_line("trace 0x1234abcd {");
251
252 assert!(spans
254 .iter()
255 .any(|span| span.style.fg == Some(Color::Magenta)));
256 }
257
258 #[test]
259 fn test_special_variable_highlighting() {
260 let highlighter = SyntaxHighlighter::new();
261 let spans = highlighter.highlight_line("print $pid;");
262
263 assert!(spans.iter().any(|span| span.style.fg == Some(Color::Cyan)));
265 }
266
267 #[test]
268 fn test_operator_highlighting() {
269 let highlighter = SyntaxHighlighter::new();
270 let spans = highlighter.highlight_line("if a == b {");
271
272 assert!(spans
274 .iter()
275 .any(|span| span.style.fg == Some(Color::Yellow)));
276 }
277
278 #[test]
279 fn test_complex_line() {
280 let highlighter = SyntaxHighlighter::new();
281 let spans = highlighter
282 .highlight_line(r#"print "Value: {} at 0x{:x}", value, 0x1000; // Debug output"#);
283
284 let colors: std::collections::HashSet<_> =
286 spans.iter().filter_map(|span| span.style.fg).collect();
287
288 assert!(colors.len() >= 3);
290 }
291}