kvcli 0.0.6

A distributed kv storage CLI
Documentation
use std::borrow::Cow;
use std::sync::Arc;

use rustyline::completion::Completer;
use rustyline::completion::FilenameCompleter;
use rustyline::completion::Pair;
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::ValidationContext;
use rustyline::validate::ValidationResult;
use rustyline::validate::Validator;
use rustyline::Context;
use rustyline::Helper;
use rustyline::Result;
use crate::ast::token_kind::TokenKind;
use crate::ast::tokenizer::{all_reserved_keywords, tokenize_sql};

/// CliHelper
pub struct CliHelper {
    completer: FilenameCompleter,
    keywords: Arc<Vec<String>>,
}

impl CliHelper {
    pub fn new() -> Self {
        Self {
            completer: FilenameCompleter::new(),
            keywords: Arc::new(Vec::new()),
        }
    }

    pub fn with_keywords(keywords: Arc<Vec<String>>) -> Self {
        Self {
            completer: FilenameCompleter::new(),
            keywords,
        }
    }
}

impl Highlighter for CliHelper {
    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
        let tokens = tokenize_sql(line);
        let mut line = line.to_owned();

        if let Ok(tokens) = tokens {
            for token in tokens.iter().rev() {
                if TokenKind::is_keyword(&token.kind)
                    || TokenKind::is_reserved_ident(&token.kind, false)
                    || TokenKind::is_reserved_function_name(&token.kind)
                {
                    line.replace_range(
                        token.span.clone(),
                        &format!("\x1b[1;32m{}\x1b[0m", token.text()),
                    );
                } else if TokenKind::is_literal(&token.kind) {
                    line.replace_range(
                        token.span.clone(),
                        &format!("\x1b[1;33m{}\x1b[0m", token.text()),
                    );
                }
            }
        }

        Cow::Owned(line)
    }

    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
        &'s self,
        prompt: &'p str,
        default: bool,
    ) -> std::borrow::Cow<'b, str> {
        let _ = default;
        std::borrow::Cow::Borrowed(prompt)
    }

    fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
        std::borrow::Cow::Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
    }

    fn highlight_candidate<'c>(
        &self,
        candidate: &'c str, // FIXME should be Completer::Candidate
        completion: rustyline::CompletionType,
    ) -> std::borrow::Cow<'c, str> {
        let _ = completion;
        std::borrow::Cow::Borrowed(candidate)
    }

    fn highlight_char(&self, line: &str, _pos: usize) -> bool {
        !line.is_empty()
    }
}

impl Hinter for CliHelper {
    type Hint = String;

    fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option<Self::Hint> {
        let last_word = line
            .split(|p: char| p.is_whitespace() || p == '.')
            .last()
            .unwrap_or(line);

        if last_word.is_empty() {
            return None;
        }

        let (_, res) = KeyWordCompleter::complete(line, pos, &self.keywords);
        if !res.is_empty() {
            Some(res[0].replacement[last_word.len()..].to_owned())
        } else {
            None
        }
    }
}

impl Completer for CliHelper {
    type Candidate = Pair;

    fn complete(
        &self,
        line: &str,
        pos: usize,
        ctx: &Context<'_>,
    ) -> std::result::Result<(usize, Vec<Pair>), ReadlineError> {
        let keyword_candidates = KeyWordCompleter::complete(line, pos, self.keywords.as_ref());
        if !keyword_candidates.1.is_empty() {
            return Ok(keyword_candidates);
        }
        self.completer.complete(line, pos, ctx)
    }
}

impl Validator for CliHelper {
    fn validate(&self, ctx: &mut ValidationContext<'_>) -> Result<ValidationResult> {
        let input = ctx.input().trim_end();
        if input.strip_suffix('\\').is_some() {
            Ok(ValidationResult::Incomplete)
        } else {
            Ok(ValidationResult::Valid(None))
        }
    }
}

impl Helper for CliHelper {}

struct KeyWordCompleter {}

impl KeyWordCompleter {
    fn complete(s: &str, pos: usize, keywords: &[String]) -> (usize, Vec<Pair>) {
        let hint = s
            .split(|p: char| p.is_whitespace() || p == '.')
            .last()
            .unwrap_or(s);
        let all_keywords = all_reserved_keywords();

        let mut results: Vec<Pair> = all_keywords
            .iter()
            .filter(|keyword| keyword.starts_with(&hint.to_ascii_lowercase()))
            .map(|keyword| Pair {
                display: keyword.to_string(),
                replacement: keyword.to_string(),
            })
            .collect();

        results.extend(
            keywords
                .iter()
                .filter(|keyword| {
                    keyword
                        .to_lowercase()
                        .starts_with(&hint.to_ascii_lowercase())
                })
                .map(|keyword| Pair {
                    display: keyword.to_string(),
                    replacement: keyword.to_string(),
                }),
        );

        if pos >= hint.len() {
            (pos - hint.len(), results)
        } else {
            (0, results)
        }
    }
}