use crate::config::RColorConfig;
use crate::r_parser::{is_atomic_node, parse_r, walk_atomic_tokens};
use nu_ansi_term::{Color, Style};
use once_cell::sync::Lazy;
use reedline::{Highlighter, StyledText};
use std::collections::HashSet;
use std::ops::ControlFlow;
use tree_sitter::Node;
use super::bracket_match::find_matching_bracket;
use super::token::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()
});
#[derive(Debug, Clone)]
pub struct Token {
pub start: usize,
pub end: usize,
pub token_type: TokenType,
}
fn node_to_token_type(node: &Node, source: &[u8]) -> TokenType {
match node.kind() {
"integer" | "float" | "complex" => TokenType::Number,
"string" | "string_content" => TokenType::String,
"escape_sequence" => TokenType::String,
"comment" => TokenType::Comment,
"true" | "false" => TokenType::Constant,
"null" | "inf" | "nan" | "na" => TokenType::Constant,
"dots" | "dot_dot_i" => TokenType::Constant,
"function" | "if" | "else" | "for" | "while" | "repeat" | "in" | "next" | "break"
| "return" => TokenType::Keyword,
"?" | ":=" | "=" | "<-" | "<<-" | "->" | "->>" | "~" | "|>" | "||" | "|" | "&&" | "&"
| "<" | "<=" | ">" | ">=" | "==" | "!=" | "+" | "-" | "*" | "/" | "::" | ":::" | "**"
| "^" | "$" | "@" | ":" | "!" | r#"\"# | "special" => TokenType::Operator,
"(" | ")" | "{" | "}" | "[" | "]" | "[[" | "]]" => TokenType::Punctuation,
"comma" | ";" => TokenType::Punctuation,
"identifier" => {
let text = node.utf8_text(source).unwrap_or("");
if KEYWORDS.contains(text) {
TokenType::Keyword
} else if CONSTANTS.contains(text) {
TokenType::Constant
} else {
TokenType::Identifier
}
}
_ => TokenType::Other,
}
}
fn collect_tokens(tree: &tree_sitter::Tree, source: &[u8]) -> Vec<Token> {
let mut tokens = Vec::new();
walk_atomic_tokens(tree.root_node(), |node| {
let token_type = node_to_token_type(&node, source);
if is_atomic_node(node.kind())
|| token_type != TokenType::Other
|| node.start_byte() < node.end_byte()
{
tokens.push(Token {
start: node.start_byte(),
end: node.end_byte(),
token_type,
});
}
ControlFlow::Continue(())
});
tokens
}
fn fill_gaps(tokens: &[Token], total_len: usize) -> Vec<Token> {
let mut result = Vec::new();
let mut pos = 0;
for token in tokens.iter() {
if token.start > pos {
result.push(Token {
start: pos,
end: token.start,
token_type: TokenType::Whitespace,
});
}
result.push(token.clone());
pos = token.end;
}
if pos < total_len {
result.push(Token {
start: pos,
end: total_len,
token_type: TokenType::Whitespace,
});
}
result
}
pub fn tokenize_r(source: &str) -> Vec<Token> {
let tree = match parse_r(source) {
Some(t) => t,
None => {
return vec![Token {
start: 0,
end: source.len(),
token_type: TokenType::Other,
}];
}
};
let mut tokens = collect_tokens(&tree, source.as_bytes());
tokens.sort_by_key(|t| t.start);
fill_gaps(&tokens, source.len())
}
pub struct RTreeSitterHighlighter {
config: RColorConfig,
highlight_matching_bracket: bool,
}
impl RTreeSitterHighlighter {
pub fn new(config: RColorConfig, highlight_matching_bracket: bool) -> Self {
RTreeSitterHighlighter {
config,
highlight_matching_bracket,
}
}
}
impl Default for RTreeSitterHighlighter {
fn default() -> Self {
Self::new(RColorConfig::default(), true)
}
}
impl Highlighter for RTreeSitterHighlighter {
fn highlight(&self, line: &str, cursor: usize) -> StyledText {
let mut styled = StyledText::new();
let tree = parse_r(line);
if let Some(ref tree) = tree {
let source = line.as_bytes();
let mut tokens = collect_tokens(tree, source);
tokens.sort_by_key(|t| t.start);
let tokens = fill_gaps(&tokens, source.len());
for token in tokens {
if token.start < line.len() && token.end <= line.len() {
let text = &line[token.start..token.end];
let style = token.token_type.style(&self.config);
styled.push((style, text.to_string()));
}
}
} else {
styled.push((Style::new(), line.to_string()));
}
if styled.buffer.is_empty() {
styled.push((Style::new(), String::new()));
}
if self.highlight_matching_bracket
&& self.config.matching_bracket != Color::Default
&& let Some(ref tree) = tree
&& let Some(bracket) = find_matching_bracket(line, cursor, tree)
{
let bg = self.config.matching_bracket;
apply_bracket_highlight(&mut styled, bracket.cursor_bracket, bg);
apply_bracket_highlight(&mut styled, bracket.matching_bracket, bg);
}
styled
}
}
fn apply_bracket_highlight(styled: &mut StyledText, pos: usize, highlight_bg: Color) {
let existing_style = {
let mut offset = 0;
let mut found = None;
for (style, text) in &styled.buffer {
let end = offset + text.len();
if pos >= offset && pos < end {
found = Some(*style);
break;
}
offset = end;
}
found.unwrap_or_default()
};
let merged = Style {
background: Some(highlight_bg),
..existing_style
};
styled.style_range(pos, pos + 1, merged);
}
#[cfg(test)]
mod tests;