From 158bcae00dbe2af50e51468ad003fb594a858e6d Mon Sep 17 00:00:00 2001 From: Freya Murphy Date: Mon, 26 Feb 2024 19:00:42 -0500 Subject: changes --- matrix-bin/Cargo.toml | 2 +- matrix-bin/src/helper.rs | 312 +++++++++++++++++++++++++++++++++++++++++++++++ matrix-bin/src/main.rs | 38 ++++-- matrix-bin/src/repl.rs | 27 +++- 4 files changed, 363 insertions(+), 16 deletions(-) create mode 100644 matrix-bin/src/helper.rs (limited to 'matrix-bin') diff --git a/matrix-bin/Cargo.toml b/matrix-bin/Cargo.toml index c4b4d5a..bdb8fb8 100644 --- a/matrix-bin/Cargo.toml +++ b/matrix-bin/Cargo.toml @@ -12,4 +12,4 @@ clap = { version = "4", features = [ "derive" ] } ctrlc = "3" matrix = { path = "../matrix" } matrix-stdlib = { path = "../matrix-stdlib" } -rustyline = "13" +rustyline = { version = "13", features = [ "derive" ] } diff --git a/matrix-bin/src/helper.rs b/matrix-bin/src/helper.rs new file mode 100644 index 0000000..c0ac5ec --- /dev/null +++ b/matrix-bin/src/helper.rs @@ -0,0 +1,312 @@ +use std::{borrow::Cow, rc::Rc, cell::RefCell}; + +use matrix::{lex::{Lexer, TokenData, Token}, vm::Vm}; +use rustyline::{validate::{Validator, ValidationResult, ValidationContext}, highlight::Highlighter, Helper, Hinter, completion::Completer}; + +#[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::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() { + start = idx; + buf.push(char); + } else if !buf.is_empty() && char.is_alphanumeric() { + buf.push(char); + } else { + if idx >= pos { + break; + } else { + buf.clear(); + } + } + idx += char.len_utf8(); + } + + let _ = (line, pos, ctx); + let globals = self.vm.borrow().global_names(); + let names: Vec> = globals + .borrow() + .clone() + .into_iter() + .filter(|n| n.starts_with(&buf)) + .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); + } +} diff --git a/matrix-bin/src/main.rs b/matrix-bin/src/main.rs index 909ee77..225b353 100644 --- a/matrix-bin/src/main.rs +++ b/matrix-bin/src/main.rs @@ -1,9 +1,10 @@ -use std::{path::PathBuf, io::{self, Read, IsTerminal}, fs}; -use clap::Parser as ArgParser; +use std::{path::PathBuf, io::{self, Read, IsTerminal}, fs, cell::RefCell, rc::Rc}; +use clap::{Parser as ArgParser, ColorChoice}; use matrix::{compiler::{Compiler, CompilerBuilder}, vm::Vm, parse::{Parser, ParserBuilder}, value::Value}; use repl::Repl; mod repl; +mod helper; #[derive(Debug, ArgParser)] #[command(version, long_about = None)] @@ -19,6 +20,10 @@ pub struct Args { #[arg(short, long)] debug: bool, + /// Choses color + #[arg(short, long)] + color: Option, + /// Disables optimizations #[arg(long)] disable_optimizations: bool, @@ -27,8 +32,11 @@ pub struct Args { pub struct State<'a> { parser: Parser, compiler: Compiler<'a>, - vm: Vm, - repl: bool + vm: Rc>, + repl: bool, + color: bool, + #[allow(unused)] + debug: bool, } impl<'a> State<'a> { @@ -60,20 +68,32 @@ impl<'a> State<'a> { matrix_stdlib::load(&mut vm); - (Self { parser, vm, compiler, repl }, file) + let color = match args.color { + Some(ColorChoice::Auto) | None => { + io::stdout().is_terminal() + }, + Some(ColorChoice::Always) => true, + Some(ColorChoice::Never) => false, + }; + + (Self { parser, vm: Rc::new(RefCell::new(vm)), compiler, repl, debug: args.debug, color }, file) } pub fn execute(&mut self, code: String) -> matrix::Result { let ast = self.parser.parse(code)?; let fun = self.compiler.compile(&ast)?; - let val = self.vm.run(fun)?; + let val = self.vm.try_borrow_mut().unwrap().run(fun)?; Ok(val) } } -pub fn error(err: matrix::Error) { - println!("\x1b[31m\x1b[1mError:\x1b[0m {err}"); +pub fn error(err: matrix::Error, state: &State) { + if state.color { + println!("\x1b[31m\x1b[1mError:\x1b[0m {err}"); + } else { + println!("Error: {err}"); + } } fn read_stdin() -> String { @@ -93,7 +113,7 @@ fn main() { if let Some(file) = file { if let Err(err) = state.execute(file) { - error(err); + error(err, &state); } } diff --git a/matrix-bin/src/repl.rs b/matrix-bin/src/repl.rs index 1b5addc..f2964d4 100644 --- a/matrix-bin/src/repl.rs +++ b/matrix-bin/src/repl.rs @@ -1,9 +1,9 @@ use std::{io::Write, sync::atomic::Ordering}; use matrix::{value::Value, vm::Interupt}; -use rustyline::Config; +use rustyline::{Config, EditMode, ColorMode, Editor, CompletionType}; -use crate::State; +use crate::{State, helper::MatrixHelper}; pub struct Repl<'a> { state: State<'a> @@ -17,24 +17,39 @@ impl<'a> Repl<'a> { pub fn run(&mut self) { - let interupt = self.state.vm.interupt(); + let interupt = self.state.vm.borrow().interupt(); ctrlc::set_handler(move || { interupt.store(Interupt::KeyboardInterupt as usize, Ordering::SeqCst); }).unwrap(); let config = Config::builder() .check_cursor_position(true) + .completion_type(CompletionType::List) + .edit_mode(EditMode::Emacs) + .color_mode(if self.state.color { ColorMode::Enabled } else { ColorMode::Disabled }) .build(); - let mut rl = rustyline::DefaultEditor::with_config(config).unwrap(); + + let helper = MatrixHelper::new(self.state.vm.clone()); + + let mut rl = Editor::with_config(config).unwrap(); + rl.set_helper(Some(helper)); + loop { let Ok(line) = rl.readline(">> ") else { break; }; + if let Err(_) = rl.add_history_entry(&line) { + break; + }; match self.state.execute(line) { - Err(err) => crate::error(err), + Err(err) => crate::error(err, &self.state), Ok(val) => { if val != Value::Nil { - println!("{val}"); + if self.state.color { + println!("{val:#}"); + } else { + println!("{val}"); + } } } } -- cgit v1.2.3-freya