#![allow(dead_code)]
use crate::config::RColorConfig;
use nu_ansi_term::{Color, Style};
use once_cell::sync::Lazy;
use reedline::{Highlighter, StyledText};
use regex::Regex;
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
Comment,
String,
Number,
Keyword,
Constant,
Operator,
Punctuation,
Identifier,
Whitespace,
Other,
}
impl TokenType {
pub fn style(self, config: &RColorConfig) -> Style {
match self {
TokenType::Comment => color_to_style(config.comment),
TokenType::String => color_to_style(config.string),
TokenType::Number => color_to_style(config.number),
TokenType::Keyword => color_to_style(config.keyword),
TokenType::Constant => color_to_style(config.constant),
TokenType::Operator => color_to_style(config.operator),
TokenType::Punctuation => color_to_style(config.punctuation),
TokenType::Identifier => color_to_style(config.identifier),
TokenType::Whitespace => Style::new(),
TokenType::Other => Style::new(),
}
}
}
fn color_to_style(color: Color) -> Style {
match color {
Color::Default => Style::new(),
c => Style::new().fg(c),
}
}
#[derive(Debug, Clone)]
pub struct Token {
pub start: usize,
pub end: usize,
pub token_type: TokenType,
}
static KEYWORDS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
"if", "else", "for", "while", "repeat", "in", "next", "break", "return", "function",
]
.into_iter()
.collect()
});
static CONSTANTS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
"TRUE",
"FALSE",
"NULL",
"Inf",
"NaN",
"NA",
"NA_integer_",
"NA_real_",
"NA_complex_",
"NA_character_",
]
.into_iter()
.collect()
});
static PATTERNS: Lazy<Vec<(Regex, TokenType)>> = Lazy::new(|| {
vec![
(Regex::new(r"^#.*").unwrap(), TokenType::Comment),
(Regex::new(r"^\s+").unwrap(), TokenType::Whitespace),
(
Regex::new(r#"^[rR]"-*\((?s:.)*?\)-*""#).unwrap(),
TokenType::String,
),
(
Regex::new(r#"^[rR]"-*\[(?s:.)*?\]-*""#).unwrap(),
TokenType::String,
),
(
Regex::new(r#"^[rR]"-*\{(?s:.)*?\}-*""#).unwrap(),
TokenType::String,
),
(
Regex::new(r"^[rR]'-*\((?s:.)*?\)-*'").unwrap(),
TokenType::String,
),
(
Regex::new(r"^[rR]'-*\[(?s:.)*?\]-*'").unwrap(),
TokenType::String,
),
(
Regex::new(r"^[rR]'-*\{(?s:.)*?\}-*'").unwrap(),
TokenType::String,
),
(
Regex::new(r#"^"([^"\\]|\\.)*""#).unwrap(),
TokenType::String,
),
(Regex::new(r"^'([^'\\]|\\.)*'").unwrap(), TokenType::String),
(
Regex::new(r#"^"([^"\\]|\\.)*$"#).unwrap(),
TokenType::String,
),
(Regex::new(r"^'([^'\\]|\\.)*$").unwrap(), TokenType::String),
(Regex::new(r"^\.\.\.").unwrap(), TokenType::Constant),
(Regex::new(r"^\.\.[0-9]+").unwrap(), TokenType::Constant),
(
Regex::new(r"^0[xX][a-fA-F0-9]+([pP][0-9]+)?[Li]?").unwrap(),
TokenType::Number,
),
(
Regex::new(r"^([0-9]+(\.[0-9]+)?|\.[0-9]+)([eE][+-]?[0-9]+)?[Li]?").unwrap(),
TokenType::Number,
),
(
Regex::new(r"^(<<-|<-|->>|->|:=|==|!=|<=|>=|&&|\|\||:::|::|[*][*])").unwrap(),
TokenType::Operator,
),
(Regex::new(r"^(\|>|%>%)").unwrap(), TokenType::Operator),
(Regex::new(r"^%[^%]*%").unwrap(), TokenType::Operator),
(Regex::new(r"^\\").unwrap(), TokenType::Operator),
(
Regex::new(r"^[<>!&|?*+\^/=~$@:-]").unwrap(),
TokenType::Operator,
),
(
Regex::new(r"^(\[\[|\]\]|\[|\]|\(|\)|;|,|\{|\})").unwrap(),
TokenType::Punctuation,
),
(
Regex::new(r"^`[^`\\]*(?:\\.[^`\\]*)*`").unwrap(),
TokenType::Identifier,
),
(
Regex::new(r"^[a-zA-Z][\w.]*").unwrap(),
TokenType::Identifier,
),
(
Regex::new(r"^\.[a-zA-Z_][\w.]*").unwrap(),
TokenType::Identifier,
),
(
Regex::new(r"^\.(?:[^0-9\w]|$)").unwrap(),
TokenType::Identifier,
),
]
});
fn classify_identifier(text: &str) -> TokenType {
if KEYWORDS.contains(text) {
TokenType::Keyword
} else if CONSTANTS.contains(text) {
TokenType::Constant
} else {
TokenType::Identifier
}
}
pub fn tokenize(input: &str) -> Vec<Token> {
let mut tokens = Vec::new();
let mut pos = 0;
while pos < input.len() {
let remaining = &input[pos..];
let mut matched = false;
for (pattern, token_type) in PATTERNS.iter() {
if let Some(m) = pattern.find(remaining)
&& m.start() == 0
&& !m.is_empty()
{
let text = &remaining[..m.len()];
let final_type = if *token_type == TokenType::Identifier {
classify_identifier(text)
} else {
*token_type
};
tokens.push(Token {
start: pos,
end: pos + m.len(),
token_type: final_type,
});
pos += m.len();
matched = true;
break;
}
}
if !matched {
let next_pos = input[pos..]
.char_indices()
.nth(1)
.map(|(i, _)| pos + i)
.unwrap_or(input.len());
tokens.push(Token {
start: pos,
end: next_pos,
token_type: TokenType::Other,
});
pos = next_pos;
}
}
tokens
}
pub struct RHighlighter {
config: RColorConfig,
}
impl RHighlighter {
pub fn new(config: RColorConfig) -> Self {
RHighlighter { config }
}
}
impl Default for RHighlighter {
fn default() -> Self {
Self::new(RColorConfig::default())
}
}
impl Highlighter for RHighlighter {
fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
let mut styled = StyledText::new();
let tokens = tokenize(line);
for token in tokens {
let text = &line[token.start..token.end];
let style = token.token_type.style(&self.config);
styled.push((style, text.to_string()));
}
if styled.buffer.is_empty() {
styled.push((Style::new(), String::new()));
}
styled
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenize_comment() {
let tokens = tokenize("# this is a comment");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Comment);
}
#[test]
fn test_tokenize_string_double() {
let tokens = tokenize(r#""hello world""#);
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::String);
}
#[test]
fn test_tokenize_string_single() {
let tokens = tokenize("'hello world'");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::String);
}
#[test]
fn test_tokenize_string_with_escape() {
let tokens = tokenize(r#""hello \"world\"""#);
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::String);
}
#[test]
fn test_tokenize_raw_string() {
let tokens = tokenize(r#"r"(hello "world")""#);
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::String);
}
#[test]
fn test_tokenize_keywords() {
let input = "if else for while function";
let tokens = tokenize(input);
let keywords: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == TokenType::Keyword)
.collect();
assert_eq!(keywords.len(), 5);
}
#[test]
fn test_tokenize_constants() {
let input = "TRUE FALSE NULL NA Inf NaN";
let tokens = tokenize(input);
let constants: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == TokenType::Constant)
.collect();
assert_eq!(constants.len(), 6);
}
#[test]
fn test_tokenize_numbers() {
let cases = vec!["42", "3.14", "1e-5", "0xFF", "1L", "2i", ".5", "1.5e+10"];
for case in cases {
let tokens = tokenize(case);
assert_eq!(
tokens[0].token_type,
TokenType::Number,
"Failed for: {}",
case
);
}
}
#[test]
fn test_tokenize_operators() {
let input = "<- -> |> %>% :: ::: == != <= >= && ||";
let tokens = tokenize(input);
let operators: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == TokenType::Operator)
.map(|t| &input[t.start..t.end])
.collect();
assert_eq!(
operators,
vec![
"<-", "->", "|>", "%>%", "::", ":::", "==", "!=", "<=", ">=", "&&", "||"
]
);
}
#[test]
fn test_tokenize_walrus_operator() {
let tokens = tokenize("dt[, x := 1]");
let ops: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == TokenType::Operator)
.map(|t| &"dt[, x := 1]"[t.start..t.end])
.collect();
assert!(ops.contains(&":="));
}
#[test]
fn test_tokenize_double_star() {
let tokens = tokenize("2 ** 3");
let ops: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == TokenType::Operator)
.map(|t| &"2 ** 3"[t.start..t.end])
.collect();
assert_eq!(ops, vec!["**"]);
}
#[test]
fn test_tokenize_lambda_backslash() {
let tokens = tokenize(r"\(x) x + 1");
assert_eq!(tokens[0].token_type, TokenType::Operator);
}
#[test]
fn test_tokenize_special_operator() {
let tokens = tokenize("%in%");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Operator);
}
#[test]
fn test_tokenize_punctuation() {
let input = "()[]{},;";
let tokens = tokenize(input);
let punct: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == TokenType::Punctuation)
.collect();
assert_eq!(punct.len(), 8);
}
#[test]
fn test_tokenize_double_bracket() {
let tokens = tokenize("x[[1]]");
assert_eq!(tokens.len(), 4);
}
#[test]
fn test_tokenize_identifier() {
let cases = vec!["foo", "bar_baz", ".hidden", "x.y.z"];
for case in cases {
let tokens = tokenize(case);
assert_eq!(tokens.len(), 1, "Failed for: {}", case);
assert_eq!(
tokens[0].token_type,
TokenType::Identifier,
"Failed for: {}",
case
);
}
}
#[test]
fn test_tokenize_data_frame() {
let tokens = tokenize("data.frame");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Identifier);
}
#[test]
fn test_tokenize_backtick_identifier() {
let tokens = tokenize("`weird name`");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Identifier);
}
#[test]
fn test_tokenize_function_call() {
let input = "print(x)";
let tokens = tokenize(input);
assert_eq!(tokens.len(), 4);
assert_eq!(tokens[0].token_type, TokenType::Identifier);
assert_eq!(tokens[1].token_type, TokenType::Punctuation);
assert_eq!(tokens[2].token_type, TokenType::Identifier);
assert_eq!(tokens[3].token_type, TokenType::Punctuation);
}
#[test]
fn test_tokenize_assignment() {
let input = "x <- 42";
let tokens = tokenize(input);
assert_eq!(tokens.len(), 5);
assert_eq!(tokens[0].token_type, TokenType::Identifier);
assert_eq!(tokens[2].token_type, TokenType::Operator);
assert_eq!(tokens[4].token_type, TokenType::Number);
}
#[test]
fn test_tokenize_library_call() {
let input = "library(dplyr)";
let tokens = tokenize(input);
assert_eq!(tokens[0].token_type, TokenType::Identifier);
}
#[test]
fn test_tokenize_na_variants() {
let cases = vec![
("NA", TokenType::Constant),
("NA_integer_", TokenType::Constant),
("NA_real_", TokenType::Constant),
("NA_complex_", TokenType::Constant),
("NA_character_", TokenType::Constant),
];
for (input, expected) in cases {
let tokens = tokenize(input);
assert_eq!(tokens[0].token_type, expected, "Failed for: {}", input);
}
}
#[test]
fn test_tokenize_dot_dot() {
let tokens = tokenize("...");
assert_eq!(tokens[0].token_type, TokenType::Constant);
let tokens = tokenize("..1");
assert_eq!(tokens[0].token_type, TokenType::Constant);
}
#[test]
fn test_highlight_basic() {
let highlighter = RHighlighter::default();
let styled = highlighter.highlight("x <- 42", 0);
assert_eq!(styled.raw_string(), "x <- 42");
}
#[test]
fn test_highlight_preserves_text() {
let highlighter = RHighlighter::default();
let input = "# comment\nx <- c(1, 2, 3)\nprint(x)";
let styled = highlighter.highlight(input, 0);
assert_eq!(styled.raw_string(), input);
}
#[test]
fn test_highlight_empty_input() {
let highlighter = RHighlighter::default();
let styled = highlighter.highlight("", 0);
assert_eq!(styled.raw_string(), "");
}
#[test]
fn test_keyword_not_in_identifier() {
let tokens = tokenize("iffy");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Identifier);
}
#[test]
fn test_keyword_with_dot_suffix() {
let tokens = tokenize("if.x");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].token_type, TokenType::Identifier);
}
#[test]
fn test_t_and_f_are_identifiers() {
let tokens = tokenize("T F");
let identifiers: Vec<_> = tokens
.iter()
.filter(|t| t.token_type == TokenType::Identifier)
.collect();
assert_eq!(identifiers.len(), 2);
}
#[test]
fn test_tokenize_operator_no_spaces() {
let input = "1+1";
let tokens = tokenize(input);
let token_texts: Vec<_> = tokens.iter().map(|t| &input[t.start..t.end]).collect();
assert_eq!(
token_texts,
vec!["1", "+", "1"],
"Operator should be separate token"
);
assert_eq!(tokens[0].token_type, TokenType::Number);
assert_eq!(tokens[1].token_type, TokenType::Operator);
assert_eq!(tokens[2].token_type, TokenType::Number);
}
#[test]
fn test_tokenize_operators_no_spaces_various() {
let cases = vec![
("1-1", vec!["1", "-", "1"]),
("2*3", vec!["2", "*", "3"]),
("4/2", vec!["4", "/", "2"]),
("2^3", vec!["2", "^", "3"]),
("x<-1", vec!["x", "<-", "1"]),
("a==b", vec!["a", "==", "b"]),
];
for (input, expected) in cases {
let tokens = tokenize(input);
let token_texts: Vec<_> = tokens.iter().map(|t| &input[t.start..t.end]).collect();
assert_eq!(token_texts, expected, "Failed for input: {}", input);
}
}
#[test]
fn test_tokenize_unary_minus() {
let input = "-1";
let tokens = tokenize(input);
let token_texts: Vec<_> = tokens.iter().map(|t| &input[t.start..t.end]).collect();
assert_eq!(token_texts, vec!["-", "1"]);
assert_eq!(tokens[0].token_type, TokenType::Operator);
assert_eq!(tokens[1].token_type, TokenType::Number);
}
}