repline: Promote to its own crate!
cl-repl: Major refactor based on the design of typeck.rs
This commit is contained in:
		@@ -8,6 +8,7 @@ members = [
 | 
			
		||||
    "cl-ast",
 | 
			
		||||
    "cl-parser",
 | 
			
		||||
    "cl-lexer",
 | 
			
		||||
    "repline",
 | 
			
		||||
]
 | 
			
		||||
resolver = "2"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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" }
 | 
			
		||||
 
 | 
			
		||||
@@ -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<dyn Error>> {
 | 
			
		||||
 | 
			
		||||
    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<Response, Box<dyn Error>>,
 | 
			
		||||
) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
    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<dyn Error>> {
 | 
			
		||||
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<dyn Error>> {
 | 
			
		||||
            }
 | 
			
		||||
            _ => 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<dyn Error>> {
 | 
			
		||||
    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<dyn Error>> {
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn live_desugar() -> Result<(), Box<dyn Error>> {
 | 
			
		||||
    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<dyn Error>> {
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn query_type_expression(prj: &mut Project) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
    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<dyn Error>> {
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn list_types(prj: &mut Project) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
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<dyn Error>> {
 | 
			
		||||
        }
 | 
			
		||||
        println!("{name}\x1b[30G| {kind}");
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn pretty_def(def: &Def, id: impl Into<usize>) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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<dyn Error>> {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								cl-repl/src/ansi.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								cl-repl/src/ansi.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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";
 | 
			
		||||
							
								
								
									
										51
									
								
								cl-repl/src/args.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								cl-repl/src/args.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<PathBuf>,
 | 
			
		||||
 | 
			
		||||
    /// files to include
 | 
			
		||||
    #[argh(option, short = 'I')]
 | 
			
		||||
    pub include: Vec<PathBuf>,
 | 
			
		||||
 | 
			
		||||
    /// 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<Self, &'static str> {
 | 
			
		||||
        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\"")?,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<dyn Error>> {
 | 
			
		||||
    if is_terminal() {
 | 
			
		||||
        println!(
 | 
			
		||||
            "--- {} v{} 💪🦈 ---",
 | 
			
		||||
            env!("CARGO_BIN_NAME"),
 | 
			
		||||
            env!("CARGO_PKG_VERSION"),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
    run(argh::from_env())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								cl-repl/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								cl-repl/src/cli.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<dyn Error>> {
 | 
			
		||||
    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<Path>,
 | 
			
		||||
) -> Result<ConValue, Box<dyn Error>> {
 | 
			
		||||
    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<impl AsRef<Path>>) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
    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<dyn Error>> {
 | 
			
		||||
    let code = Parser::new(Lexer::new(code)).file()?;
 | 
			
		||||
    println!("{code}");
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn run_code(code: &str, env: &mut Environment) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
    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(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								cl-repl/src/ctx.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								cl-repl/src/ctx.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<ConValue> {
 | 
			
		||||
        code.interpret(&mut self.env)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for Context {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self::new()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,446 +1,11 @@
 | 
			
		||||
//! Utilities for cl-frontend
 | 
			
		||||
//! The Conlang REPL, based on [repline]
 | 
			
		||||
//! 
 | 
			
		||||
//! # TODO
 | 
			
		||||
//! - [ ] Readline-like line editing
 | 
			
		||||
//! - [ ] Raw mode?
 | 
			
		||||
//! 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<PathBuf>,
 | 
			
		||||
 | 
			
		||||
        /// files to include
 | 
			
		||||
        #[argh(option, short = 'I')]
 | 
			
		||||
        pub include: Vec<PathBuf>,
 | 
			
		||||
 | 
			
		||||
        /// 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<Self, &'static str> {
 | 
			
		||||
            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<Program<'t, Parsed>> {
 | 
			
		||||
            self.parse_file().or_else(|_| self.parse_stmt())
 | 
			
		||||
        }
 | 
			
		||||
        pub fn parse_stmt(&self) -> PResult<Program<'t, Parsed>> {
 | 
			
		||||
            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<Program<'t, Parsed>> {
 | 
			
		||||
            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<ConValue> {
 | 
			
		||||
            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<dyn Error>> {
 | 
			
		||||
        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<Path>,
 | 
			
		||||
    ) -> Result<ConValue, Box<dyn Error>> {
 | 
			
		||||
        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<Parsable>,
 | 
			
		||||
        path: Option<impl AsRef<Path>>,
 | 
			
		||||
    ) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
        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<Parsable>) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
        code.parse()?.print();
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn interpret(code: Program<Parsable>, env: &mut Environment) -> Result<(), Box<dyn Error>> {
 | 
			
		||||
        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<T: Display, E: Display>(&self, res: Result<T, E>) {
 | 
			
		||||
            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<Parsed>) {
 | 
			
		||||
            match self.mode {
 | 
			
		||||
                Mode::Tokenize => {}
 | 
			
		||||
                Mode::Beautify => self.beautify(code),
 | 
			
		||||
                Mode::Interpret => self.interpret(code),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        fn tokenize(&mut self, code: &Program<Parsable>) {
 | 
			
		||||
            for token in code.lex() {
 | 
			
		||||
                match token {
 | 
			
		||||
                    Ok(token) => print_token(&token),
 | 
			
		||||
                    Err(e) => println!("{e}"),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        fn interpret(&mut self, code: &Program<Parsed>) {
 | 
			
		||||
            match code.run(&mut self.env) {
 | 
			
		||||
                Ok(ConValue::Empty) => {}
 | 
			
		||||
                res => self.prompt_result(res),
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        fn beautify(&mut self, code: &Program<Parsed>) {
 | 
			
		||||
            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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										76
									
								
								cl-repl/src/menu.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								cl-repl/src/menu.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
@@ -1,607 +0,0 @@
 | 
			
		||||
//! A small pseudo-multiline editing library
 | 
			
		||||
// #![allow(unused)]
 | 
			
		||||
 | 
			
		||||
pub mod error {
 | 
			
		||||
    /// Result type for Repline
 | 
			
		||||
    pub type ReplResult<T> = std::result::Result<T, Error>;
 | 
			
		||||
    /// 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<std::io::Error> for Error {
 | 
			
		||||
        fn from(value: std::io::Error) -> Self {
 | 
			
		||||
            Self::IoFailure(value)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub mod chars {
 | 
			
		||||
    //! Converts an <code>[Iterator]<Item = [u8]></code> into an
 | 
			
		||||
    //! <code>[Iterator]<Item = [char]></code>
 | 
			
		||||
 | 
			
		||||
    use super::error::*;
 | 
			
		||||
 | 
			
		||||
    /// Converts an <code>[Iterator]<Item = [u8]></code> into an
 | 
			
		||||
    /// <code>[Iterator]<Item = [char]></code>
 | 
			
		||||
    #[derive(Clone, Debug)]
 | 
			
		||||
    pub struct Chars<I: Iterator<Item = u8>>(pub I);
 | 
			
		||||
    impl<I: Iterator<Item = u8>> Chars<I> {
 | 
			
		||||
        pub fn new(bytes: I) -> Self {
 | 
			
		||||
            Self(bytes)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    impl<I: Iterator<Item = u8>> Iterator for Chars<I> {
 | 
			
		||||
        type Item = ReplResult<char>;
 | 
			
		||||
        fn next(&mut self) -> Option<Self::Item> {
 | 
			
		||||
            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<T, E>`](Result) or [`Option<T>`](Option)
 | 
			
		||||
    //! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
 | 
			
		||||
 | 
			
		||||
    /// Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
 | 
			
		||||
    /// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
 | 
			
		||||
    pub struct Flatten<T, I: Iterator<Item = T>>(pub I);
 | 
			
		||||
    impl<T, E, I: Iterator<Item = Result<T, E>>> Iterator for Flatten<Result<T, E>, I> {
 | 
			
		||||
        type Item = T;
 | 
			
		||||
        fn next(&mut self) -> Option<Self::Item> {
 | 
			
		||||
            self.0.next()?.ok()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    impl<T, I: Iterator<Item = Option<T>>> Iterator for Flatten<Option<T>, I> {
 | 
			
		||||
        type Item = T;
 | 
			
		||||
        fn next(&mut self) -> Option<Self::Item> {
 | 
			
		||||
            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<W: Write> {
 | 
			
		||||
        out: W,
 | 
			
		||||
    }
 | 
			
		||||
    impl<W: Write> EagerWriter<W> {
 | 
			
		||||
        pub fn new(writer: W) -> Self {
 | 
			
		||||
            Self { out: writer }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    impl<W: Write> Write for EagerWriter<W> {
 | 
			
		||||
        fn write(&mut self, buf: &[u8]) -> Result<usize> {
 | 
			
		||||
            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<Flatten<Result<u8>, Bytes<R>>>,
 | 
			
		||||
 | 
			
		||||
    history: VecDeque<String>, // 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<String> {
 | 
			
		||||
        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<W: Write>(&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<W: Write>(&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<W: Write>(&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<Item = &'a char>,
 | 
			
		||||
        w: &mut W,
 | 
			
		||||
    ) -> std::io::Result<()> {
 | 
			
		||||
        for c in c {
 | 
			
		||||
            write!(w, "{c}")?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[derive(Debug)]
 | 
			
		||||
    pub struct Editor<'a> {
 | 
			
		||||
        head: VecDeque<char>,
 | 
			
		||||
        tail: VecDeque<char>,
 | 
			
		||||
 | 
			
		||||
        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<Item = &char> {
 | 
			
		||||
            self.head.iter()
 | 
			
		||||
        }
 | 
			
		||||
        pub fn undraw<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&mut self, w: &mut W) -> ReplResult<Option<char>> {
 | 
			
		||||
            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<T: IntoIterator<Item = char>, 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<W: Write>(&mut self, w: &mut W) -> ReplResult<char> {
 | 
			
		||||
            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<W: Write>(&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<Item = char>) -> 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<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&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(())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								cl-repl/src/tools.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								cl-repl/src/tools.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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(),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								repline/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								repline/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -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"
 | 
			
		||||
							
								
								
									
										283
									
								
								repline/src/editor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								repline/src/editor.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<Item = &'a char>,
 | 
			
		||||
    w: &mut W,
 | 
			
		||||
) -> std::io::Result<()> {
 | 
			
		||||
    for c in c {
 | 
			
		||||
        write!(w, "{c}")?;
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
pub struct Editor<'a> {
 | 
			
		||||
    head: VecDeque<char>,
 | 
			
		||||
    tail: VecDeque<char>,
 | 
			
		||||
 | 
			
		||||
    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<Item = &char> {
 | 
			
		||||
        let Self { head, tail, .. } = self;
 | 
			
		||||
        head.iter().chain(tail.iter())
 | 
			
		||||
    }
 | 
			
		||||
    pub fn undraw<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&mut self, w: &mut W) -> ReplResult<Option<char>> {
 | 
			
		||||
        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<T: IntoIterator<Item = char>, 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<W: Write>(&mut self, w: &mut W) -> ReplResult<char> {
 | 
			
		||||
        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<W: Write>(&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<Item = char>) -> 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<W: Write>(&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<W: Write>(&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<W: Write>(&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<W: Write>(&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(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								repline/src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								repline/src/error.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
use crate::iter::chars::BadUnicode;
 | 
			
		||||
 | 
			
		||||
/// Result type for Repline
 | 
			
		||||
pub type ReplResult<T> = std::result::Result<T, Error>;
 | 
			
		||||
/// 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<std::io::Error> for Error {
 | 
			
		||||
    fn from(value: std::io::Error) -> Self {
 | 
			
		||||
        Self::IoFailure(value)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl From<BadUnicode> for Error {
 | 
			
		||||
    fn from(value: BadUnicode) -> Self {
 | 
			
		||||
        let BadUnicode(code) = value;
 | 
			
		||||
        Self::BadUnicode(code)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								repline/src/iter.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								repline/src/iter.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
//! Shmancy iterator adapters
 | 
			
		||||
 | 
			
		||||
pub use chars::Chars;
 | 
			
		||||
pub use flatten::Flatten;
 | 
			
		||||
 | 
			
		||||
pub mod chars {
 | 
			
		||||
    //! Converts an <code>[Iterator]<Item = [u8]></code> into an
 | 
			
		||||
    //! <code>[Iterator]<Item = [Result]<[char], [BadUnicode]>></code>
 | 
			
		||||
 | 
			
		||||
    /// 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 <code>[Iterator]<Item = [u8]></code> into an
 | 
			
		||||
    /// <code>[Iterator]<Item = [char]></code>
 | 
			
		||||
    #[derive(Clone, Debug)]
 | 
			
		||||
    pub struct Chars<I: Iterator<Item = u8>>(pub I);
 | 
			
		||||
    impl<I: Iterator<Item = u8>> Iterator for Chars<I> {
 | 
			
		||||
        type Item = Result<char, BadUnicode>;
 | 
			
		||||
        fn next(&mut self) -> Option<Self::Item> {
 | 
			
		||||
            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<T, E>`](Result) or [`Option<T>`](Option)
 | 
			
		||||
    //! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
 | 
			
		||||
 | 
			
		||||
    /// Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
 | 
			
		||||
    /// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
 | 
			
		||||
    #[derive(Clone, Debug)]
 | 
			
		||||
    pub struct Flatten<T, I: Iterator<Item = T>>(pub I);
 | 
			
		||||
    impl<T, E, I: Iterator<Item = Result<T, E>>> Iterator for Flatten<Result<T, E>, I> {
 | 
			
		||||
        type Item = T;
 | 
			
		||||
        fn next(&mut self) -> Option<Self::Item> {
 | 
			
		||||
            self.0.next()?.ok()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    impl<T, I: Iterator<Item = Option<T>>> Iterator for Flatten<Option<T>, I> {
 | 
			
		||||
        type Item = T;
 | 
			
		||||
        fn next(&mut self) -> Option<Self::Item> {
 | 
			
		||||
            self.0.next()?
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								repline/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								repline/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
							
								
								
									
										56
									
								
								repline/src/prebaked.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								repline/src/prebaked.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<F>(color: &str, begin: &str, again: &str, mut f: F) -> Result<(), RlError>
 | 
			
		||||
where F: FnMut(&str) -> Result<Response, Box<dyn Error>> {
 | 
			
		||||
    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(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								repline/src/raw.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								repline/src/raw.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										175
									
								
								repline/src/repline.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								repline/src/repline.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<Flatten<Result<u8>, Bytes<R>>>,
 | 
			
		||||
 | 
			
		||||
    history: VecDeque<String>, // 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<String> {
 | 
			
		||||
        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<W: Write>(&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<W: Write>(&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<W: Write>(&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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user