From 24a72ea847b52549cfe305249d413d86f8289765 Mon Sep 17 00:00:00 2001 From: HalidOdat Date: Sat, 25 Jul 2020 22:48:13 +0200 Subject: [PATCH] Added keyword and operator colors and matching bracket validator to cli (#590) --- Cargo.lock | 13 +++++ boa_cli/Cargo.toml | 3 + boa_cli/src/main.rs | 136 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cf66f99ee7..e7b28546ff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,7 +99,10 @@ dependencies = [ "Boa", "colored", "jemallocator", + "lazy_static", + "regex", "rustyline", + "rustyline-derive", "serde_json", "structopt", ] @@ -833,6 +836,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustyline-derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a50e29610a5be68d4a586a5cce3bfb572ed2c2a74227e4168444b7bf4e5235" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ryu" version = "1.0.5" diff --git a/boa_cli/Cargo.toml b/boa_cli/Cargo.toml index 58e0e59b5a1..85df6b0fd8d 100644 --- a/boa_cli/Cargo.toml +++ b/boa_cli/Cargo.toml @@ -13,9 +13,12 @@ edition = "2018" [dependencies] Boa = { path = "../boa", features = ["serde"] } rustyline = "6.2.0" +rustyline-derive = "0.3.1" structopt = "0.3.15" serde_json = "1.0.56" colored = "2.0.0" +regex = "1" +lazy_static = "1.4.0" [target.x86_64-unknown-linux-gnu.dependencies] jemallocator = "0.3.2" diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 8e778c2c878..a666dc4a10d 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -32,7 +32,18 @@ use boa::{ syntax::ast::{node::StatementList, token::Token}, }; use colored::*; -use rustyline::{config::Config, error::ReadlineError, EditMode, Editor}; +use lazy_static::lazy_static; +use regex::{Captures, Regex}; +use rustyline::{ + config::Config, + error::ReadlineError, + highlight::Highlighter, + validate::{MatchingBracketValidator, ValidationContext, ValidationResult, Validator}, + EditMode, Editor, +}; +use rustyline_derive::{Completer, Helper, Hinter}; +use std::borrow::Cow; +use std::collections::HashSet; use std::{fs::read_to_string, path::PathBuf}; use structopt::{clap::arg_enum, StructOpt}; @@ -206,10 +217,14 @@ pub fn main() -> Result<(), std::io::Error> { }) .build(); - let mut editor = Editor::<()>::with_config(config); + let mut editor = Editor::with_config(config); let _ = editor.load_history(CLI_HISTORY); + editor.set_helper(Some(RLHelper { + highlighter: LineHighlighter, + validator: MatchingBracketValidator::new(), + })); - let readline = "> ".cyan().bold().to_string(); + let readline = ">> ".cyan().bold().to_string(); loop { match editor.readline(&readline) { @@ -243,3 +258,118 @@ pub fn main() -> Result<(), std::io::Error> { Ok(()) } + +#[derive(Completer, Helper, Hinter)] +struct RLHelper { + highlighter: LineHighlighter, + validator: MatchingBracketValidator, +} + +impl Validator for RLHelper { + fn validate(&self, ctx: &mut ValidationContext<'_>) -> Result { + self.validator.validate(ctx) + } + + fn validate_while_typing(&self) -> bool { + self.validator.validate_while_typing() + } +} + +impl Highlighter for RLHelper { + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + hint.into() + } + + fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + self.highlighter.highlight(line, pos) + } + + fn highlight_candidate<'c>( + &self, + candidate: &'c str, + _completion: rustyline::CompletionType, + ) -> Cow<'c, str> { + self.highlighter.highlight(candidate, 0) + } + + fn highlight_char(&self, line: &str, _: usize) -> bool { + !line.is_empty() + } +} + +lazy_static! { + static ref KEYWORDS: HashSet<&'static str> = { + let mut keywords = HashSet::new(); + keywords.insert("break"); + keywords.insert("case"); + keywords.insert("catch"); + keywords.insert("class"); + keywords.insert("const"); + keywords.insert("continue"); + keywords.insert("default"); + keywords.insert("delete"); + keywords.insert("do"); + keywords.insert("else"); + keywords.insert("export"); + keywords.insert("extends"); + keywords.insert("finally"); + keywords.insert("for"); + keywords.insert("function"); + keywords.insert("if"); + keywords.insert("import"); + keywords.insert("instanceof"); + keywords.insert("new"); + keywords.insert("return"); + keywords.insert("super"); + keywords.insert("switch"); + keywords.insert("this"); + keywords.insert("throw"); + keywords.insert("try"); + keywords.insert("typeof"); + keywords.insert("var"); + keywords.insert("void"); + keywords.insert("while"); + keywords.insert("with"); + keywords.insert("yield"); + keywords.insert("await"); + keywords.insert("enum"); + keywords.insert("let"); + keywords + }; +} + +struct LineHighlighter; + +impl Highlighter for LineHighlighter { + fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> { + let mut coloured = line.to_string(); + + let reg = Regex::new( + r"(?x) + (?P[$A-z_]+[$A-z_0-9]*) | + (?P[+\-/*%~^!&|=<>,.;:])", + ) + .unwrap(); + + coloured = reg + .replace_all(&coloured, |caps: &Captures<'_>| { + if let Some(cap) = caps.name("identifier") { + match cap.as_str() { + "true" | "false" | "null" | "Infinity" => cap.as_str().purple().to_string(), + "undefined" => cap.as_str().truecolor(100, 100, 100).to_string(), + identifier if KEYWORDS.contains(identifier) => { + cap.as_str().yellow().bold().to_string() + } + _ => cap.as_str().to_string(), + } + } else if let Some(cap) = caps.name("op") { + cap.as_str().green().to_string() + } else { + caps[0].to_string() + } + }) + .to_string(); + + coloured.into() + } +}