use std::borrow::Cow;
use std::sync::{Arc, Mutex};
use nu_ansi_term::{Color, Style};
use reedline::{
Completer, Highlighter, Prompt, PromptEditMode, PromptHistorySearch,
PromptHistorySearchStatus, Span, StyledText, Suggestion, ValidationResult, Validator,
};
pub const BUILTINS: &[&str] = &[
"text", "anno", "error", "log", "button", "col", "emit", "query", "link", "seal", "subscribe", "add", "sub", "mul", "lt", "gt", ];
pub const KEYWORDS: &[&str] = &["let", "fn", "if", "else", "loop"];
pub const METAS: &[&str] = &[
":help", ":env", ":reset", ":nox", ":type", ":ast", ":load", ":quit",
];
pub type Binds = Arc<Mutex<Vec<String>>>;
pub fn bound_names(binds: &Binds) -> Vec<String> {
binds
.lock()
.unwrap()
.iter()
.filter_map(|l| {
l.strip_prefix("let ")
.and_then(|r| r.split(['=', ':', ' ']).next())
.map(|n| n.trim().to_string())
})
.filter(|n| !n.is_empty())
.collect()
}
pub struct RunePrompt;
impl Prompt for RunePrompt {
fn render_prompt_left(&self) -> Cow<'_, str> {
Cow::Borrowed("\x1b[35mrune\x1b[0m")
}
fn render_prompt_right(&self) -> Cow<'_, str> {
Cow::Borrowed("")
}
fn render_prompt_indicator(&self, _: PromptEditMode) -> Cow<'_, str> {
Cow::Borrowed("\x1b[90m โธ \x1b[0m")
}
fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
Cow::Borrowed("\x1b[90m โฆ \x1b[0m")
}
fn render_prompt_history_search_indicator(&self, h: PromptHistorySearch) -> Cow<'_, str> {
let pre = match h.status {
PromptHistorySearchStatus::Passing => "",
PromptHistorySearchStatus::Failing => "failing ",
};
Cow::Owned(format!("({pre}search: {}) ", h.term))
}
}
pub struct RuneCompleter {
pub binds: Binds,
}
impl Completer for RuneCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
let start = line[..pos]
.rfind(|c: char| c.is_whitespace() || "([,".contains(c))
.map(|i| i + 1)
.unwrap_or(0);
let word = &line[start..pos];
let pool: Vec<String> = if word.starts_with(':') || line.starts_with(':') {
METAS.iter().map(|s| s.to_string()).collect()
} else {
BUILTINS
.iter()
.chain(KEYWORDS.iter())
.map(|s| s.to_string())
.chain(bound_names(&self.binds))
.collect()
};
pool.into_iter()
.filter(|c| c.starts_with(word) && c.as_str() != word)
.map(|value| Suggestion {
value,
description: None,
style: None,
extra: None,
span: Span::new(start, pos),
append_whitespace: false,
match_indices: None,
})
.collect()
}
}
pub struct RuneHighlighter {
pub binds: Binds,
}
impl Highlighter for RuneHighlighter {
fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
let names = bound_names(&self.binds);
let mut st = StyledText::new();
if line.starts_with(':') {
st.push((Style::new().fg(Color::Blue), line.to_string()));
return st;
}
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '"' {
let mut j = i + 1;
while j < chars.len() && chars[j] != '"' {
j += 1;
}
let end = (j + 1).min(chars.len());
let s: String = chars[i..end].iter().collect();
st.push((Style::new().fg(Color::Green), s));
i = end;
} else if c.is_alphabetic() || c == '_' {
let mut j = i;
while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '_') {
j += 1;
}
let w: String = chars[i..j].iter().collect();
let style = if BUILTINS.contains(&w.as_str()) {
Style::new().fg(Color::Cyan)
} else if KEYWORDS.contains(&w.as_str()) {
Style::new().fg(Color::Magenta)
} else if names.contains(&w) {
Style::new().fg(Color::LightYellow)
} else {
Style::default()
};
st.push((style, w));
i = j;
} else if c.is_ascii_digit() {
let mut j = i;
while j < chars.len() && (chars[j].is_ascii_digit() || chars[j] == '.') {
j += 1;
}
st.push((Style::new().fg(Color::Yellow), chars[i..j].iter().collect()));
i = j;
} else {
st.push((Style::default(), c.to_string()));
i += 1;
}
}
st
}
}
pub struct RuneValidator;
impl Validator for RuneValidator {
fn validate(&self, line: &str) -> ValidationResult {
let mut depth = 0i32;
let mut in_str = false;
for c in line.chars() {
match c {
'"' => in_str = !in_str,
'(' | '[' if !in_str => depth += 1,
')' | ']' if !in_str => depth -= 1,
_ => {}
}
}
if depth > 0 || in_str {
ValidationResult::Incomplete
} else {
ValidationResult::Complete
}
}
}