use crate::api::ClientApi;
use crate::sql::{parse_req, run_sql};
use crate::Config;
use clap::{Arg, ArgAction, ArgMatches};
use colored::*;
use std::io::Write;
use rustyline::completion::Completer;
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::history::DefaultHistory;
use rustyline::validate::{MatchingBracketValidator, Validator};
use rustyline::{Editor, Helper};
use syntect::easy::HighlightLines;
use syntect::highlighting::{Theme, ThemeSet};
use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
use syntect::util::LinesWithEndings;
static SQL_SYNTAX: &str = include_str!("../../tools/sublime/SpaceTimeDbSQL.sublime-syntax");
static SYNTAX_NAME: &str = "SQL (SpaceTimeDb)";
static AUTO_COMPLETE: &str = "\
true
false
select
from
insert
into
values
update,
delete,
create,
where
join
sort by
.exit
.clear
";
pub fn cli() -> clap::Command {
clap::Command::new("repl").about("Runs an interactive command prompt.")
.arg(
Arg::new("database")
.required(true)
.help("The domain or address of the database you would like to query"),
)
.arg(
Arg::new("as_identity")
.long("as-identity")
.short('i')
.conflicts_with("anon_identity")
.help("The identity to use for querying the database")
.long_help("The identity to use for querying the database. If no identity is provided, the default one will be used."),
)
.arg(
Arg::new("anon_identity")
.long("anon-identity")
.short('a')
.conflicts_with("as_identity")
.action(ArgAction::SetTrue)
.help("If this flag is present, no identity will be provided when querying the database")
)
}
pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let con = parse_req(config, args).await?;
let database = con.database.clone();
let mut rl = Editor::<ReplHelper, DefaultHistory>::new().unwrap();
if rl.load_history(".history.txt").is_err() {
eprintln!("No previous history.");
}
rl.set_helper(Some(ReplHelper::new().unwrap()));
println!(
"\
┌──────────────────────────────────────────────────────────┐
│ .exit: Exit the REPL │
│ .clear: Clear the Screen │
│ │
│ Give us feedback in our Discord server: │
│ https://discord.gg/w2DVqNZXdN │
└──────────────────────────────────────────────────────────┘",
);
let api = ClientApi::new(con);
loop {
let readline = rl.readline(&format!("🪐{}>", &database).green());
match readline {
Ok(line) => match line.as_str() {
".exit" => break,
".clear" => {
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
std::io::stdout().flush().ok();
}
sql => match run_sql(api.sql(), sql).await {
Ok(()) => {
rl.add_history_entry(line).ok();
}
Err(err) => {
eprintln!("{}", err.to_string().red())
}
},
},
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("\n{}", "Aborted!".red());
break;
}
x => {
eprintln!("\nUnexpected: {x:?}");
break;
}
}
}
rl.save_history(".history.txt").ok();
Ok(())
}
pub(crate) struct ReplHelper {
syntaxes: SyntaxSet,
theme: Theme,
brackets: MatchingBracketValidator,
}
impl ReplHelper {
pub fn new() -> Result<Self, ()> {
let syntax_def = SyntaxDefinition::load_from_str(SQL_SYNTAX, false, Some(SYNTAX_NAME)).unwrap();
let mut builder = SyntaxSetBuilder::new();
builder.add(syntax_def);
let syntaxes = builder.build();
let _ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let theme = ts.themes["base16-ocean.dark"].clone();
Ok(ReplHelper {
syntaxes,
theme,
brackets: MatchingBracketValidator::new(),
})
}
}
impl Helper for ReplHelper {}
impl Completer for ReplHelper {
type Candidate = String;
fn complete(
&self,
line: &str,
pos: usize,
_: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
let mut name = String::new();
let mut name_pos = pos;
while let Some(char) = line
.chars()
.nth(name_pos.wrapping_sub(1))
.filter(|c| c.is_ascii_alphanumeric() || ['_', '.'].contains(c))
{
name.push(char);
name_pos -= 1;
}
if name.is_empty() {
return Ok((0, vec![]));
}
name = name.chars().rev().collect();
let mut completions: Vec<_> = AUTO_COMPLETE.split('\n').map(str::to_string).collect();
completions = completions
.iter()
.filter_map(|it| it.starts_with(&name).then(|| it.clone()))
.collect();
Ok((name_pos, completions))
}
}
impl Hinter for ReplHelper {
type Hint = String;
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
if line.len() > pos {
return None;
}
if let Ok((mut completion_pos, completions)) = self.complete(line, pos, ctx) {
if completions.is_empty() {
return None;
}
let mut hint = completions[0].clone();
while completion_pos < pos {
if hint.is_empty() {
return None;
}
hint.remove(0);
completion_pos += 1;
}
Some(hint)
} else {
None
}
}
}
impl Highlighter for ReplHelper {
fn highlight<'l>(&self, line: &'l str, _: usize) -> std::borrow::Cow<'l, str> {
let mut h = HighlightLines::new(self.syntaxes.find_syntax_by_name(SYNTAX_NAME).unwrap(), &self.theme);
let mut out = String::new();
for line in LinesWithEndings::from(line) {
let ranges = h.highlight_line(line, &self.syntaxes).unwrap();
let escaped = syntect::util::as_24_bit_terminal_escaped(&ranges[..], false);
out += &escaped;
}
std::borrow::Cow::Owned(out)
}
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str, _: bool) -> std::borrow::Cow<'b, str> {
std::borrow::Cow::Owned(prompt.green().to_string())
}
fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
std::borrow::Cow::Owned(hint.bright_black().to_string())
}
fn highlight_candidate<'c>(&self, candidate: &'c str, _: rustyline::CompletionType) -> std::borrow::Cow<'c, str> {
std::borrow::Cow::Owned(candidate.bright_cyan().to_string())
}
fn highlight_char(&self, _: &str, _: usize) -> bool {
true
}
}
impl Validator for ReplHelper {
fn validate(
&self,
ctx: &mut rustyline::validate::ValidationContext,
) -> rustyline::Result<rustyline::validate::ValidationResult> {
self.brackets.validate(ctx)
}
}