From 06214cf499ff7a99d4ec4da17d16e8bce9d5de2d Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 30 Jan 2022 09:42:04 +0800 Subject: [PATCH] Add key bindings to repl. --- src/bin/rhai-repl.rs | 369 ++++++++++++++++++++++++++++--------------- 1 file changed, 246 insertions(+), 123 deletions(-) diff --git a/src/bin/rhai-repl.rs b/src/bin/rhai-repl.rs index 6ddc128b..5f60a199 100644 --- a/src/bin/rhai-repl.rs +++ b/src/bin/rhai-repl.rs @@ -1,14 +1,12 @@ -use rustyline::error::ReadlineError; -use rustyline::Editor; use rhai::{Dynamic, Engine, EvalAltResult, Module, Scope, AST, INT}; +use rustyline::config::Builder; +use rustyline::error::ReadlineError; +use rustyline::{Cmd, Editor, Event, EventHandler, KeyCode, KeyEvent, Modifiers, Movement}; +use smallvec::smallvec; -use std::{ - env, - fs::File, - io::{stdin, stdout, Read, Write}, - path::Path, - process::exit, -}; +use std::{env, fs::File, io::Read, path::Path, process::exit}; + +const HISTORY_FILE: &str = ".rhai-repl-history"; /// Pretty-print error. fn print_error(input: &str, mut err: EvalAltResult) { @@ -46,7 +44,9 @@ fn print_error(input: &str, mut err: EvalAltResult) { /// Print help text. fn print_help() { println!("help => print this help"); + println!("keys => print list of key bindings"); println!("quit, exit => quit"); + println!("history => print lines history"); println!("scope => print all variables in the scope"); println!("strict => toggle on/off Strict Variables Mode"); #[cfg(not(feature = "no_optimize"))] @@ -57,7 +57,45 @@ fn print_help() { println!("json => output all functions in JSON format"); println!("ast => print the last AST (optimized)"); println!("astu => print the last raw, un-optimized AST"); - println!(r"end a line with '\' to continue to the next line."); + println!(); + println!("press Shift-Enter to continue to the next line,"); + println!(r"or end a line with '\' (e.g. when pasting code)."); + println!(); +} + +/// Print key bindings. +fn print_keys() { + println!("Home => move to beginning of line"); + println!("Ctrl-Home => move to beginning of input"); + println!("End => move to end of line"); + println!("Ctrl-End => move to end of input"); + println!("Left => move left"); + println!("Ctrl-Left => move left by one word"); + println!("Right => move right by one word"); + println!("Ctrl-Right => move right"); + println!("Up => previous line or history"); + println!("Ctrl-Up => previous history"); + println!("Down => next line or history"); + println!("Ctrl-Down => next history"); + println!("Ctrl-R => reverse search history"); + println!(" (Ctrl-S forward, Ctrl-G cancel)"); + println!("Ctrl-L => clear screen"); + println!("Escape => clear all input"); + println!("Ctrl-C => exit"); + println!("Ctrl-D => EOF (when line empty)"); + println!("Ctrl-H, Backspace => backspace"); + println!("Ctrl-D, Del => delete character"); + println!("Ctrl-U => delete from start"); + println!("Ctrl-W => delete previous word"); + println!("Ctrl-T => transpose characters"); + println!("Ctrl-V => insert special character"); + println!("Ctrl-Y => paste yank"); + println!("Ctrl-Z => suspend (Unix), undo (Windows)"); + println!("Ctrl-_ => undo"); + println!("Enter => run code"); + println!("Shift-Ctrl-Enter => continue to next line"); + println!(); + println!("Plus all standard Emacs key bindings"); println!(); } @@ -82,6 +120,153 @@ fn print_scope(scope: &Scope) { println!(); } +// Load script files specified in the command line. +#[cfg(not(feature = "no_module"))] +#[cfg(not(feature = "no_std"))] +fn load_script_files(engine: &mut Engine) { + // Load init scripts + let mut contents = String::new(); + let mut has_init_scripts = false; + + for filename in env::args().skip(1) { + let filename = match Path::new(&filename).canonicalize() { + Err(err) => { + eprintln!("Error script file path: {}\n{}", filename, err); + exit(1); + } + Ok(f) => { + match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) { + Ok(f) => f.into(), + _ => f, + } + } + }; + + contents.clear(); + + let mut f = match File::open(&filename) { + Err(err) => { + eprintln!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + Ok(f) => f, + }; + + if let Err(err) = f.read_to_string(&mut contents) { + println!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + + let module = match engine + .compile(&contents) + .map_err(|err| err.into()) + .and_then(|mut ast| { + ast.set_source(filename.to_string_lossy().to_string()); + Module::eval_ast_as_new(Scope::new(), &ast, &engine) + }) { + Err(err) => { + let filename = filename.to_string_lossy(); + + eprintln!("{:=<1$}", "", filename.len()); + eprintln!("{}", filename); + eprintln!("{:=<1$}", "", filename.len()); + eprintln!(""); + + print_error(&contents, *err); + exit(1); + } + Ok(m) => m, + }; + + engine.register_global_module(module.into()); + + has_init_scripts = true; + + println!("Script '{}' loaded.", filename.to_string_lossy()); + } + + if has_init_scripts { + println!(); + } +} + +// Setup the Rustyline editor. +fn setup_editor() -> Editor<()> { + let config = Builder::new() + .tab_stop(4) + .indent_size(4) + .bracketed_paste(true) + .build(); + let mut rl = Editor::<()>::with_config(config); + + // Bind more keys + + // On Windows, Esc clears the input buffer + #[cfg(target_family = "windows")] + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Esc, Modifiers::empty())]), + EventHandler::Simple(Cmd::Kill(Movement::WholeBuffer)), + ); + // On Windows, Ctrl-Z is undo + #[cfg(target_family = "windows")] + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent::ctrl('z')]), + EventHandler::Simple(Cmd::Undo(1)), + ); + // Map Shift-Return to insert a new line - bypass need for `\` continuation + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent( + KeyCode::Char('m'), + Modifiers::CTRL_SHIFT + )]), + EventHandler::Simple(Cmd::Newline), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent( + KeyCode::Char('j'), + Modifiers::CTRL_SHIFT + )]), + EventHandler::Simple(Cmd::Newline), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Enter, Modifiers::SHIFT)]), + EventHandler::Simple(Cmd::Newline), + ); + // Map Ctrl-Home and Ctrl-End for beginning/end of input + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Home, Modifiers::CTRL)]), + EventHandler::Simple(Cmd::Move(Movement::BeginningOfBuffer)), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::End, Modifiers::CTRL)]), + EventHandler::Simple(Cmd::Move(Movement::EndOfBuffer)), + ); + // Map Ctrl-Up and Ctrl-Down to skip up/down the history, even through multi-line histories + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Down, Modifiers::CTRL)]), + EventHandler::Simple(Cmd::NextHistory), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Up, Modifiers::CTRL)]), + EventHandler::Simple(Cmd::PreviousHistory), + ); + + // Load the history file + if rl.load_history(HISTORY_FILE).is_err() { + eprintln!("! No previous lines history!"); + } + + rl +} + fn main() { let title = format!("Rhai REPL tool (version {})", env!("CARGO_PKG_VERSION")); println!("{}", title); @@ -95,80 +280,7 @@ fn main() { #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] - { - // Load init scripts - let mut contents = String::new(); - let mut has_init_scripts = false; - - for filename in env::args().skip(1) { - let filename = match Path::new(&filename).canonicalize() { - Err(err) => { - eprintln!("Error script file path: {}\n{}", filename, err); - exit(1); - } - Ok(f) => { - match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) { - Ok(f) => f.into(), - _ => f, - } - } - }; - - contents.clear(); - - let mut f = match File::open(&filename) { - Err(err) => { - eprintln!( - "Error reading script file: {}\n{}", - filename.to_string_lossy(), - err - ); - exit(1); - } - Ok(f) => f, - }; - - if let Err(err) = f.read_to_string(&mut contents) { - println!( - "Error reading script file: {}\n{}", - filename.to_string_lossy(), - err - ); - exit(1); - } - - let module = match engine - .compile(&contents) - .map_err(|err| err.into()) - .and_then(|mut ast| { - ast.set_source(filename.to_string_lossy().to_string()); - Module::eval_ast_as_new(Scope::new(), &ast, &engine) - }) { - Err(err) => { - let filename = filename.to_string_lossy(); - - eprintln!("{:=<1$}", "", filename.len()); - eprintln!("{}", filename); - eprintln!("{:=<1$}", "", filename.len()); - eprintln!(""); - - print_error(&contents, *err); - exit(1); - } - Ok(m) => m, - }; - - engine.register_global_module(module.into()); - - has_init_scripts = true; - - println!("Script '{}' loaded.", filename.to_string_lossy()); - } - - if has_init_scripts { - println!(); - } - } + load_script_files(&mut engine); // Setup Engine #[cfg(not(feature = "no_optimize"))] @@ -194,10 +306,7 @@ fn main() { let mut scope = Scope::new(); // REPL line editor setup - let mut rl = Editor::<()>::new(); - if rl.load_history(".rhai-repl-history.txt").is_err() { - println!("No previous history."); - } + let mut rl = setup_editor(); // REPL loop let mut input = String::new(); @@ -210,46 +319,36 @@ fn main() { 'main_loop: loop { input.clear(); - let readline = rl.readline("rhai-repl> "); + loop { + let prompt = if input.is_empty() { + "rhai-repl> " + } else { + " > " + }; - match readline { - Ok(line) => { - rl.add_history_entry(line.as_str()); - input = line; - }, + match rl.readline(prompt) { + // Line continuation + Ok(line) if line.ends_with("\\") => { + input += &line[..line.len() - 1]; + input.push('\n'); + } + Ok(line) => { + input += line.trim_end(); + if !input.is_empty() { + rl.add_history_entry(input.clone()); + } + break; + } - Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { - break 'main_loop - }, + Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break 'main_loop, - Err(err) => { - eprintln!("Error: {:?}", err); - break 'main_loop + Err(err) => { + eprintln!("Error: {:?}", err); + break 'main_loop; + } } } - //loop { - // match stdin().read_line(&mut input) { - // Ok(0) => break 'main_loop, - // Ok(_) => (), - // Err(err) => panic!("input error: {}", err), - // } - - // let line = input.as_str().trim_end(); - - // // Allow line continuation - // if line.ends_with('\\') { - // let len = line.len(); - // input.truncate(len - 1); - // input.push('\n'); - // } else { - // break; - // } - - // print!("> "); - // stdout().flush().expect("couldn't flush stdout"); - //} - let script = input.trim(); if script.is_empty() { @@ -262,7 +361,29 @@ fn main() { print_help(); continue; } + "keys" => { + print_keys(); + continue; + } "exit" | "quit" => break, // quit + "history" => { + for (i, h) in rl.history().iter().enumerate() { + match &h.split('\n').collect::>()[..] { + [line] => println!("[{}] {}", i + 1, line), + lines => { + for (x, line) in lines.iter().enumerate() { + let number = format!("[{}]", i + 1); + if x == 0 { + println!("{} {}", number, line.trim_end()); + } else { + println!("{0:>1$} {2}", "", number.len(), line.trim_end()); + } + } + } + } + } + continue; + } "strict" if engine.strict_variables() => { engine.set_strict_variables(false); println!("Strict Variables Mode turned OFF."); @@ -365,5 +486,7 @@ fn main() { main_ast.clear_statements(); } - rl.save_history(".rhai-repl-history.txt").unwrap(); + rl.save_history(HISTORY_FILE).unwrap(); + + println!("Bye!"); }