use std::{borrow::Cow, rc::Rc, cell::RefCell}; use rustyline::{validate::{Validator, ValidationResult, ValidationContext}, highlight::Highlighter, Helper, Hinter, completion::Completer}; use matrix_lang::prelude::*; #[derive(Helper, Hinter)] pub struct MatrixHelper { vm: Rc> } impl MatrixHelper { pub fn new(vm: Rc>) -> Self { Self { vm } } } macro_rules! unmatched { ($token:expr, $matched:expr) => { Ok(::rustyline::validate::ValidationResult::Invalid(Some(format!("Token '{:?}' at {}:{} does not have a matching '{:?}'", $token.data, $token.pos.row, $token.pos.col, $matched)))) }; } impl Validator for MatrixHelper { fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result { let mut lexer = Lexer::new(ctx.input()); let mut stack = Vec::new(); loop { use TokenData as T; let token = match lexer.next_token() { Ok(t) if t.data == T::Eof => break, Ok(t) => t, Err(err) => return Ok(ValidationResult::Invalid(Some(format!("{err}")))) }; match token.data { T::LeftParen | T::LeftBrack | T::LeftBrace | T::For | T::Try => { stack.push(token.data); }, T::RightParen => match stack.pop() { Some(T::LeftParen) => {}, _ => return unmatched!(token, T::LeftParen) }, T::RightBrack => match stack.pop() { Some(T::LeftBrack) => {}, _ => return unmatched!(token, T::LeftBrack) }, T::RightBrace => match stack.pop() { Some(T::LeftBrace) => {}, _ => return unmatched!(token, T::LeftBrace) }, T::In => match stack.pop() { Some(T::For) => {}, _ => return unmatched!(token, T::For) }, T::Catch => match stack.pop() { Some(T::Try) => {}, _ => return unmatched!(token, T::Try) }, _ => {} }; } if stack.is_empty() { return Ok(ValidationResult::Valid(None)) } else { return Ok(ValidationResult::Incomplete) } } fn validate_while_typing(&self) -> bool { true } } fn find_matching_bracket(line: &str, pos: usize) -> Option { if pos >= line.len() { return None } let c = line.as_bytes()[pos]; let (target, fwd) = match c { b'(' => (b')', true), b')' => (b'(', false), b'[' => (b']', true), b']' => (b'[', false), b'{' => (b'}', true), b'}' => (b'{', false), _ => return None, }; let mut depth = 0; let mut idx = 0; if fwd { let bytes = &line.as_bytes()[pos+1..]; for &b in bytes { if b == c { depth += 1; } else if b == target { if depth == 0 { return Some(pos + idx + 1) } else { depth -= 1; } } idx += 1; } } else { let bytes = &line.as_bytes()[..pos]; for &b in bytes.iter().rev() { if b == c { depth += 1; } else if b == target { if depth == 0 { return Some(pos - idx - 1) } else { depth -= 1; } } idx += 1; } } None } fn get_token_at(line: &str, pos: usize) -> Option { use TokenData as T; let mut lexer = Lexer::new(line); loop { match lexer.next_token() { Ok(Token { data: T::Eof, ..}) => return None, Ok(t) if t.bidx <= pos && (t.bidx + t.blen) > pos => return Some(t), Err(_) => return None, _ => continue } } } fn find_matching_ident(line: &str, pos: usize) -> Option> { use TokenData as T; match get_token_at(line, pos) { Some(Token { data: T::Ident(ident), ..}) => Some(ident), _ => return None } } impl Highlighter for MatrixHelper { fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { let matching = find_matching_bracket(line, pos); let ident = find_matching_ident(line, pos); use TokenData as T; let mut lexer = Lexer::new(line); let mut buf = String::new(); let mut last = 0; loop { let token = match lexer.next_token() { Ok(t) if t.data == T::Eof => break, Ok(t) => t, Err(_) => break }; let bl = token.bidx; let br = token.bidx + token.blen; buf += &line[last..bl]; last = br; let color = match token.data { T::LeftParen | T::RightParen | T::LeftBrack | T::RightBrack | T::LeftBrace | T::RightBrace => { match matching { Some(idx) if bl == idx || bl == pos => "\x1b[33;40m", _ => "" } } T::Int(_) | T::True | T::False | T::Nil | T::Float(_) | T::Complex(_) => "\x1b[33m", T::Ident(tok) => { match (ident.as_ref(), lexer.peek_token()) { (Some(ident), Ok(t)) if t.data == T::LeftParen && ident.as_ref() == tok.as_ref() => "\x1b[34;40m", (Some(ident), _) if ident.as_ref() == tok.as_ref() => "\x1b[40m", (_, Ok(t)) if t.data == T::LeftParen => "\x1b[34m", _ => "" } } T::Regex(_) => "\x1b[31m", T::String(_) => "\x1b[32m", T::If | T::Else | T::While | T::Let | T::Const | T::Function | T::Continue | T::Break | T::Do | T::Loop | T::Return | T::For | T::In | T::Try | T::Catch => "\x1b[38;2;203;166;247m", _ => { buf += &token.str; continue; } }; let clear = "\x1b[0m"; buf += &format!("{color}{}{clear}", token.str); } buf += &line[last..]; Cow::Owned(buf) } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> std::borrow::Cow<'b, str> { let _ = default; Cow::Owned(format!("\x1b[35m{prompt}\x1b[0m")) } fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Cow::Owned(format!(" \x1b[31;40m Error: \x1b[0;40m{hint} \x1b[0m")) } fn highlight_candidate<'c>( &self, candidate: &'c str, completion: rustyline::CompletionType, ) -> std::borrow::Cow<'c, str> { let _ = completion; Cow::Borrowed(candidate) } fn highlight_char(&self, line: &str, _pos: usize, forced: bool) -> bool { forced || !line.is_empty() } } impl Completer for MatrixHelper { type Candidate = Rc; fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>, ) -> rustyline::Result<(usize, Vec)> { let mut idx = 0; let mut buf = String::new(); let mut start = 0; for char in line.chars() { if buf.is_empty() && (char.is_alphabetic() || char == '_') { start = idx; buf.push(char); } else if !buf.is_empty() && (char.is_alphanumeric() || char == '_') { buf.push(char); } else { if idx >= pos { break; } else { buf.clear(); } } idx += char.len_utf8(); } let _ = (line, pos, ctx); let globals = self.vm.borrow().globals(); let names: Vec> = globals .borrow() .clone() .into_iter() .filter_map(|n| if n.name.starts_with(&buf) { Some(n.name.clone()) } else { None }) .collect(); if buf.is_empty() { start = pos; } Ok((start, names)) } fn update(&self, line: &mut rustyline::line_buffer::LineBuffer, start: usize, elected: &str, cl: &mut rustyline::Changeset) { let end = line.pos(); line.replace(start..end, elected, cl); } }