use rustyline::completion::{Completer, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
use rustyline::{Context, Helper};
pub const DOT_COMMANDS: &[&str] = &[
".help", ".quit", ".exit", ".labels", ".rels", ".schema", ".indexes", ".mode", ".dump",
".read", ".import", ".save", ".timing",
];
#[derive(Default)]
pub struct ShellHelper {
candidates: Vec<String>,
}
impl ShellHelper {
pub fn set_candidates(&mut self, candidates: Vec<String>) {
self.candidates = candidates;
}
}
impl Completer for ShellHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
let start = line[..pos]
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
let word = &line[start..pos];
let pool: Vec<&str> = if word.starts_with('.') {
DOT_COMMANDS.to_vec()
} else {
self.candidates.iter().map(String::as_str).collect()
};
let matches: Vec<Pair> = pool
.into_iter()
.filter(|cand| cand.starts_with(word))
.map(|cand| Pair {
display: cand.to_string(),
replacement: cand.to_string(),
})
.collect();
Ok((start, matches))
}
}
impl Validator for ShellHelper {
fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result<ValidationResult> {
let input = ctx.input();
let trimmed = input.trim();
if trimmed.is_empty() || trimmed.starts_with('.') {
return Ok(ValidationResult::Valid(None));
}
if trimmed.ends_with(';') && is_balanced(input) {
Ok(ValidationResult::Valid(None))
} else {
Ok(ValidationResult::Incomplete)
}
}
}
impl Hinter for ShellHelper {
type Hint = String;
}
impl Highlighter for ShellHelper {}
impl Helper for ShellHelper {}
fn is_balanced(s: &str) -> bool {
let mut depth: i32 = 0;
let mut quote: Option<char> = None;
let mut prev_backslash = false;
for c in s.chars() {
match quote {
Some(q) => {
if c == q && !prev_backslash {
quote = None;
}
}
None => match c {
'\'' | '"' => quote = Some(c),
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
_ => {}
},
}
prev_backslash = c == '\\' && !prev_backslash;
}
quote.is_none() && depth <= 0
}
#[cfg(test)]
mod tests {
use super::is_balanced;
#[test]
fn balanced_single_line() {
assert!(is_balanced("MATCH (n) RETURN n"));
assert!(is_balanced("RETURN 1"));
}
#[test]
fn unbalanced_is_incomplete() {
assert!(!is_balanced("MATCH (n")); assert!(!is_balanced("RETURN [1, 2")); assert!(!is_balanced("RETURN 'oops")); }
#[test]
fn quotes_shield_brackets() {
assert!(is_balanced("RETURN '(' AS p")); assert!(is_balanced("RETURN \"a)b\" AS s"));
}
}