use crate::r_parser::{is_atomic_node, parse_r};
use nu_ansi_term::Style;
use reedline::{Hinter, History, SearchQuery};
fn get_first_r_token(s: &str) -> String {
if s.is_empty() {
return String::new();
}
if let Some(tree) = parse_r(s) {
let root = tree.root_node();
if let Some((start, end)) = find_first_token_bounds(&root, s.as_bytes()) {
let leading_ws = &s[..start];
let token = &s[start..end];
return format!("{}{}", leading_ws, token);
}
}
get_first_word(s)
}
fn get_first_word(s: &str) -> String {
let mut chars = s.chars().peekable();
let mut result = String::new();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
result.push(c);
chars.next();
} else {
break;
}
}
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
break;
}
result.push(c);
chars.next();
}
result
}
fn find_first_token_bounds(node: &tree_sitter::Node, source: &[u8]) -> Option<(usize, usize)> {
let mut cursor = node.walk();
let root_id = node.id();
loop {
let current = cursor.node();
let kind = current.kind();
if is_atomic_node(kind) {
let start = current.start_byte();
let end = current.end_byte();
if start < end {
return Some((start, end));
}
}
if current.child_count() == 0 {
let start = current.start_byte();
let end = current.end_byte();
if start < end {
if let Ok(text) = std::str::from_utf8(&source[start..end])
&& !text.chars().all(char::is_whitespace)
{
return Some((start, end));
}
}
} else if !is_atomic_node(kind) {
if cursor.goto_first_child() {
continue;
}
}
if cursor.goto_next_sibling() {
continue;
}
loop {
if !cursor.goto_parent() {
return None; }
if cursor.node().id() == root_id {
return None; }
if cursor.goto_next_sibling() {
break;
}
}
}
}
pub struct RLanguageHinter {
style: Style,
current_hint: String,
min_chars: usize,
cwd_aware: bool,
}
impl RLanguageHinter {
pub fn new() -> Self {
RLanguageHinter {
style: Style::new(),
current_hint: String::new(),
min_chars: 1,
cwd_aware: false,
}
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
#[allow(dead_code)]
pub fn with_min_chars(mut self, min_chars: usize) -> Self {
self.min_chars = min_chars;
self
}
#[must_use]
pub fn with_cwd_aware(mut self, cwd_aware: bool) -> Self {
self.cwd_aware = cwd_aware;
self
}
}
impl Default for RLanguageHinter {
fn default() -> Self {
Self::new()
}
}
impl Hinter for RLanguageHinter {
fn handle(
&mut self,
line: &str,
#[allow(unused_variables)] pos: usize,
history: &dyn History,
use_ansi_coloring: bool,
cwd: &str,
) -> String {
self.current_hint = if line.chars().count() >= self.min_chars {
if self.cwd_aware {
let cwd_results = history
.search(SearchQuery::last_with_prefix_and_cwd(
line.to_string(),
cwd.to_string(),
history.session(),
))
.unwrap_or_default();
if !cwd_results.is_empty() {
cwd_results[0]
.command_line
.get(line.len()..)
.unwrap_or_default()
.to_string()
} else {
self.search_all_history(line, history)
}
} else {
self.search_all_history(line, history)
}
} else {
String::new()
};
if use_ansi_coloring && !self.current_hint.is_empty() {
self.style.paint(&self.current_hint).to_string()
} else {
self.current_hint.clone()
}
}
fn complete_hint(&self) -> String {
self.current_hint.clone()
}
fn next_hint_token(&self) -> String {
get_first_r_token(&self.current_hint)
}
}
impl RLanguageHinter {
fn search_all_history(&self, prefix: &str, history: &dyn History) -> String {
history
.search(SearchQuery::last_with_prefix(
prefix.to_string(),
history.session(),
))
.unwrap_or_default()
.first()
.map(|entry| {
entry
.command_line
.get(prefix.len()..)
.unwrap_or_default()
.to_string()
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_first_r_token_pipe_operator() {
let result = get_first_r_token("|> filter(x > 0)");
assert_eq!(result, "|>");
}
#[test]
fn test_get_first_r_token_assignment_operator() {
let result = get_first_r_token("<- 42");
assert_eq!(result, "<-");
}
#[test]
fn test_get_first_r_token_double_arrow() {
let result = get_first_r_token("<<- value");
assert_eq!(result, "<<-");
}
#[test]
fn test_get_first_r_token_right_arrow() {
let result = get_first_r_token("-> y");
assert_eq!(result, "->");
}
#[test]
fn test_get_first_r_token_namespace() {
let result = get_first_r_token("::mutate");
assert_eq!(result, "::");
}
#[test]
fn test_get_first_r_token_triple_colon() {
let result = get_first_r_token(":::internal");
assert_eq!(result, ":::");
}
#[test]
fn test_get_first_r_token_comparison() {
let result = get_first_r_token(">= 5");
assert_eq!(result, ">=");
let result = get_first_r_token("<= 5");
assert_eq!(result, "<=");
let result = get_first_r_token("== TRUE");
assert_eq!(result, "==");
let result = get_first_r_token("!= FALSE");
assert_eq!(result, "!=");
}
#[test]
fn test_get_first_r_token_identifier() {
let result = get_first_r_token("filter(x > 0)");
assert_eq!(result, "filter");
}
#[test]
fn test_get_first_r_token_with_leading_space() {
let result = get_first_r_token(" |> filter()");
assert_eq!(result, " |>");
}
#[test]
fn test_get_first_r_token_number() {
let result = get_first_r_token("42 + 1");
assert_eq!(result, "42");
}
#[test]
fn test_get_first_r_token_string() {
let result = get_first_r_token(r#""hello" world"#);
assert_eq!(result, r#""hello""#);
}
#[test]
fn test_get_first_r_token_empty() {
let result = get_first_r_token("");
assert_eq!(result, "");
}
#[test]
fn test_get_first_r_token_special_operator() {
let result = get_first_r_token("%% 2");
assert!(!result.is_empty());
}
#[test]
fn test_get_first_r_token_logical_and() {
let result = get_first_r_token("&& y");
assert_eq!(result, "&&");
}
#[test]
fn test_get_first_r_token_logical_or() {
let result = get_first_r_token("|| y");
assert_eq!(result, "||");
}
#[test]
fn test_get_first_r_token_walrus() {
let result = get_first_r_token(":= value");
assert_eq!(result, ":=");
}
#[test]
fn test_get_first_r_token_power() {
let result = get_first_r_token("** 2");
assert_eq!(result, "**");
}
}