From 2a62a1c714cb92e92253dd72fc9dd4ba0a6d64c2 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 19 Apr 2024 07:30:17 -0500 Subject: [PATCH] repline: Promote to its own crate! cl-repl: Major refactor based on the design of typeck.rs --- Cargo.toml | 1 + cl-repl/Cargo.toml | 3 +- cl-repl/examples/typeck.rs | 67 ++-- cl-repl/examples/yaml.rs | 2 +- cl-repl/src/ansi.rs | 14 + cl-repl/src/args.rs | 51 ++++ cl-repl/src/bin/conlang.rs | 12 +- cl-repl/src/cli.rs | 89 ++++++ cl-repl/src/ctx.rs | 26 ++ cl-repl/src/lib.rs | 453 +-------------------------- cl-repl/src/menu.rs | 76 +++++ cl-repl/src/repline.rs | 607 ------------------------------------- cl-repl/src/tools.rs | 11 + repline/Cargo.toml | 11 + repline/src/editor.rs | 283 +++++++++++++++++ repline/src/error.rs | 42 +++ repline/src/iter.rs | 68 +++++ repline/src/lib.rs | 13 + repline/src/prebaked.rs | 56 ++++ repline/src/raw.rs | 21 ++ repline/src/repline.rs | 175 +++++++++++ 21 files changed, 967 insertions(+), 1114 deletions(-) create mode 100644 cl-repl/src/ansi.rs create mode 100644 cl-repl/src/args.rs create mode 100644 cl-repl/src/cli.rs create mode 100644 cl-repl/src/ctx.rs create mode 100644 cl-repl/src/menu.rs delete mode 100644 cl-repl/src/repline.rs create mode 100644 cl-repl/src/tools.rs create mode 100644 repline/Cargo.toml create mode 100644 repline/src/editor.rs create mode 100644 repline/src/error.rs create mode 100644 repline/src/iter.rs create mode 100644 repline/src/lib.rs create mode 100644 repline/src/prebaked.rs create mode 100644 repline/src/raw.rs create mode 100644 repline/src/repline.rs diff --git a/Cargo.toml b/Cargo.toml index 6359944..8555030 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "cl-ast", "cl-parser", "cl-lexer", + "repline", ] resolver = "2" diff --git a/cl-repl/Cargo.toml b/cl-repl/Cargo.toml index e8caf3f..5ed4b2f 100644 --- a/cl-repl/Cargo.toml +++ b/cl-repl/Cargo.toml @@ -15,9 +15,8 @@ cl-lexer = { path = "../cl-lexer" } cl-token = { path = "../cl-token" } cl-parser = { path = "../cl-parser" } cl-interpret = { path = "../cl-interpret" } -crossterm = "0.27.0" +repline = { path = "../repline" } argh = "0.1.12" [dev-dependencies] -cl-structures = { path = "../cl-structures" } cl-typeck = { path = "../cl-typeck" } diff --git a/cl-repl/examples/typeck.rs b/cl-repl/examples/typeck.rs index 5d38823..54e5529 100644 --- a/cl-repl/examples/typeck.rs +++ b/cl-repl/examples/typeck.rs @@ -4,10 +4,10 @@ use cl_ast::{ }; use cl_lexer::Lexer; use cl_parser::Parser; -use cl_repl::repline::{error::Error as RlError, Repline}; use cl_typeck::{ definition::Def, name_collector::NameCollectable, project::Project, type_resolver::resolve, }; +use repline::{error::Error as RlError, prebaked::*}; use std::error::Error; // Path to display in standard library errors @@ -40,53 +40,21 @@ fn main() -> Result<(), Box> { unsafe { TREES.push(code) }.collect_in_root(&mut prj)?; - main_menu(&mut prj) -} - -pub enum Response { - Accept, - Deny, - Break, -} - -fn read_and( - color: &str, - begin: &str, - mut f: impl FnMut(&str) -> Result>, -) -> Result<(), Box> { - let mut rl = Repline::new(color, begin, "? >"); - loop { - let line = match rl.read() { - Err(RlError::CtrlC(_)) => break, - Err(RlError::CtrlD(line)) => { - rl.deny(); - line - } - Ok(line) => line, - Err(e) => Err(e)?, - }; - print!("\x1b[G\x1b[J"); - match f(&line) { - Ok(Response::Accept) => rl.accept(), - Ok(Response::Deny) => rl.deny(), - Ok(Response::Break) => break, - Err(e) => print!("\x1b[40G\x1bJ\x1b[91m{e}\x1b[0m"), - } - } + main_menu(&mut prj)?; Ok(()) } -fn main_menu(prj: &mut Project) -> Result<(), Box> { +fn main_menu(prj: &mut Project) -> Result<(), RlError> { banner(); - read_and(C_MAIN, "mu>", |line| { + read_and(C_MAIN, "mu>", "? >", |line| { match line.trim() { - "c" | "code" => enter_code(prj), - "clear" => clear(), + "c" | "code" => enter_code(prj)?, + "clear" => clear()?, "e" | "exit" => return Ok(Response::Break), "l" | "list" => list_types(prj), - "q" | "query" => query_type_expression(prj), - "r" | "resolve" => resolve_all(prj), - "d" | "desugar" => live_desugar(), + "q" | "query" => query_type_expression(prj)?, + "r" | "resolve" => resolve_all(prj)?, + "d" | "desugar" => live_desugar()?, "h" | "help" => { println!( "Valid commands are: @@ -102,12 +70,12 @@ fn main_menu(prj: &mut Project) -> Result<(), Box> { } _ => Err(r#"Invalid command. Type "help" to see the list of valid commands."#)?, } - .map(|_| Response::Accept) + Ok(Response::Accept) }) } -fn enter_code(prj: &mut Project) -> Result<(), Box> { - read_and(C_CODE, "cl>", |line| { +fn enter_code(prj: &mut Project) -> Result<(), RlError> { + read_and(C_CODE, "cl>", "? >", |line| { if line.trim().is_empty() { return Ok(Response::Break); } @@ -120,8 +88,8 @@ fn enter_code(prj: &mut Project) -> Result<(), Box> { }) } -fn live_desugar() -> Result<(), Box> { - read_and(C_RESV, "se>", |line| { +fn live_desugar() -> Result<(), RlError> { + read_and(C_RESV, "se>", "? >", |line| { let code = Parser::new(Lexer::new(line)).stmt()?; println!("Raw, as parsed:\n{C_LISTING}{code}\x1b[0m"); @@ -135,8 +103,8 @@ fn live_desugar() -> Result<(), Box> { }) } -fn query_type_expression(prj: &mut Project) -> Result<(), Box> { - read_and(C_RESV, "ty>", |line| { +fn query_type_expression(prj: &mut Project) -> Result<(), RlError> { + read_and(C_RESV, "ty>", "? >", |line| { if line.trim().is_empty() { return Ok(Response::Break); } @@ -156,7 +124,7 @@ fn resolve_all(prj: &mut Project) -> Result<(), Box> { Ok(()) } -fn list_types(prj: &mut Project) -> Result<(), Box> { +fn list_types(prj: &mut Project) { println!(" name\x1b[30G type"); for (idx, Def { name, vis, kind, .. }) in prj.pool.iter().enumerate() { print!("{idx:3}: {vis}"); @@ -165,7 +133,6 @@ fn list_types(prj: &mut Project) -> Result<(), Box> { } println!("{name}\x1b[30G| {kind}"); } - Ok(()) } fn pretty_def(def: &Def, id: impl Into) { diff --git a/cl-repl/examples/yaml.rs b/cl-repl/examples/yaml.rs index 1e18ce2..14984b4 100644 --- a/cl-repl/examples/yaml.rs +++ b/cl-repl/examples/yaml.rs @@ -2,7 +2,7 @@ use cl_lexer::Lexer; use cl_parser::Parser; -use cl_repl::repline::{error::Error as RlError, Repline}; +use repline::{error::Error as RlError, Repline}; use std::error::Error; fn main() -> Result<(), Box> { diff --git a/cl-repl/src/ansi.rs b/cl-repl/src/ansi.rs new file mode 100644 index 0000000..0e537e8 --- /dev/null +++ b/cl-repl/src/ansi.rs @@ -0,0 +1,14 @@ +//! ANSI escape sequences + +pub const RED: &str = "\x1b[31m"; +pub const GREEN: &str = "\x1b[32m"; // the color of type checker mode +pub const CYAN: &str = "\x1b[36m"; +pub const BRIGHT_GREEN: &str = "\x1b[92m"; +pub const BRIGHT_BLUE: &str = "\x1b[94m"; +pub const BRIGHT_MAGENTA: &str = "\x1b[95m"; +pub const BRIGHT_CYAN: &str = "\x1b[96m"; +pub const RESET: &str = "\x1b[0m"; +pub const OUTPUT: &str = "\x1b[38;5;117m"; + +pub const CLEAR_LINES: &str = "\x1b[G\x1b[J"; +pub const CLEAR_ALL: &str = "\x1b[H\x1b[2J"; diff --git a/cl-repl/src/args.rs b/cl-repl/src/args.rs new file mode 100644 index 0000000..40265ba --- /dev/null +++ b/cl-repl/src/args.rs @@ -0,0 +1,51 @@ +//! Handles argument parsing (currently using the [argh] crate) + +use argh::FromArgs; +use std::{io::IsTerminal, path::PathBuf, str::FromStr}; + +/// The Conlang prototype debug interface +#[derive(Clone, Debug, FromArgs, PartialEq, Eq, PartialOrd, Ord)] +pub struct Args { + /// the main source file + #[argh(positional)] + pub file: Option, + + /// files to include + #[argh(option, short = 'I')] + pub include: Vec, + + /// the CLI operating mode (`f`mt | `l`ex | `r`un) + #[argh(option, short = 'm', default = "Default::default()")] + pub mode: Mode, + + /// whether to start the repl (`true` or `false`) + #[argh(option, short = 'r', default = "is_terminal()")] + pub repl: bool, +} + +/// gets whether stdin AND stdout are a terminal, for pipelining +pub fn is_terminal() -> bool { + std::io::stdin().is_terminal() && std::io::stdout().is_terminal() +} + +/// The CLI's operating mode +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum Mode { + #[default] + Menu, + Lex, + Fmt, + Run, +} + +impl FromStr for Mode { + type Err = &'static str; + fn from_str(s: &str) -> Result { + Ok(match s { + "f" | "fmt" | "p" | "pretty" => Mode::Fmt, + "l" | "lex" | "tokenize" | "token" => Mode::Lex, + "r" | "run" => Mode::Run, + _ => Err("Recognized modes are: 'r' \"run\", 'f' \"fmt\", 'l' \"lex\"")?, + }) + } +} diff --git a/cl-repl/src/bin/conlang.rs b/cl-repl/src/bin/conlang.rs index 5653326..06ea88a 100644 --- a/cl-repl/src/bin/conlang.rs +++ b/cl-repl/src/bin/conlang.rs @@ -1,13 +1,5 @@ -use cl_repl::{cli::run, tools::is_terminal}; -use std::error::Error; +use cl_repl::cli::run; -fn main() -> Result<(), Box> { - if is_terminal() { - println!( - "--- {} v{} 💪🦈 ---", - env!("CARGO_BIN_NAME"), - env!("CARGO_PKG_VERSION"), - ); - } +fn main() -> Result<(), Box> { run(argh::from_env()) } diff --git a/cl-repl/src/cli.rs b/cl-repl/src/cli.rs new file mode 100644 index 0000000..fa9c7bf --- /dev/null +++ b/cl-repl/src/cli.rs @@ -0,0 +1,89 @@ +//! Implement's the command line interface +use crate::{ + args::{Args, Mode}, + ctx::Context, + menu, + tools::print_token, +}; +use cl_interpret::{env::Environment, interpret::Interpret, temp_type_impl::ConValue}; +use cl_lexer::Lexer; +use cl_parser::Parser; +use std::{error::Error, path::Path}; + +/// Run the command line interface +pub fn run(args: Args) -> Result<(), Box> { + let Args { file, include, mode, repl } = args; + + let mut env = Environment::new(); + for path in include { + load_file(&mut env, path)?; + } + + if repl { + if let Some(file) = file { + load_file(&mut env, file)?; + } + let mut ctx = Context::with_env(env); + match mode { + Mode::Menu => menu::main_menu(&mut ctx)?, + Mode::Lex => menu::lex(&mut ctx)?, + Mode::Fmt => menu::fmt(&mut ctx)?, + Mode::Run => menu::run(&mut ctx)?, + } + } else { + let code = match &file { + Some(file) => std::fs::read_to_string(file)?, + None => std::io::read_to_string(std::io::stdin())?, + }; + + match mode { + Mode::Lex => lex_code(&code, file), + Mode::Fmt => fmt_code(&code), + Mode::Run | Mode::Menu => run_code(&code, &mut env), + }?; + } + Ok(()) +} + +fn load_file( + env: &mut Environment, + path: impl AsRef, +) -> Result> { + let file = std::fs::read_to_string(path)?; + let code = Parser::new(Lexer::new(&file)).file()?; + Ok(env.eval(&code)?) +} + +fn lex_code(code: &str, path: Option>) -> Result<(), Box> { + for token in Lexer::new(code) { + if let Some(path) = &path { + print!("{}:", path.as_ref().display()); + } + match token { + Ok(token) => print_token(&token), + Err(e) => println!("{e}"), + } + } + Ok(()) +} + +fn fmt_code(code: &str) -> Result<(), Box> { + let code = Parser::new(Lexer::new(code)).file()?; + println!("{code}"); + Ok(()) +} + +fn run_code(code: &str, env: &mut Environment) -> Result<(), Box> { + let code = Parser::new(Lexer::new(code)).file()?; + match code.interpret(env)? { + ConValue::Empty => {} + ret => println!("{ret}"), + } + if env.get("main").is_ok() { + match env.call("main", &[])? { + ConValue::Empty => {} + ret => println!("{ret}"), + } + } + Ok(()) +} diff --git a/cl-repl/src/ctx.rs b/cl-repl/src/ctx.rs new file mode 100644 index 0000000..11686a8 --- /dev/null +++ b/cl-repl/src/ctx.rs @@ -0,0 +1,26 @@ +use cl_interpret::{ + env::Environment, error::IResult, interpret::Interpret, temp_type_impl::ConValue, +}; + +#[derive(Clone, Debug)] +pub struct Context { + pub env: Environment, +} + +impl Context { + pub fn new() -> Self { + Self { env: Environment::new() } + } + pub fn with_env(env: Environment) -> Self { + Self { env } + } + pub fn run(&mut self, code: &impl Interpret) -> IResult { + code.interpret(&mut self.env) + } +} + +impl Default for Context { + fn default() -> Self { + Self::new() + } +} diff --git a/cl-repl/src/lib.rs b/cl-repl/src/lib.rs index 7d3ca07..84d9e0c 100644 --- a/cl-repl/src/lib.rs +++ b/cl-repl/src/lib.rs @@ -1,446 +1,11 @@ -//! Utilities for cl-frontend -//! -//! # TODO -//! - [ ] Readline-like line editing -//! - [ ] Raw mode? +//! The Conlang REPL, based on [repline] +//! +//! Uses [argh] for argument parsing. #![warn(clippy::all)] -pub mod ansi { - // ANSI color escape sequences - pub const ANSI_RED: &str = "\x1b[31m"; - pub const ANSI_GREEN: &str = "\x1b[32m"; // the color of type checker mode - pub const ANSI_CYAN: &str = "\x1b[36m"; - // pub const ANSI_BRIGHT_GREEN: &str = "\x1b[92m"; - pub const ANSI_BRIGHT_BLUE: &str = "\x1b[94m"; - pub const ANSI_BRIGHT_MAGENTA: &str = "\x1b[95m"; - // const ANSI_BRIGHT_CYAN: &str = "\x1b[96m"; - pub const ANSI_RESET: &str = "\x1b[0m"; - pub const ANSI_OUTPUT: &str = "\x1b[38;5;117m"; - - pub const ANSI_CLEAR_LINES: &str = "\x1b[G\x1b[J"; -} - -pub mod args { - use crate::tools::is_terminal; - use argh::FromArgs; - use std::{path::PathBuf, str::FromStr}; - - /// The Conlang prototype debug interface - #[derive(Clone, Debug, FromArgs, PartialEq, Eq, PartialOrd, Ord)] - pub struct Args { - /// the main source file - #[argh(positional)] - pub file: Option, - - /// files to include - #[argh(option, short = 'I')] - pub include: Vec, - - /// the Repl mode to start in - #[argh(option, short = 'm', default = "Default::default()")] - pub mode: Mode, - - /// whether to start the repl (`true` or `false`) - #[argh(option, short = 'r', default = "is_terminal()")] - pub repl: bool, - } - - /// The CLI's operating mode - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] - pub enum Mode { - Tokenize, - Beautify, - #[default] - Interpret, - } - - impl Mode { - pub fn ansi_color(self) -> &'static str { - use super::ansi::*; - match self { - Mode::Tokenize => ANSI_BRIGHT_BLUE, - Mode::Beautify => ANSI_BRIGHT_MAGENTA, - Mode::Interpret => ANSI_CYAN, - } - } - } - - impl FromStr for Mode { - type Err = &'static str; - fn from_str(s: &str) -> Result { - Ok(match s { - "i" | "interpret" | "r" | "run" => Mode::Interpret, - "b" | "beautify" | "p" | "pretty" => Mode::Beautify, - "t" | "tokenize" | "token" => Mode::Tokenize, - _ => Err("Recognized modes are: 'r' \"run\", 'p' \"pretty\", 't' \"token\"")?, - }) - } - } -} - -pub mod program { - use cl_ast::ast; - use cl_interpret::{ - env::Environment, error::IResult, interpret::Interpret, temp_type_impl::ConValue, - }; - use cl_lexer::Lexer; - use cl_parser::{error::PResult, Parser}; - use std::fmt::Display; - - pub struct Parsable; - - pub enum Parsed { - File(ast::File), - Stmt(ast::Stmt), - } - - pub struct Program<'t, Variant> { - text: &'t str, - data: Variant, - } - impl<'t, V> Program<'t, V> { - pub fn lex(&self) -> Lexer { - Lexer::new(self.text) - } - } - - impl<'t> Program<'t, Parsable> { - pub fn new(text: &'t str) -> Self { - Self { text, data: Parsable } - } - pub fn parse(self) -> PResult> { - self.parse_file().or_else(|_| self.parse_stmt()) - } - pub fn parse_stmt(&self) -> PResult> { - let stmt = Parser::new(self.lex()).stmt()?; - // let stmt = WhileElseDesugar.fold_stmt(stmt); - - Ok(Program { data: Parsed::Stmt(stmt), text: self.text }) - } - pub fn parse_file(&self) -> PResult> { - let file = Parser::new(self.lex()).file()?; - // let file = WhileElseDesugar.fold_file(file); - - Ok(Program { data: Parsed::File(file), text: self.text }) - } - } - - impl<'t> Program<'t, Parsed> { - pub fn debug(&self) { - match &self.data { - Parsed::File(v) => eprintln!("{v:?}"), - Parsed::Stmt(v) => eprintln!("{v:?}"), - } - } - pub fn print(&self) { - match &self.data { - Parsed::File(v) => println!("{v}"), - Parsed::Stmt(v) => println!("{v}"), - }; - } - - pub fn run(&self, env: &mut Environment) -> IResult { - match &self.data { - Parsed::File(v) => v.interpret(env), - Parsed::Stmt(v) => v.interpret(env), - } - } - } - - impl<'t> Display for Program<'t, Parsed> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.data { - Parsed::File(v) => write!(f, "{v}"), - Parsed::Stmt(v) => write!(f, "{v}"), - } - } - } -} - -pub mod cli { - //! Implement's the command line interface - use crate::{ - args::{Args, Mode}, - program::{Parsable, Program}, - repl::Repl, - tools::print_token, - }; - use cl_interpret::{env::Environment, temp_type_impl::ConValue}; - use cl_lexer::Lexer; - use cl_parser::Parser; - use std::{error::Error, path::Path}; - - /// Run the command line interface - pub fn run(args: Args) -> Result<(), Box> { - let Args { file, include, mode, repl } = args; - - let mut env = Environment::new(); - for path in include { - load_file(&mut env, path)?; - } - - if repl { - if let Some(file) = file { - load_file(&mut env, file)?; - } - Repl::with_env(mode, env).repl() - } else { - let code = match &file { - Some(file) => std::fs::read_to_string(file)?, - None => std::io::read_to_string(std::io::stdin())?, - }; - let code = Program::new(&code); - - match mode { - Mode::Tokenize => tokenize(code, file), - Mode::Beautify => beautify(code), - Mode::Interpret => interpret(code, &mut env), - }?; - } - Ok(()) - } - - fn load_file( - env: &mut Environment, - path: impl AsRef, - ) -> Result> { - let file = std::fs::read_to_string(path)?; - let code = Parser::new(Lexer::new(&file)).file()?; - env.eval(&code).map_err(Into::into) - } - - fn tokenize( - code: Program, - path: Option>, - ) -> Result<(), Box> { - for token in code.lex() { - if let Some(ref path) = path { - print!("{}:", path.as_ref().display()); - } - match token { - Ok(token) => print_token(&token), - Err(e) => println!("{e}"), - } - } - Ok(()) - } - - fn beautify(code: Program) -> Result<(), Box> { - code.parse()?.print(); - Ok(()) - } - - fn interpret(code: Program, env: &mut Environment) -> Result<(), Box> { - match code.parse()?.run(env)? { - ConValue::Empty => {} - ret => println!("{ret}"), - } - if env.get("main").is_ok() { - match env.call("main", &[])? { - ConValue::Empty => {} - ret => println!("{ret}"), - } - } - Ok(()) - } -} - -pub mod repl { - use crate::{ - ansi::*, - args::Mode, - program::{Parsable, Parsed, Program}, - tools::print_token, - }; - use cl_interpret::{env::Environment, temp_type_impl::ConValue}; - use std::fmt::Display; - - /// Implements the interactive interpreter - #[derive(Clone, Debug)] - pub struct Repl { - prompt_again: &'static str, // " ?>" - prompt_begin: &'static str, // "cl>" - prompt_error: &'static str, // "! >" - prompt_succs: &'static str, // " ->" - env: Environment, - mode: Mode, - } - - impl Default for Repl { - fn default() -> Self { - Self { - prompt_begin: "cl>", - prompt_again: " ?>", - prompt_error: "! >", - prompt_succs: " =>", - env: Default::default(), - mode: Default::default(), - } - } - } - - /// Prompt functions - impl Repl { - pub fn prompt_result(&self, res: Result) { - match &res { - Ok(v) => self.prompt_succs(v), - Err(e) => self.prompt_error(e), - } - } - pub fn prompt_error(&self, err: &impl Display) { - let Self { prompt_error: prompt, .. } = self; - println!("{ANSI_CLEAR_LINES}{ANSI_RED}{prompt} {err}{ANSI_RESET}") - } - pub fn prompt_succs(&self, value: &impl Display) { - let Self { prompt_succs: _prompt, .. } = self; - println!("{ANSI_GREEN}{value}{ANSI_RESET}") - } - /// Resets the cursor to the start of the line, clears the terminal, - /// and sets the output color - pub fn begin_output(&self) { - print!("{ANSI_CLEAR_LINES}{ANSI_OUTPUT}") - } - pub fn clear_line(&self) {} - } - /// The actual REPL - impl Repl { - /// Constructs a new [Repl] with the provided [Mode] - pub fn new(mode: Mode) -> Self { - Self { mode, ..Default::default() } - } - /// Constructs a new [Repl] with the provided [Mode] and [Environment] - pub fn with_env(mode: Mode, env: Environment) -> Self { - Self { mode, env, ..Default::default() } - } - /// Runs the main REPL loop - pub fn repl(&mut self) { - use crate::repline::{error::Error, Repline}; - - let mut rl = Repline::new(self.mode.ansi_color(), self.prompt_begin, self.prompt_again); - fn clear_line() { - print!("\x1b[G\x1b[J"); - } - loop { - let buf = match rl.read() { - Ok(buf) => buf, - // Ctrl-C: break if current line is empty - Err(Error::CtrlC(buf)) => { - if buf.is_empty() || buf.ends_with('\n') { - return; - } - rl.accept(); - println!("Cancelled. (Press Ctrl+C again to quit.)"); - continue; - } - // Ctrl-D: reset input, and parse it for errors - Err(Error::CtrlD(buf)) => { - rl.deny(); - if let Err(e) = Program::new(&buf).parse() { - clear_line(); - self.prompt_error(&e); - } - continue; - } - Err(e) => { - self.prompt_error(&e); - return; - } - }; - - self.begin_output(); - if self.command(&buf) { - rl.deny(); - rl.set_color(self.mode.ansi_color()); - continue; - } - let code = Program::new(&buf); - if self.mode == Mode::Tokenize { - self.tokenize(&code); - rl.deny(); - continue; - } - match code.lex().into_iter().find(|l| l.is_err()) { - None => {} - Some(Ok(_)) => unreachable!(), - Some(Err(error)) => { - rl.deny(); - self.prompt_error(&error); - continue; - } - } - if let Ok(mut code) = code.parse() { - rl.accept(); - self.dispatch(&mut code); - } - } - } - - fn help(&self) { - println!( - "Commands:\n- $tokens\n Tokenize Mode:\n Outputs information derived by the Lexer\n- $pretty\n Beautify Mode:\n Pretty-prints the input\n- $run\n Interpret Mode:\n Interprets the input using Conlang\'s work-in-progress interpreter\n- $mode\n Prints the current mode\n- $help\n Prints this help message" - ); - } - fn command(&mut self, line: &str) -> bool { - let Some(line) = line.trim().strip_prefix('$') else { - return false; - }; - if let Ok(mode) = line.parse() { - self.mode = mode; - } else { - match line { - "$run" => self.mode = Mode::Interpret, - "mode" => println!("{:?} Mode", self.mode), - "help" => self.help(), - _ => return false, - } - } - true - } - /// Dispatches calls to repl functions based on the program - fn dispatch(&mut self, code: &mut Program) { - match self.mode { - Mode::Tokenize => {} - Mode::Beautify => self.beautify(code), - Mode::Interpret => self.interpret(code), - } - } - fn tokenize(&mut self, code: &Program) { - for token in code.lex() { - match token { - Ok(token) => print_token(&token), - Err(e) => println!("{e}"), - } - } - } - fn interpret(&mut self, code: &Program) { - match code.run(&mut self.env) { - Ok(ConValue::Empty) => {} - res => self.prompt_result(res), - } - } - fn beautify(&mut self, code: &Program) { - code.print() - } - } -} - -pub mod tools { - use cl_token::Token; - use std::io::IsTerminal; - /// Prints a token in the particular way cl-repl does - pub fn print_token(t: &Token) { - println!( - "{:02}:{:02}: {:#19} │{}│", - t.line(), - t.col(), - t.ty(), - t.data(), - ) - } - /// gets whether stdin AND stdout are a terminal, for pipelining - pub fn is_terminal() -> bool { - std::io::stdin().is_terminal() && std::io::stdout().is_terminal() - } -} - -pub mod repline; +pub mod ansi; +pub mod args; +pub mod cli; +pub mod ctx; +pub mod menu; +pub mod tools; diff --git a/cl-repl/src/menu.rs b/cl-repl/src/menu.rs new file mode 100644 index 0000000..e6e7779 --- /dev/null +++ b/cl-repl/src/menu.rs @@ -0,0 +1,76 @@ +use crate::{ansi, ctx}; +use cl_lexer::Lexer; +use cl_parser::Parser; +use repline::{error::ReplResult, prebaked::*}; + +fn clear() { + println!("{}", ansi::CLEAR_ALL); + banner() +} + +pub fn banner() { + println!("--- conlang v{} 💪🦈 ---\n", env!("CARGO_PKG_VERSION")) +} + +/// Presents a selection interface to the user +pub fn main_menu(ctx: &mut ctx::Context) -> ReplResult<()> { + banner(); + read_and(ansi::GREEN, "mu>", " ?>", |line| { + match line.trim() { + "clear" => clear(), + "l" | "lex" => lex(ctx)?, + "f" | "fmt" => fmt(ctx)?, + "r" | "run" => run(ctx)?, + "q" | "quit" => return Ok(Response::Break), + "h" | "help" => println!( + "Valid commands + lex (l): Spin up a lexer, and lex some lines + fmt (f): Format the input + run (r): Enter the REPL, and evaluate some statements + help (h): Print this list + quit (q): Exit the program" + ), + _ => Err("Unknown command. Type \"help\" for help")?, + } + Ok(Response::Accept) + }) +} + +pub fn run(ctx: &mut ctx::Context) -> ReplResult<()> { + read_and(ansi::CYAN, "cl>", " ?>", |line| { + let code = Parser::new(Lexer::new(line)).stmt()?; + + print!("{}", ansi::OUTPUT); + match ctx.run(&code) { + Ok(v) => println!("{}{v}", ansi::RESET), + Err(e) => println!("{}! > {e}{}", ansi::RED, ansi::RESET), + } + Ok(Response::Accept) + }) +} + +pub fn lex(_ctx: &mut ctx::Context) -> ReplResult<()> { + read_and(ansi::BRIGHT_BLUE, "lx>", " ?>", |line| { + for token in Lexer::new(line) { + match token { + Ok(token) => crate::tools::print_token(&token), + Err(e) => eprintln!("! > {}{e}{}", ansi::RED, ansi::RESET), + } + } + + Ok(Response::Accept) + }) +} + +pub fn fmt(_ctx: &mut ctx::Context) -> ReplResult<()> { + read_and(ansi::BRIGHT_MAGENTA, "cl>", " ?>", |line| { + let mut p = Parser::new(Lexer::new(line)); + + match p.stmt() { + Ok(code) => println!("{}{code}{}", ansi::OUTPUT, ansi::RESET), + Err(e) => Err(e)?, + } + + Ok(Response::Accept) + }) +} diff --git a/cl-repl/src/repline.rs b/cl-repl/src/repline.rs deleted file mode 100644 index 20ad64c..0000000 --- a/cl-repl/src/repline.rs +++ /dev/null @@ -1,607 +0,0 @@ -//! A small pseudo-multiline editing library -// #![allow(unused)] - -pub mod error { - /// Result type for Repline - pub type ReplResult = std::result::Result; - /// Borrowed error (does not implement [Error](std::error::Error)!) - #[derive(Debug)] - pub enum Error { - /// User broke with Ctrl+C - CtrlC(String), - /// User broke with Ctrl+D - CtrlD(String), - /// Invalid unicode codepoint - BadUnicode(u32), - /// Error came from [std::io] - IoFailure(std::io::Error), - /// End of input - EndOfInput, - } - - impl std::error::Error for Error {} - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::CtrlC(_) => write!(f, "Ctrl+C"), - Error::CtrlD(_) => write!(f, "Ctrl+D"), - Error::BadUnicode(u) => write!(f, "0x{u:x} is not a valid unicode codepoint"), - Error::IoFailure(s) => write!(f, "{s}"), - Error::EndOfInput => write!(f, "End of input"), - } - } - } - impl From for Error { - fn from(value: std::io::Error) -> Self { - Self::IoFailure(value) - } - } -} - -pub mod chars { - //! Converts an [Iterator] into an - //! [Iterator] - - use super::error::*; - - /// Converts an [Iterator] into an - /// [Iterator] - #[derive(Clone, Debug)] - pub struct Chars>(pub I); - impl> Chars { - pub fn new(bytes: I) -> Self { - Self(bytes) - } - } - impl> Iterator for Chars { - type Item = ReplResult; - fn next(&mut self) -> Option { - let Self(bytes) = self; - let start = bytes.next()? as u32; - let (mut out, count) = match start { - start if start & 0x80 == 0x00 => (start, 0), // ASCII valid range - start if start & 0xe0 == 0xc0 => (start & 0x1f, 1), // 1 continuation byte - start if start & 0xf0 == 0xe0 => (start & 0x0f, 2), // 2 continuation bytes - start if start & 0xf8 == 0xf0 => (start & 0x07, 3), // 3 continuation bytes - _ => return None, - }; - for _ in 0..count { - let cont = bytes.next()? as u32; - if cont & 0xc0 != 0x80 { - return None; - } - out = out << 6 | (cont & 0x3f); - } - Some(char::from_u32(out).ok_or(Error::BadUnicode(out))) - } - } -} - -pub mod flatten { - //! Flattens an [Iterator] returning [`Result`](Result) or [`Option`](Option) - //! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T` - - /// Flattens an [Iterator] returning [`Result`](Result) or [`Option`](Option) - /// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T` - pub struct Flatten>(pub I); - impl>> Iterator for Flatten, I> { - type Item = T; - fn next(&mut self) -> Option { - self.0.next()?.ok() - } - } - impl>> Iterator for Flatten, I> { - type Item = T; - fn next(&mut self) -> Option { - self.0.next()? - } - } -} - -pub mod raw { - //! Sets the terminal to [`raw`] mode for the duration of the returned object's lifetime. - - /// Sets the terminal to raw mode for the duration of the returned object's lifetime. - pub fn raw() -> impl Drop { - Raw::default() - } - struct Raw(); - impl Default for Raw { - fn default() -> Self { - std::thread::yield_now(); - crossterm::terminal::enable_raw_mode() - .expect("should be able to transition into raw mode"); - Raw() - } - } - impl Drop for Raw { - fn drop(&mut self) { - crossterm::terminal::disable_raw_mode() - .expect("should be able to transition out of raw mode"); - // std::thread::yield_now(); - } - } -} - -mod out { - #![allow(unused)] - use std::io::{Result, Write}; - - /// A [Writer](Write) that flushes after every wipe - #[derive(Clone, Debug)] - pub(super) struct EagerWriter { - out: W, - } - impl EagerWriter { - pub fn new(writer: W) -> Self { - Self { out: writer } - } - } - impl Write for EagerWriter { - fn write(&mut self, buf: &[u8]) -> Result { - let out = self.out.write(buf)?; - self.out.flush()?; - Ok(out) - } - fn flush(&mut self) -> Result<()> { - self.out.flush() - } - } -} - -use self::{chars::Chars, editor::Editor, error::*, flatten::Flatten, raw::raw}; -use std::{ - collections::VecDeque, - io::{stdout, Bytes, Read, Result, Write}, -}; - -pub struct Repline<'a, R: Read> { - input: Chars, Bytes>>, - - history: VecDeque, // previous lines - hindex: usize, // current index into the history buffer - - ed: Editor<'a>, // the current line buffer -} - -impl<'a, R: Read> Repline<'a, R> { - /// Constructs a [Repline] with the given [Reader](Read), color, begin, and again prompts. - pub fn with_input(input: R, color: &'a str, begin: &'a str, again: &'a str) -> Self { - Self { - input: Chars(Flatten(input.bytes())), - history: Default::default(), - hindex: 0, - ed: Editor::new(color, begin, again), - } - } - /// Set the terminal prompt color - pub fn set_color(&mut self, color: &'a str) { - self.ed.color = color - } - /// Reads in a line, and returns it for validation - pub fn read(&mut self) -> ReplResult { - const INDENT: &str = " "; - let mut stdout = stdout().lock(); - let stdout = &mut stdout; - let _make_raw = raw(); - // self.ed.begin_frame(stdout)?; - // self.ed.redraw_frame(stdout)?; - self.ed.print_head(stdout)?; - loop { - stdout.flush()?; - match self.input.next().ok_or(Error::EndOfInput)?? { - // Ctrl+C: End of Text. Immediately exits. - // Ctrl+D: End of Transmission. Ends the current line. - '\x03' => { - drop(_make_raw); - writeln!(stdout)?; - return Err(Error::CtrlC(self.ed.to_string())); - } - '\x04' => { - drop(_make_raw); - writeln!(stdout)?; - return Err(Error::CtrlD(self.ed.to_string())); - } - // Tab: extend line by 4 spaces - '\t' => { - self.ed.extend(INDENT.chars(), stdout)?; - } - // ignore newlines, process line feeds. Not sure how cross-platform this is. - '\n' => {} - '\r' => { - self.ed.push('\n', stdout)?; - return Ok(self.ed.to_string()); - } - // Ctrl+Backspace in my terminal - '\x17' => { - self.ed.erase_word(stdout)?; - } - // Escape sequence - '\x1b' => self.escape(stdout)?, - // backspace - '\x08' | '\x7f' => { - let ed = &mut self.ed; - if ed.ends_with(INDENT.chars()) { - for _ in 0..INDENT.len() { - ed.pop(stdout)?; - } - } else { - ed.pop(stdout)?; - } - } - c if c.is_ascii_control() => { - if cfg!(debug_assertions) { - self.ed.extend(c.escape_debug(), stdout)?; - } - } - c => { - self.ed.push(c, stdout)?; - } - } - } - } - /// Handle ANSI Escape - fn escape(&mut self, w: &mut W) -> ReplResult<()> { - match self.input.next().ok_or(Error::EndOfInput)?? { - '[' => self.csi(w)?, - 'O' => todo!("Process alternate character mode"), - other => self.ed.extend(other.escape_debug(), w)?, - } - Ok(()) - } - /// Handle ANSI Control Sequence Introducer - fn csi(&mut self, w: &mut W) -> ReplResult<()> { - match self.input.next().ok_or(Error::EndOfInput)?? { - 'A' => { - self.hindex = self.hindex.saturating_sub(1); - self.restore_history(w)? - } - 'B' => { - self.hindex = self.hindex.saturating_add(1).min(self.history.len()); - self.restore_history(w)? - } - 'C' => self.ed.cursor_forward(1, w)?, - 'D' => self.ed.cursor_back(1, w)?, - 'H' => self.ed.home(w)?, - 'F' => self.ed.end(w)?, - '3' => { - if let '~' = self.input.next().ok_or(Error::EndOfInput)?? { - let _ = self.ed.delete(w); - } - } - other => { - if cfg!(debug_assertions) { - self.ed.extend(other.escape_debug(), w)?; - } - } - } - Ok(()) - } - /// Restores the currently selected history - pub fn restore_history(&mut self, w: &mut W) -> ReplResult<()> { - let Self { history, hindex, ed, .. } = self; - ed.undraw(w)?; - ed.clear(); - ed.print_head(w)?; - if let Some(history) = history.get(*hindex) { - ed.extend(history.chars(), w)? - } - Ok(()) - } - - /// Append line to history and clear it - pub fn accept(&mut self) { - self.history_append(self.ed.iter().collect()); - self.ed.clear(); - self.hindex = self.history.len(); - } - /// Append line to history - pub fn history_append(&mut self, mut buf: String) { - while buf.ends_with(char::is_whitespace) { - buf.pop(); - } - if !self.history.contains(&buf) { - self.history.push_back(buf) - } - while self.history.len() > 20 { - self.history.pop_front(); - } - } - /// Clear the line - pub fn deny(&mut self) { - self.ed.clear() - } -} - -impl<'a> Repline<'a, std::io::Stdin> { - pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self { - Self::with_input(std::io::stdin(), color, begin, again) - } -} - -pub mod editor { - use crossterm::{cursor::*, execute, queue, style::*, terminal::*}; - use std::{collections::VecDeque, fmt::Display, io::Write}; - - use super::error::{Error, ReplResult}; - - fn is_newline(c: &char) -> bool { - *c == '\n' - } - - fn write_chars<'a, W: Write>( - c: impl IntoIterator, - w: &mut W, - ) -> std::io::Result<()> { - for c in c { - write!(w, "{c}")?; - } - Ok(()) - } - - #[derive(Debug)] - pub struct Editor<'a> { - head: VecDeque, - tail: VecDeque, - - pub color: &'a str, - begin: &'a str, - again: &'a str, - } - - impl<'a> Editor<'a> { - pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self { - Self { head: Default::default(), tail: Default::default(), color, begin, again } - } - pub fn iter(&self) -> impl Iterator { - self.head.iter() - } - pub fn undraw(&self, w: &mut W) -> ReplResult<()> { - let Self { head, .. } = self; - match head.iter().copied().filter(is_newline).count() { - 0 => write!(w, "\x1b[0G"), - lines => write!(w, "\x1b[{}F", lines), - }?; - queue!(w, Clear(ClearType::FromCursorDown))?; - // write!(w, "\x1b[0J")?; - Ok(()) - } - pub fn redraw(&self, w: &mut W) -> ReplResult<()> { - let Self { head, tail, color, begin, again } = self; - write!(w, "{color}{begin}\x1b[0m ")?; - // draw head - for c in head { - match c { - '\n' => write!(w, "\r\n{color}{again}\x1b[0m "), - _ => w.write_all({ *c as u32 }.to_le_bytes().as_slice()), - }? - } - // save cursor - execute!(w, SavePosition)?; - // draw tail - for c in tail { - match c { - '\n' => write!(w, "\r\n{color}{again}\x1b[0m "), - _ => write!(w, "{c}"), - }? - } - // restore cursor - execute!(w, RestorePosition)?; - Ok(()) - } - pub fn prompt(&self, w: &mut W) -> ReplResult<()> { - let Self { head, color, begin, again, .. } = self; - queue!( - w, - MoveToColumn(0), - Print(color), - Print(if head.is_empty() { begin } else { again }), - ResetColor, - Print(' '), - )?; - Ok(()) - } - pub fn print_head(&self, w: &mut W) -> ReplResult<()> { - self.prompt(w)?; - write_chars( - self.head.iter().skip( - self.head - .iter() - .rposition(is_newline) - .unwrap_or(self.head.len()) - + 1, - ), - w, - )?; - Ok(()) - } - pub fn print_tail(&self, w: &mut W) -> ReplResult<()> { - let Self { tail, .. } = self; - queue!(w, SavePosition, Clear(ClearType::UntilNewLine))?; - write_chars(tail.iter().take_while(|&c| !is_newline(c)), w)?; - queue!(w, RestorePosition)?; - Ok(()) - } - pub fn push(&mut self, c: char, w: &mut W) -> ReplResult<()> { - // Tail optimization: if the tail is empty, - //we don't have to undraw and redraw on newline - if self.tail.is_empty() { - self.head.push_back(c); - match c { - '\n' => { - write!(w, "\r\n")?; - self.print_head(w)?; - } - c => { - queue!(w, Print(c))?; - } - }; - return Ok(()); - } - - if '\n' == c { - self.undraw(w)?; - } - self.head.push_back(c); - match c { - '\n' => self.redraw(w)?, - _ => { - write!(w, "{c}")?; - self.print_tail(w)?; - } - } - Ok(()) - } - pub fn pop(&mut self, w: &mut W) -> ReplResult> { - if let Some('\n') = self.head.back() { - self.undraw(w)?; - } - let c = self.head.pop_back(); - // if the character was a newline, we need to go back a line - match c { - Some('\n') => self.redraw(w)?, - Some(_) => { - // go back a char - queue!(w, MoveLeft(1), Print(' '), MoveLeft(1))?; - self.print_tail(w)?; - } - None => {} - } - Ok(c) - } - - pub fn extend, W: Write>( - &mut self, - iter: T, - w: &mut W, - ) -> ReplResult<()> { - for c in iter { - self.push(c, w)?; - } - Ok(()) - } - - pub fn restore(&mut self, s: &str) { - self.clear(); - self.head.extend(s.chars()) - } - pub fn clear(&mut self) { - self.head.clear(); - self.tail.clear(); - } - pub fn delete(&mut self, w: &mut W) -> ReplResult { - match self.tail.front() { - Some('\n') => { - self.undraw(w)?; - let out = self.tail.pop_front(); - self.redraw(w)?; - out - } - _ => { - let out = self.tail.pop_front(); - self.print_tail(w)?; - out - } - } - .ok_or(Error::EndOfInput) - } - pub fn erase_word(&mut self, w: &mut W) -> ReplResult<()> { - while self.pop(w)?.filter(|c| !c.is_whitespace()).is_some() {} - Ok(()) - } - pub fn len(&self) -> usize { - self.head.len() + self.tail.len() - } - pub fn is_empty(&self) -> bool { - self.head.is_empty() && self.tail.is_empty() - } - pub fn ends_with(&self, iter: impl DoubleEndedIterator) -> bool { - let mut iter = iter.rev(); - let mut head = self.head.iter().rev(); - loop { - match (iter.next(), head.next()) { - (None, _) => break true, - (Some(_), None) => break false, - (Some(a), Some(b)) if a != *b => break false, - (Some(_), Some(_)) => continue, - } - } - } - /// Moves the cursor back `steps` steps - pub fn cursor_back(&mut self, steps: usize, w: &mut W) -> ReplResult<()> { - for _ in 0..steps { - if let Some('\n') = self.head.back() { - self.undraw(w)?; - } - let Some(c) = self.head.pop_back() else { - return Ok(()); - }; - self.tail.push_front(c); - match c { - '\n' => self.redraw(w)?, - _ => queue!(w, MoveLeft(1))?, - } - } - Ok(()) - } - /// Moves the cursor forward `steps` steps - pub fn cursor_forward(&mut self, steps: usize, w: &mut W) -> ReplResult<()> { - for _ in 0..steps { - if let Some('\n') = self.tail.front() { - self.undraw(w)? - } - let Some(c) = self.tail.pop_front() else { - return Ok(()); - }; - self.head.push_back(c); - match c { - '\n' => self.redraw(w)?, - _ => queue!(w, MoveRight(1))?, - } - } - Ok(()) - } - /// Goes to the beginning of the current line - pub fn home(&mut self, w: &mut W) -> ReplResult<()> { - loop { - match self.head.back() { - Some('\n') | None => break Ok(()), - Some(_) => self.cursor_back(1, w)?, - } - } - } - /// Goes to the end of the current line - pub fn end(&mut self, w: &mut W) -> ReplResult<()> { - loop { - match self.tail.front() { - Some('\n') | None => break Ok(()), - Some(_) => self.cursor_forward(1, w)?, - } - } - } - } - - impl<'a, 'e> IntoIterator for &'e Editor<'a> { - type Item = &'e char; - type IntoIter = std::iter::Chain< - std::collections::vec_deque::Iter<'e, char>, - std::collections::vec_deque::Iter<'e, char>, - >; - fn into_iter(self) -> Self::IntoIter { - self.head.iter().chain(self.tail.iter()) - } - } - impl<'a> Display for Editor<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use std::fmt::Write; - let Self { head, tail, .. } = self; - for c in head { - f.write_char(*c)?; - } - for c in tail { - f.write_char(*c)?; - } - Ok(()) - } - } -} diff --git a/cl-repl/src/tools.rs b/cl-repl/src/tools.rs new file mode 100644 index 0000000..98c593a --- /dev/null +++ b/cl-repl/src/tools.rs @@ -0,0 +1,11 @@ +use cl_token::Token; +/// Prints a token in the particular way [cl-repl](crate) does +pub fn print_token(t: &Token) { + println!( + "{:02}:{:02}: {:#19} │{}│", + t.line(), + t.col(), + t.ty(), + t.data(), + ) +} diff --git a/repline/Cargo.toml b/repline/Cargo.toml new file mode 100644 index 0000000..0437167 --- /dev/null +++ b/repline/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "repline" +repository.workspace = true +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +crossterm = "0.27.0" diff --git a/repline/src/editor.rs b/repline/src/editor.rs new file mode 100644 index 0000000..9fdb6fb --- /dev/null +++ b/repline/src/editor.rs @@ -0,0 +1,283 @@ +//! The [Editor] + +use crossterm::{cursor::*, execute, queue, style::*, terminal::*}; +use std::{collections::VecDeque, fmt::Display, io::Write}; + +use super::error::{Error, ReplResult}; + +fn is_newline(c: &char) -> bool { + *c == '\n' +} + +fn write_chars<'a, W: Write>( + c: impl IntoIterator, + w: &mut W, +) -> std::io::Result<()> { + for c in c { + write!(w, "{c}")?; + } + Ok(()) +} + +#[derive(Clone, Debug)] +pub struct Editor<'a> { + head: VecDeque, + tail: VecDeque, + + pub color: &'a str, + begin: &'a str, + again: &'a str, +} + +impl<'a> Editor<'a> { + pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self { + Self { head: Default::default(), tail: Default::default(), color, begin, again } + } + pub fn iter(&self) -> impl Iterator { + let Self { head, tail, .. } = self; + head.iter().chain(tail.iter()) + } + pub fn undraw(&self, w: &mut W) -> ReplResult<()> { + let Self { head, .. } = self; + match head.iter().copied().filter(is_newline).count() { + 0 => write!(w, "\x1b[0G"), + lines => write!(w, "\x1b[{}F", lines), + }?; + queue!(w, Clear(ClearType::FromCursorDown))?; + // write!(w, "\x1b[0J")?; + Ok(()) + } + pub fn redraw(&self, w: &mut W) -> ReplResult<()> { + let Self { head, tail, color, begin, again } = self; + write!(w, "{color}{begin}\x1b[0m ")?; + // draw head + for c in head { + match c { + '\n' => write!(w, "\r\n{color}{again}\x1b[0m "), + _ => w.write_all({ *c as u32 }.to_le_bytes().as_slice()), + }? + } + // save cursor + execute!(w, SavePosition)?; + // draw tail + for c in tail { + match c { + '\n' => write!(w, "\r\n{color}{again}\x1b[0m "), + _ => write!(w, "{c}"), + }? + } + // restore cursor + execute!(w, RestorePosition)?; + Ok(()) + } + pub fn prompt(&self, w: &mut W) -> ReplResult<()> { + let Self { head, color, begin, again, .. } = self; + queue!( + w, + MoveToColumn(0), + Print(color), + Print(if head.is_empty() { begin } else { again }), + ResetColor, + Print(' '), + )?; + Ok(()) + } + pub fn print_head(&self, w: &mut W) -> ReplResult<()> { + self.prompt(w)?; + write_chars( + self.head.iter().skip( + self.head + .iter() + .rposition(is_newline) + .unwrap_or(self.head.len()) + + 1, + ), + w, + )?; + Ok(()) + } + pub fn print_tail(&self, w: &mut W) -> ReplResult<()> { + let Self { tail, .. } = self; + queue!(w, SavePosition, Clear(ClearType::UntilNewLine))?; + write_chars(tail.iter().take_while(|&c| !is_newline(c)), w)?; + queue!(w, RestorePosition)?; + Ok(()) + } + pub fn push(&mut self, c: char, w: &mut W) -> ReplResult<()> { + // Tail optimization: if the tail is empty, + //we don't have to undraw and redraw on newline + if self.tail.is_empty() { + self.head.push_back(c); + match c { + '\n' => { + write!(w, "\r\n")?; + self.print_head(w)?; + } + c => { + queue!(w, Print(c))?; + } + }; + return Ok(()); + } + + if '\n' == c { + self.undraw(w)?; + } + self.head.push_back(c); + match c { + '\n' => self.redraw(w)?, + _ => { + write!(w, "{c}")?; + self.print_tail(w)?; + } + } + Ok(()) + } + pub fn pop(&mut self, w: &mut W) -> ReplResult> { + if let Some('\n') = self.head.back() { + self.undraw(w)?; + } + let c = self.head.pop_back(); + // if the character was a newline, we need to go back a line + match c { + Some('\n') => self.redraw(w)?, + Some(_) => { + // go back a char + queue!(w, MoveLeft(1), Print(' '), MoveLeft(1))?; + self.print_tail(w)?; + } + None => {} + } + Ok(c) + } + + pub fn extend, W: Write>( + &mut self, + iter: T, + w: &mut W, + ) -> ReplResult<()> { + for c in iter { + self.push(c, w)?; + } + Ok(()) + } + + pub fn restore(&mut self, s: &str) { + self.clear(); + self.head.extend(s.chars()) + } + pub fn clear(&mut self) { + self.head.clear(); + self.tail.clear(); + } + pub fn delete(&mut self, w: &mut W) -> ReplResult { + match self.tail.front() { + Some('\n') => { + self.undraw(w)?; + let out = self.tail.pop_front(); + self.redraw(w)?; + out + } + _ => { + let out = self.tail.pop_front(); + self.print_tail(w)?; + out + } + } + .ok_or(Error::EndOfInput) + } + pub fn erase_word(&mut self, w: &mut W) -> ReplResult<()> { + while self.pop(w)?.filter(|c| !c.is_whitespace()).is_some() {} + Ok(()) + } + pub fn len(&self) -> usize { + self.head.len() + self.tail.len() + } + pub fn is_empty(&self) -> bool { + self.head.is_empty() && self.tail.is_empty() + } + pub fn ends_with(&self, iter: impl DoubleEndedIterator) -> bool { + let mut iter = iter.rev(); + let mut head = self.head.iter().rev(); + loop { + match (iter.next(), head.next()) { + (None, _) => break true, + (Some(_), None) => break false, + (Some(a), Some(b)) if a != *b => break false, + (Some(_), Some(_)) => continue, + } + } + } + /// Moves the cursor back `steps` steps + pub fn cursor_back(&mut self, steps: usize, w: &mut W) -> ReplResult<()> { + for _ in 0..steps { + if let Some('\n') = self.head.back() { + self.undraw(w)?; + } + let Some(c) = self.head.pop_back() else { + return Ok(()); + }; + self.tail.push_front(c); + match c { + '\n' => self.redraw(w)?, + _ => queue!(w, MoveLeft(1))?, + } + } + Ok(()) + } + /// Moves the cursor forward `steps` steps + pub fn cursor_forward(&mut self, steps: usize, w: &mut W) -> ReplResult<()> { + for _ in 0..steps { + if let Some('\n') = self.tail.front() { + self.undraw(w)? + } + let Some(c) = self.tail.pop_front() else { + return Ok(()); + }; + self.head.push_back(c); + match c { + '\n' => self.redraw(w)?, + _ => queue!(w, MoveRight(1))?, + } + } + Ok(()) + } + /// Goes to the beginning of the current line + pub fn home(&mut self, w: &mut W) -> ReplResult<()> { + loop { + match self.head.back() { + Some('\n') | None => break Ok(()), + Some(_) => self.cursor_back(1, w)?, + } + } + } + /// Goes to the end of the current line + pub fn end(&mut self, w: &mut W) -> ReplResult<()> { + loop { + match self.tail.front() { + Some('\n') | None => break Ok(()), + Some(_) => self.cursor_forward(1, w)?, + } + } + } +} + +impl<'a, 'e> IntoIterator for &'e Editor<'a> { + type Item = &'e char; + type IntoIter = std::iter::Chain< + std::collections::vec_deque::Iter<'e, char>, + std::collections::vec_deque::Iter<'e, char>, + >; + fn into_iter(self) -> Self::IntoIter { + self.head.iter().chain(self.tail.iter()) + } +} +impl<'a> Display for Editor<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use std::fmt::Write; + for c in self.iter() { + f.write_char(*c)?; + } + Ok(()) + } +} diff --git a/repline/src/error.rs b/repline/src/error.rs new file mode 100644 index 0000000..241254e --- /dev/null +++ b/repline/src/error.rs @@ -0,0 +1,42 @@ +use crate::iter::chars::BadUnicode; + +/// Result type for Repline +pub type ReplResult = std::result::Result; +/// Borrowed error (does not implement [Error](std::error::Error)!) +#[derive(Debug)] +pub enum Error { + /// User broke with Ctrl+C + CtrlC(String), + /// User broke with Ctrl+D + CtrlD(String), + /// Invalid unicode codepoint + BadUnicode(u32), + /// Error came from [std::io] + IoFailure(std::io::Error), + /// End of input + EndOfInput, +} + +impl std::error::Error for Error {} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CtrlC(_) => write!(f, "Ctrl+C"), + Error::CtrlD(_) => write!(f, "Ctrl+D"), + Error::BadUnicode(u) => write!(f, "\\u{{{u:x}}} is not a valid unicode codepoint"), + Error::IoFailure(s) => write!(f, "{s}"), + Error::EndOfInput => write!(f, "End of input"), + } + } +} +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::IoFailure(value) + } +} +impl From for Error { + fn from(value: BadUnicode) -> Self { + let BadUnicode(code) = value; + Self::BadUnicode(code) + } +} diff --git a/repline/src/iter.rs b/repline/src/iter.rs new file mode 100644 index 0000000..4af50bc --- /dev/null +++ b/repline/src/iter.rs @@ -0,0 +1,68 @@ +//! Shmancy iterator adapters + +pub use chars::Chars; +pub use flatten::Flatten; + +pub mod chars { + //! Converts an [Iterator] into an + //! [Iterator]> + + /// Invalid unicode codepoint found when iterating over [Chars] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct BadUnicode(pub u32); + impl std::error::Error for BadUnicode {} + impl std::fmt::Display for BadUnicode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(code) = self; + write!(f, "Bad unicode: {code}") + } + } + + /// Converts an [Iterator] into an + /// [Iterator] + #[derive(Clone, Debug)] + pub struct Chars>(pub I); + impl> Iterator for Chars { + type Item = Result; + fn next(&mut self) -> Option { + let Self(bytes) = self; + let start = bytes.next()? as u32; + let (mut out, count) = match start { + start if start & 0x80 == 0x00 => (start, 0), // ASCII valid range + start if start & 0xe0 == 0xc0 => (start & 0x1f, 1), // 1 continuation byte + start if start & 0xf0 == 0xe0 => (start & 0x0f, 2), // 2 continuation bytes + start if start & 0xf8 == 0xf0 => (start & 0x07, 3), // 3 continuation bytes + _ => return None, + }; + for _ in 0..count { + let cont = bytes.next()? as u32; + if cont & 0xc0 != 0x80 { + return None; + } + out = out << 6 | (cont & 0x3f); + } + Some(char::from_u32(out).ok_or(BadUnicode(out))) + } + } +} +pub mod flatten { + //! Flattens an [Iterator] returning [`Result`](Result) or [`Option`](Option) + //! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T` + + /// Flattens an [Iterator] returning [`Result`](Result) or [`Option`](Option) + /// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T` + #[derive(Clone, Debug)] + pub struct Flatten>(pub I); + impl>> Iterator for Flatten, I> { + type Item = T; + fn next(&mut self) -> Option { + self.0.next()?.ok() + } + } + impl>> Iterator for Flatten, I> { + type Item = T; + fn next(&mut self) -> Option { + self.0.next()? + } + } +} diff --git a/repline/src/lib.rs b/repline/src/lib.rs new file mode 100644 index 0000000..d779cef --- /dev/null +++ b/repline/src/lib.rs @@ -0,0 +1,13 @@ +//! A small pseudo-multiline editing library + +mod editor; +mod iter; +mod raw; + +pub mod error; +pub mod prebaked; +pub mod repline; + +pub use error::Error; +pub use prebaked::{read_and, Response}; +pub use repline::Repline; diff --git a/repline/src/prebaked.rs b/repline/src/prebaked.rs new file mode 100644 index 0000000..ba2aa15 --- /dev/null +++ b/repline/src/prebaked.rs @@ -0,0 +1,56 @@ +//! Here's a menu I prepared earlier! +//! +//! Constructs a [Repline] and repeatedly runs the provided closure on the input strings, +//! obeying the closure's [Response]. + +use std::error::Error; + +use crate::{error::Error as RlError, repline::Repline}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +/// Control codes for the [prebaked menu](read_and) +pub enum Response { + /// Accept the line, and save it to history + Accept, + /// Reject the line, and clear the buffer + Deny, + /// End the loop + Break, + /// Gather more input and try again + Continue, +} + +/// Implements a basic menu loop using an embedded [Repline]. +/// +/// Repeatedly runs the provided closure on the input strings, +/// obeying the closure's [Response]. +/// +/// Captures and displays all user [Error]s. +/// +/// # Keybinds +/// - `Ctrl+C` exits the loop +/// - `Ctrl+D` clears the input, but *runs the closure* with the old input +pub fn read_and(color: &str, begin: &str, again: &str, mut f: F) -> Result<(), RlError> +where F: FnMut(&str) -> Result> { + let mut rl = Repline::new(color, begin, again); + loop { + let line = match rl.read() { + Err(RlError::CtrlC(_)) => break, + Err(RlError::CtrlD(line)) => { + rl.deny(); + line + } + Ok(line) => line, + Err(e) => Err(e)?, + }; + print!("\x1b[G\x1b[J"); + match f(&line) { + Ok(Response::Accept) => rl.accept(), + Ok(Response::Deny) => rl.deny(), + Ok(Response::Break) => break, + Ok(Response::Continue) => continue, + Err(e) => print!("\x1b[40G\x1b[A\x1bJ\x1b[91m{e}\x1b[0m\x1b[B"), + } + } + Ok(()) +} diff --git a/repline/src/raw.rs b/repline/src/raw.rs new file mode 100644 index 0000000..1b66d3f --- /dev/null +++ b/repline/src/raw.rs @@ -0,0 +1,21 @@ +//! Sets the terminal to [`raw`] mode for the duration of the returned object's lifetime. + +/// Sets the terminal to raw mode for the duration of the returned object's lifetime. +pub fn raw() -> impl Drop { + Raw::default() +} +struct Raw(); +impl Default for Raw { + fn default() -> Self { + std::thread::yield_now(); + crossterm::terminal::enable_raw_mode() + .expect("should be able to transition into raw mode"); + Raw() + } +} +impl Drop for Raw { + fn drop(&mut self) { + crossterm::terminal::disable_raw_mode() + .expect("should be able to transition out of raw mode"); + } +} diff --git a/repline/src/repline.rs b/repline/src/repline.rs new file mode 100644 index 0000000..4dc19e7 --- /dev/null +++ b/repline/src/repline.rs @@ -0,0 +1,175 @@ +//! Prompts the user, reads the lines. Not much more to it than that. +//! +//! This module is in charge of parsing keyboard input and interpreting it for the line editor. + +use crate::{editor::Editor, error::*, iter::*, raw::raw}; +use std::{ + collections::VecDeque, + io::{stdout, Bytes, Read, Result, Write}, +}; + +/// Prompts the user, reads the lines. Not much more to it than that. +#[derive(Debug)] +pub struct Repline<'a, R: Read> { + input: Chars, Bytes>>, + + history: VecDeque, // previous lines + hindex: usize, // current index into the history buffer + + ed: Editor<'a>, // the current line buffer +} + +impl<'a> Repline<'a, std::io::Stdin> { + pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self { + Self::with_input(std::io::stdin(), color, begin, again) + } +} + +impl<'a, R: Read> Repline<'a, R> { + /// Constructs a [Repline] with the given [Reader](Read), color, begin, and again prompts. + pub fn with_input(input: R, color: &'a str, begin: &'a str, again: &'a str) -> Self { + Self { + input: Chars(Flatten(input.bytes())), + history: Default::default(), + hindex: 0, + ed: Editor::new(color, begin, again), + } + } + /// Set the terminal prompt color + pub fn set_color(&mut self, color: &'a str) { + self.ed.color = color + } + /// Append line to history and clear it + pub fn accept(&mut self) { + self.history_append(self.ed.to_string()); + self.ed.clear(); + self.hindex = self.history.len(); + } + /// Clear the line + pub fn deny(&mut self) { + self.ed.clear() + } + /// Reads in a line, and returns it for validation + pub fn read(&mut self) -> ReplResult { + const INDENT: &str = " "; + let mut stdout = stdout().lock(); + let stdout = &mut stdout; + let _make_raw = raw(); + // self.ed.begin_frame(stdout)?; + // self.ed.redraw_frame(stdout)?; + self.ed.print_head(stdout)?; + loop { + stdout.flush()?; + match self.input.next().ok_or(Error::EndOfInput)?? { + // Ctrl+C: End of Text. Immediately exits. + '\x03' => { + drop(_make_raw); + writeln!(stdout)?; + return Err(Error::CtrlC(self.ed.to_string())); + } + // Ctrl+D: End of Transmission. Ends the current line. + '\x04' => { + drop(_make_raw); + writeln!(stdout)?; + return Err(Error::CtrlD(self.ed.to_string())); + } + // Tab: extend line by 4 spaces + '\t' => { + self.ed.extend(INDENT.chars(), stdout)?; + } + // ignore newlines, process line feeds. Not sure how cross-platform this is. + '\n' => {} + '\r' => { + self.ed.push('\n', stdout)?; + return Ok(self.ed.to_string()); + } + // Ctrl+Backspace in my terminal + '\x17' => { + self.ed.erase_word(stdout)?; + } + // Escape sequence + '\x1b' => self.escape(stdout)?, + // backspace + '\x08' | '\x7f' => { + let ed = &mut self.ed; + if ed.ends_with(INDENT.chars()) { + for _ in 0..INDENT.len() { + ed.pop(stdout)?; + } + } else { + ed.pop(stdout)?; + } + } + c if c.is_ascii_control() => { + if cfg!(debug_assertions) { + self.ed.extend(c.escape_debug(), stdout)?; + } + } + c => { + self.ed.push(c, stdout)?; + } + } + } + } + /// Handle ANSI Escape + fn escape(&mut self, w: &mut W) -> ReplResult<()> { + match self.input.next().ok_or(Error::EndOfInput)?? { + '[' => self.csi(w)?, + 'O' => todo!("Process alternate character mode"), + other => self.ed.extend(other.escape_debug(), w)?, + } + Ok(()) + } + /// Handle ANSI Control Sequence Introducer + fn csi(&mut self, w: &mut W) -> ReplResult<()> { + match self.input.next().ok_or(Error::EndOfInput)?? { + 'A' => { + self.hindex = self.hindex.saturating_sub(1); + self.restore_history(w)? + } + 'B' => { + self.hindex = self.hindex.saturating_add(1).min(self.history.len()); + self.restore_history(w)? + } + 'C' => self.ed.cursor_forward(1, w)?, + 'D' => self.ed.cursor_back(1, w)?, + 'H' => self.ed.home(w)?, + 'F' => self.ed.end(w)?, + '3' => { + if let '~' = self.input.next().ok_or(Error::EndOfInput)?? { + let _ = self.ed.delete(w); + } + } + other => { + if cfg!(debug_assertions) { + self.ed.extend(other.escape_debug(), w)?; + } + } + } + Ok(()) + } + /// Restores the currently selected history + fn restore_history(&mut self, w: &mut W) -> ReplResult<()> { + let Self { history, hindex, ed, .. } = self; + ed.undraw(w)?; + ed.clear(); + ed.print_head(w)?; + if let Some(history) = history.get(*hindex) { + ed.extend(history.chars(), w)? + } + Ok(()) + } + + /// Append line to history + fn history_append(&mut self, mut buf: String) { + while buf.ends_with(char::is_whitespace) { + buf.pop(); + } + if !self.history.contains(&buf) { + self.history.push_back(buf) + } + while self.history.len() > 20 { + self.history.pop_front(); + } + } +}