matrix/matrix-bin/src/helper.rs
2024-03-01 19:51:32 -05:00

323 lines
9.6 KiB
Rust

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<RefCell<Vm>>
}
impl MatrixHelper {
pub fn new(vm: Rc<RefCell<Vm>>) -> 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<ValidationResult> {
let mut lexer = Lexer::new(ctx.input());
let mut stack = Vec::new();
loop {
use TokenData as T;
let token = match lexer.next_token_cmt() {
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)
},
T::Comment => {
let str = token.str.as_str();
let l = str.len();
if l >= 2 && &str[0..2] == "/*" {
if &str[l-2..l] != "*/" {
return Ok(ValidationResult::Incomplete)
}
}
}
_ => {}
};
}
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<usize> {
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<Token> {
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<Rc<str>> {
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_cmt() {
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",
T::Comment => "\x1b[38;2;108;112;134m",
_ => {
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<str>;
fn complete(
&self,
line: &str,
pos: usize,
ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
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<Rc<str>> = 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);
}
}