1use crate::expression::parser::parse_line_reference;
4use crate::units::parse_unit;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct HighlightedSpan {
10 pub text: String,
11 pub highlight_type: HighlightType,
12}
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum HighlightType {
17 Number,
19 Unit,
21 LineReference,
23 Keyword,
25 Operator,
27 Variable,
29 Function,
31 Normal,
33}
34
35impl HighlightType {
36 pub fn rgb_color(&self) -> (u8, u8, u8) {
39 match self {
40 HighlightType::Number => (173, 216, 230), HighlightType::Unit => (144, 238, 144), HighlightType::LineReference => (221, 160, 221), HighlightType::Keyword => (255, 255, 0), HighlightType::Operator => (0, 255, 255), HighlightType::Variable => (224, 255, 255), HighlightType::Function => (0, 255, 255), HighlightType::Normal => (200, 200, 200), }
49 }
50}
51
52pub fn highlight_expression(
54 text: &str,
55 variables: &HashMap<String, String>,
56) -> Vec<HighlightedSpan> {
57 let mut spans = Vec::new();
58 let mut current_pos = 0;
59 let chars: Vec<char> = text.chars().collect();
60
61 while current_pos < chars.len() {
62 if chars[current_pos].is_ascii_alphabetic() {
63 let start_pos = current_pos;
65
66 while current_pos < chars.len()
67 && (chars[current_pos].is_ascii_alphabetic()
68 || chars[current_pos].is_ascii_digit()
69 || chars[current_pos] == '_')
70 {
71 current_pos += 1;
72 }
73
74 let word_text: String = chars[start_pos..current_pos].iter().collect();
75
76 let highlight_type = if parse_line_reference(&word_text).is_some() {
78 HighlightType::LineReference
79 } else if word_text.to_lowercase() == "to"
80 || word_text.to_lowercase() == "in"
81 || word_text.to_lowercase() == "of"
82 {
83 HighlightType::Keyword
84 } else if word_text.to_lowercase() == "sqrt" || word_text.to_lowercase() == "sum_above"
85 {
86 HighlightType::Function
87 } else if parse_unit(&word_text).is_some() {
88 HighlightType::Unit
89 } else if variables.contains_key(&word_text) {
90 HighlightType::Variable
91 } else {
92 HighlightType::Normal
93 };
94
95 spans.push(HighlightedSpan {
96 text: word_text,
97 highlight_type,
98 });
99 } else if chars[current_pos].is_ascii_digit() || chars[current_pos] == '.' {
100 let start_pos = current_pos;
102 let mut has_digit = false;
103 let mut has_dot = false;
104
105 while current_pos < chars.len() {
106 let ch = chars[current_pos];
107 if ch.is_ascii_digit() {
108 has_digit = true;
109 current_pos += 1;
110 } else if ch == '.' && !has_dot {
111 has_dot = true;
112 current_pos += 1;
113 } else if ch == ',' {
114 current_pos += 1;
115 } else {
116 break;
117 }
118 }
119
120 let number_text: String = chars[start_pos..current_pos].iter().collect();
121
122 if has_digit {
123 spans.push(HighlightedSpan {
124 text: number_text,
125 highlight_type: HighlightType::Number,
126 });
127 } else {
128 spans.push(HighlightedSpan {
129 text: number_text,
130 highlight_type: HighlightType::Normal,
131 });
132 current_pos = start_pos + 1;
133 }
134 } else if chars[current_pos] == '%' {
135 spans.push(HighlightedSpan {
137 text: "%".to_string(),
138 highlight_type: HighlightType::Unit,
139 });
140 current_pos += 1;
141 } else if "$€£¥₹₩".contains(chars[current_pos]) {
142 spans.push(HighlightedSpan {
144 text: chars[current_pos].to_string(),
145 highlight_type: HighlightType::Unit,
146 });
147 current_pos += 1;
148 } else if "+-*/()=^".contains(chars[current_pos]) {
149 spans.push(HighlightedSpan {
151 text: chars[current_pos].to_string(),
152 highlight_type: HighlightType::Operator,
153 });
154 current_pos += 1;
155 } else {
156 spans.push(HighlightedSpan {
158 text: chars[current_pos].to_string(),
159 highlight_type: HighlightType::Normal,
160 });
161 current_pos += 1;
162 }
163 }
164
165 spans
166}
167
168pub fn highlight_expression_with_cursor(
171 text: &str,
172 cursor_col: usize,
173 variables: &HashMap<String, String>,
174) -> (Vec<HighlightedSpan>, usize) {
175 let spans = highlight_expression(text, variables);
176 (spans, cursor_col)
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_number_highlighting() {
187 let variables = HashMap::new();
188 let spans = highlight_expression("123.45", &variables);
189
190 assert_eq!(spans.len(), 1);
191 assert_eq!(spans[0].text, "123.45");
192 assert_eq!(spans[0].highlight_type, HighlightType::Number);
193 }
194
195 #[test]
196 fn test_operator_highlighting() {
197 let variables = HashMap::new();
198 let spans = highlight_expression("5 + 3", &variables);
199
200 assert_eq!(spans.len(), 5); assert_eq!(spans[0].highlight_type, HighlightType::Number);
202 assert_eq!(spans[1].highlight_type, HighlightType::Normal); assert_eq!(spans[2].highlight_type, HighlightType::Operator);
204 assert_eq!(spans[3].highlight_type, HighlightType::Normal); assert_eq!(spans[4].highlight_type, HighlightType::Number);
206 }
207
208 #[test]
209 fn test_unit_highlighting() {
210 let variables = HashMap::new();
211 let spans = highlight_expression("100 GB", &variables);
212
213 assert_eq!(spans.len(), 3); assert_eq!(spans[0].highlight_type, HighlightType::Number);
215 assert_eq!(spans[1].highlight_type, HighlightType::Normal); assert_eq!(spans[2].highlight_type, HighlightType::Unit);
217 }
218
219 #[test]
220 fn test_line_reference_highlighting() {
221 let variables = HashMap::new();
222 let spans = highlight_expression("line1 + 5", &variables);
223
224 assert!(
225 spans
226 .iter()
227 .any(|s| s.highlight_type == HighlightType::LineReference)
228 );
229 assert!(spans.iter().any(|s| s.text == "line1"));
230 }
231
232 #[test]
233 fn test_variable_highlighting() {
234 let mut variables = HashMap::new();
235 variables.insert("x".to_string(), "42".to_string());
236
237 let spans = highlight_expression("x * 2", &variables);
238
239 assert!(
240 spans
241 .iter()
242 .any(|s| s.highlight_type == HighlightType::Variable)
243 );
244 assert!(spans.iter().any(|s| s.text == "x"));
245 }
246
247 #[test]
248 fn test_keyword_highlighting() {
249 let variables = HashMap::new();
250 let spans = highlight_expression("100 GB to MB", &variables);
251
252 assert!(
253 spans
254 .iter()
255 .any(|s| s.highlight_type == HighlightType::Keyword)
256 );
257 assert!(spans.iter().any(|s| s.text == "to"));
258 }
259
260 #[test]
261 fn test_function_highlighting() {
262 let variables = HashMap::new();
263 let spans = highlight_expression("sqrt(16)", &variables);
264
265 assert!(
266 spans
267 .iter()
268 .any(|s| s.highlight_type == HighlightType::Function)
269 );
270 assert!(spans.iter().any(|s| s.text == "sqrt"));
271
272 let spans = highlight_expression("sum_above()", &variables);
274 assert!(
275 spans
276 .iter()
277 .any(|s| s.highlight_type == HighlightType::Function)
278 );
279 assert!(spans.iter().any(|s| s.text == "sum_above"));
280 }
281}