use std::{borrow::Cow, cell::RefCell, collections::HashMap, rc::Rc}; use rustyline::{completion::Completer, highlight::Highlighter, hint::Hinter, validate::{ValidationContext, ValidationResult, Validator}, Helper, Result}; use talc_lang::{Lexer, Vm}; #[derive(Clone, Copy)] enum TokenType { String, Symbol, Number, Literal } pub struct TalcHelper { vm: Rc>, lex: Lexer, token_types: HashMap, } macro_rules! load_tokens { ($token_types:expr, $lex:expr, {$($($tok:literal)|+ => $ty:expr,)*}) => {{ $($( $token_types.insert($lex.lex($tok).next().unwrap().unwrap().1.0, $ty); )*)* }}; } impl TalcHelper { pub fn new(vm: Rc>) -> Self { let lex = Lexer::new(); let mut token_types = HashMap::new(); load_tokens!(token_types, lex, { "\"\"" | "''" => TokenType::String, ":a" | ":''" | ":\"\"" => TokenType::Symbol, "0" | "0.0" | "0x0" | "0b0" | "0o0" | "0s0" => TokenType::Number, "true" | "false" | "nil" => TokenType::Literal, }); Self { vm, lex, token_types, } } } impl Helper for TalcHelper {} impl Completer for TalcHelper { type Candidate = String; fn complete( &self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>, ) -> Result<(usize, Vec)> { let mut res = String::new(); for ch in line[..pos].chars().rev() { if matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '_') { res.push(ch); } else { break } } let res: String = res.chars().rev().collect(); let mut keys = self.vm.borrow().globals().keys() .map(|sym| sym.name()) .filter(|name| name.starts_with(&res)) .map(|s| s.to_string()) .collect::>(); keys.sort(); Ok((pos - res.as_bytes().len(), keys)) } } impl Hinter for TalcHelper { type Hint = String; } impl Highlighter for TalcHelper { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { let mut tokens = self.lex.lex(line).peekable(); let mut buf = String::new(); let mut last = 0; while let Some(Ok((l, tok, r))) = tokens.next() { buf += &line[last..l]; last = r; let tokty = self.token_types.get(&tok.0); buf += match tokty { Some(TokenType::Literal) => "\x1b[93m", Some(TokenType::Number) => "\x1b[93m", Some(TokenType::String) => "\x1b[92m", Some(TokenType::Symbol) => "\x1b[96m", None => "", }; buf += tok.1; if tokty.is_some() { buf += "\x1b[0m" } } buf += &line[last..]; Cow::Owned(buf) } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, _default: bool, ) -> Cow<'b, str> { Cow::Owned(format!("\x1b[94m{prompt}\x1b[0m")) } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Cow::Owned(format!("\x1b[37m{hint}\x1b[0m")) } fn highlight_char(&self, line: &str, _: usize, forced: bool) -> bool { forced || !line.is_empty() } } impl Validator for TalcHelper { fn validate(&self, ctx: &mut ValidationContext) -> Result { let tokens = self.lex.lex(ctx.input()); let mut delims = Vec::new(); let mut mismatch = None; for token in tokens { let token = match token { Ok(t) => t, Err(e) => return Ok(ValidationResult::Invalid( Some(e.to_string()))), }; let t = token.1.1; match t { "(" | "{" | "[" | "if" | "while" | "for" | "try" => delims.push(token.1.1), ")" => match delims.pop() { Some("(") => (), v => { mismatch = Some((v, t)); break } }, "}" => match delims.pop() { Some("{") => (), v => { mismatch = Some((v, t)); break } }, "]" => match delims.pop() { Some("[") => (), v => { mismatch = Some((v, t)); break } }, "then" => match delims.pop() { Some("if") => delims.push(t), v => { mismatch = Some((v, t)); break } } "catch" => match delims.pop() { Some("try") => delims.push(t), v => { mismatch = Some((v, t)); break } } "do" => match delims.last().copied() { Some("while") | Some("for") | Some("catch") => { delims.pop(); delims.push(t); }, _ => delims.push(t) }, "elif" | "else" => match delims.pop() { Some("then") => delims.push(t), v => { mismatch = Some((v, t)); break } }, "end" => match delims.pop() { Some("then" | "elif" | "else" | "do" | "try") => (), v => { mismatch = Some((v, t)); break } }, _ => (), } } match mismatch { Some((None, b)) => return Ok(ValidationResult::Invalid(Some( format!(" found unmatched {b}")))), Some((Some(a), b)) => return Ok(ValidationResult::Invalid(Some( format!(" found {a} matched with {b}")))), _ => (), } if delims.is_empty() { Ok(ValidationResult::Valid(None)) } else { Ok(ValidationResult::Incomplete) } } }