use crate::expression::parser::parse_line_reference;
use crate::units::parse_unit;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct HighlightedSpan {
pub text: String,
pub highlight_type: HighlightType,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HighlightType {
Number,
Unit,
LineReference,
Keyword,
Operator,
Variable,
Function,
Normal,
}
impl HighlightType {
pub fn rgb_color(&self) -> (u8, u8, u8) {
match self {
HighlightType::Number => (65, 105, 225), HighlightType::Unit => (34, 139, 34), HighlightType::LineReference => (255, 140, 0), HighlightType::Keyword => (178, 34, 34), HighlightType::Operator => (128, 0, 128), HighlightType::Variable => (72, 61, 139), HighlightType::Function => (138, 43, 226), HighlightType::Normal => (105, 105, 105), }
}
}
pub fn highlight_expression(
text: &str,
variables: &HashMap<String, String>,
) -> Vec<HighlightedSpan> {
let mut spans = Vec::new();
let mut current_pos = 0;
let chars: Vec<char> = text.chars().collect();
while current_pos < chars.len() {
if chars[current_pos].is_ascii_alphabetic() {
let start_pos = current_pos;
while current_pos < chars.len()
&& (chars[current_pos].is_ascii_alphabetic()
|| chars[current_pos].is_ascii_digit()
|| chars[current_pos] == '_')
{
current_pos += 1;
}
let word_text: String = chars[start_pos..current_pos].iter().collect();
let highlight_type = if parse_line_reference(&word_text).is_some() {
HighlightType::LineReference
} else if word_text.to_lowercase() == "to"
|| word_text.to_lowercase() == "in"
|| word_text.to_lowercase() == "of"
{
HighlightType::Keyword
} else if word_text.to_lowercase() == "sqrt" || word_text.to_lowercase() == "sum_above"
{
HighlightType::Function
} else if parse_unit(&word_text).is_some() {
HighlightType::Unit
} else if variables.contains_key(&word_text) {
HighlightType::Variable
} else {
HighlightType::Normal
};
spans.push(HighlightedSpan {
text: word_text,
highlight_type,
});
} else if chars[current_pos].is_ascii_digit() || chars[current_pos] == '.' {
let start_pos = current_pos;
let mut has_digit = false;
let mut has_dot = false;
while current_pos < chars.len() {
let ch = chars[current_pos];
if ch.is_ascii_digit() {
has_digit = true;
current_pos += 1;
} else if ch == '.' && !has_dot {
has_dot = true;
current_pos += 1;
} else if ch == ',' {
current_pos += 1;
} else {
break;
}
}
let number_text: String = chars[start_pos..current_pos].iter().collect();
if has_digit {
spans.push(HighlightedSpan {
text: number_text,
highlight_type: HighlightType::Number,
});
} else {
spans.push(HighlightedSpan {
text: number_text,
highlight_type: HighlightType::Normal,
});
current_pos = start_pos + 1;
}
} else if chars[current_pos] == '%' {
spans.push(HighlightedSpan {
text: "%".to_string(),
highlight_type: HighlightType::Unit,
});
current_pos += 1;
} else if "$€£¥₹₩".contains(chars[current_pos]) {
spans.push(HighlightedSpan {
text: chars[current_pos].to_string(),
highlight_type: HighlightType::Unit,
});
current_pos += 1;
} else if "+-*/()=^".contains(chars[current_pos]) {
spans.push(HighlightedSpan {
text: chars[current_pos].to_string(),
highlight_type: HighlightType::Operator,
});
current_pos += 1;
} else {
spans.push(HighlightedSpan {
text: chars[current_pos].to_string(),
highlight_type: HighlightType::Normal,
});
current_pos += 1;
}
}
spans
}
pub fn highlight_expression_with_cursor(
text: &str,
cursor_col: usize,
variables: &HashMap<String, String>,
) -> (Vec<HighlightedSpan>, usize) {
let spans = highlight_expression(text, variables);
(spans, cursor_col)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_number_highlighting() {
let variables = HashMap::new();
let spans = highlight_expression("123.45", &variables);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].text, "123.45");
assert_eq!(spans[0].highlight_type, HighlightType::Number);
}
#[test]
fn test_operator_highlighting() {
let variables = HashMap::new();
let spans = highlight_expression("5 + 3", &variables);
assert_eq!(spans.len(), 5); assert_eq!(spans[0].highlight_type, HighlightType::Number);
assert_eq!(spans[1].highlight_type, HighlightType::Normal); assert_eq!(spans[2].highlight_type, HighlightType::Operator);
assert_eq!(spans[3].highlight_type, HighlightType::Normal); assert_eq!(spans[4].highlight_type, HighlightType::Number);
}
#[test]
fn test_unit_highlighting() {
let variables = HashMap::new();
let spans = highlight_expression("100 GB", &variables);
assert_eq!(spans.len(), 3); assert_eq!(spans[0].highlight_type, HighlightType::Number);
assert_eq!(spans[1].highlight_type, HighlightType::Normal); assert_eq!(spans[2].highlight_type, HighlightType::Unit);
}
#[test]
fn test_line_reference_highlighting() {
let variables = HashMap::new();
let spans = highlight_expression("line1 + 5", &variables);
assert!(
spans
.iter()
.any(|s| s.highlight_type == HighlightType::LineReference)
);
assert!(spans.iter().any(|s| s.text == "line1"));
}
#[test]
fn test_variable_highlighting() {
let mut variables = HashMap::new();
variables.insert("x".to_string(), "42".to_string());
let spans = highlight_expression("x * 2", &variables);
assert!(
spans
.iter()
.any(|s| s.highlight_type == HighlightType::Variable)
);
assert!(spans.iter().any(|s| s.text == "x"));
}
#[test]
fn test_keyword_highlighting() {
let variables = HashMap::new();
let spans = highlight_expression("100 GB to MB", &variables);
assert!(
spans
.iter()
.any(|s| s.highlight_type == HighlightType::Keyword)
);
assert!(spans.iter().any(|s| s.text == "to"));
}
#[test]
fn test_function_highlighting() {
let variables = HashMap::new();
let spans = highlight_expression("sqrt(16)", &variables);
assert!(
spans
.iter()
.any(|s| s.highlight_type == HighlightType::Function)
);
assert!(spans.iter().any(|s| s.text == "sqrt"));
let spans = highlight_expression("sum_above()", &variables);
assert!(
spans
.iter()
.any(|s| s.highlight_type == HighlightType::Function)
);
assert!(spans.iter().any(|s| s.text == "sum_above"));
}
}