Skip to content

Commit

Permalink
Initial implementation of frontend-configurable command runner
Browse files Browse the repository at this point in the history
  • Loading branch information
rben01 committed Nov 30, 2024
1 parent 2d6e1e2 commit 3610031
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 65 deletions.
137 changes: 75 additions & 62 deletions numbat-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use config::{ColorMode, Config, ExchangeRateFetchingPolicy, IntroBanner, PrettyP
use highlighter::NumbatHighlighter;

use itertools::Itertools;
use numbat::command::{self, CommandParser, SourcelessCommandParser};
use numbat::command::{
self, CommandContext, CommandControlFlow, CommandParser, CommandRunner, SourcelessCommandParser,
};
use numbat::compact_str::{CompactString, ToCompactString};
use numbat::diagnostic::ErrorDiagnostic;
use numbat::help::help_markup;
Expand Down Expand Up @@ -353,6 +355,60 @@ impl Cli {
) -> Result<()> {
let mut session_history = SessionHistory::default();

let cmd_runner = CommandRunner::<Editor<NumbatHelper, DefaultHistory>>::new_all_disabled()
.enable_help(|| {
let help = help_markup();
print!("{}", ansi_format(&help, true));
// currently, the ansi formatter adds indents
// _after_ each newline and so we need to manually
// add an extra blank line to absorb this indent
println!();
CommandControlFlow::Normal
})
.enable_info(|ctx, item| {
let help = ctx.print_info_for_keyword(item);
println!("{}", ansi_format(&help, true));
CommandControlFlow::Normal
})
.enable_list(|ctx, item| {
let m = match item {
None => ctx.print_environment(),
Some(command::ListItems::Functions) => ctx.print_functions(),
Some(command::ListItems::Dimensions) => ctx.print_dimensions(),
Some(command::ListItems::Variables) => ctx.print_variables(),
Some(command::ListItems::Units) => ctx.print_units(),
};
println!("{}", ansi_format(&m, false));
CommandControlFlow::Normal
})
.enable_clear(|rl| match rl.clear_screen() {
Ok(_) => CommandControlFlow::Normal,
Err(_) => CommandControlFlow::Return,
})
.enable_save(|ctx, sh, dst, interactive| {
let save_result = sh.save(
dst,
SessionHistoryOptions {
include_err_lines: false,
trim_lines: true,
},
);
match save_result {
Ok(_) => {
let m = m::text("successfully saved session history to")
+ m::space()
+ m::string(dst.to_compact_string());
println!("{}", ansi_format(&m, interactive));
CommandControlFlow::Normal
}
Err(err) => {
ctx.print_diagnostic(*err);
CommandControlFlow::Continue
}
}
})
.enable_quit(|| CommandControlFlow::Return);

loop {
let readline = rl.readline(&self.config.prompt);
match readline {
Expand All @@ -361,7 +417,9 @@ impl Cli {
rl.add_history_entry(&line)?;

// if we enter here, the line looks like a command
if let Some(sourceless_parser) = SourcelessCommandParser::new(&line) {
if let Some(sourceless_parser) =
SourcelessCommandParser::new(&line, &cmd_runner)
{
let mut parser = CommandParser::new(
sourceless_parser,
self.context
Expand All @@ -372,67 +430,22 @@ impl Cli {
);

match parser.parse_command() {
Ok(command) => match command {
command::Command::Help => {
let help = help_markup();
print!("{}", ansi_format(&help, true));
// currently, the ansi formatter adds indents
// _after_ each newline and so we need to manually
// add an extra blank line to absorb this indent
println!();
}
command::Command::Info { item } => {
let help = self
.context
.lock()
.unwrap()
.print_info_for_keyword(item);
println!("{}", ansi_format(&help, true));
}
command::Command::List { items } => {
let context = self.context.lock().unwrap();
let m = match items {
None => context.print_environment(),
Some(command::ListItems::Functions) => {
context.print_functions()
}
Some(command::ListItems::Dimensions) => {
context.print_dimensions()
}
Some(command::ListItems::Variables) => {
context.print_variables()
}
Some(command::ListItems::Units) => {
context.print_units()
}
};
println!("{}", ansi_format(&m, false));
Ok(cmd) => {
let mut context = self.context.lock().unwrap();
match cmd_runner.run(
cmd,
CommandContext {
ctx: &mut context,
editor: rl,
session_history: &session_history,
interactive,
},
) {
CommandControlFlow::Normal => {}
CommandControlFlow::Continue => continue,
CommandControlFlow::Return => return Ok(()),
}
command::Command::Clear => rl.clear_screen()?,
command::Command::Save { dst } => {
let save_result = session_history.save(
dst,
SessionHistoryOptions {
include_err_lines: false,
trim_lines: true,
},
);
match save_result {
Ok(_) => {
let m = m::text(
"successfully saved session history to",
) + m::space()
+ m::string(dst.to_compact_string());
println!("{}", ansi_format(&m, interactive));
}
Err(err) => {
self.print_diagnostic(*err);
continue;
}
}
}
command::Command::Quit => return Ok(()),
},
}
Err(e) => {
self.print_diagnostic(e);
continue;
Expand Down
162 changes: 159 additions & 3 deletions numbat/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use std::str::{FromStr, SplitWhitespace};

use crate::{
parser::ParseErrorKind,
session_history::SessionHistory,
span::{ByteIndex, Span},
ParseError,
Context, ParseError,
};

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -46,6 +47,104 @@ impl FromStr for CommandKind {
}
}

#[derive(Default)]
pub enum CommandControlFlow {
#[default]
Normal,
Continue,
Return,
}

pub struct CommandContext<'ctx, 'aux, Editor> {
pub ctx: &'ctx mut Context,
pub editor: &'aux mut Editor,
pub session_history: &'aux SessionHistory,
pub interactive: bool,
}

pub struct CommandRunner<Editor> {
help: Option<fn() -> CommandControlFlow>,
info: Option<fn(&mut Context, &str) -> CommandControlFlow>,
list: Option<fn(&Context, Option<ListItems>) -> CommandControlFlow>,
clear: Option<fn(&mut Editor) -> CommandControlFlow>,
#[allow(clippy::type_complexity)]
save: Option<fn(&Context, &SessionHistory, &str, bool) -> CommandControlFlow>,
quit: Option<fn() -> CommandControlFlow>,
}

impl<Editor> Default for CommandRunner<Editor> {
fn default() -> Self {
Self {
help: None,
info: None,
list: None,
clear: None,
save: None,
quit: None,
}
}
}

impl<Editor> CommandRunner<Editor> {
pub fn new_all_disabled() -> Self {
Self::default()
}

pub fn enable_help(mut self, action: fn() -> CommandControlFlow) -> Self {
self.help = Some(action);
self
}

pub fn enable_info(mut self, action: fn(&mut Context, &str) -> CommandControlFlow) -> Self {
self.info = Some(action);
self
}

pub fn enable_list(
mut self,
action: fn(&Context, Option<ListItems>) -> CommandControlFlow,
) -> Self {
self.list = Some(action);
self
}

pub fn enable_clear(mut self, action: fn(&mut Editor) -> CommandControlFlow) -> Self {
self.clear = Some(action);
self
}

pub fn enable_save(
mut self,
action: fn(&Context, &SessionHistory, &str, bool) -> CommandControlFlow,
) -> Self {
self.save = Some(action);
self
}

pub fn enable_quit(mut self, action: fn() -> CommandControlFlow) -> Self {
self.quit = Some(action);
self
}

pub fn run(&self, cmd: Command, args: CommandContext<Editor>) -> CommandControlFlow {
let CommandContext {
ctx,
editor,
session_history,
interactive,
} = args;

match cmd {
Command::Help => self.help.unwrap()(),
Command::Info { item } => self.info.unwrap()(ctx, item),
Command::List { items } => self.list.unwrap()(ctx, items),
Command::Clear => self.clear.unwrap()(editor),
Command::Save { dst } => self.save.unwrap()(ctx, session_history, dst, interactive),
Command::Quit => self.quit.unwrap()(),
}
}
}

#[derive(Debug, Clone, PartialEq)]
pub enum Command<'a> {
Help,
Expand Down Expand Up @@ -86,10 +185,22 @@ impl<'a> SourcelessCommandParser<'a> {
///
/// If this returns `None`, you should proceed with parsing the input as an ordinary
/// numbat expression
pub fn new(input: &'a str) -> Option<Self> {
pub fn new<Editor>(input: &'a str, config: &CommandRunner<Editor>) -> Option<Self> {
let mut words: SplitWhitespace<'_> = input.split_whitespace();
let command_kind = words.next().and_then(|w| w.parse().ok())?;

let is_supported = match command_kind {
CommandKind::Help => config.help.is_some(),
CommandKind::Info => config.info.is_some(),
CommandKind::List => config.list.is_some(),
CommandKind::Clear => config.clear.is_some(),
CommandKind::Save => config.save.is_some(),
CommandKind::Quit(_) => config.quit.is_some(),
};
if !is_supported {
return None;
}

let mut word_boundaries = Vec::new();
let mut prev_char_was_whitespace = true;
let mut start_idx = 0;
Expand Down Expand Up @@ -272,8 +383,28 @@ impl<'a> CommandParser<'a> {
mod test {
use super::*;

fn default_cf0() -> CommandControlFlow {
CommandControlFlow::default()
}

fn parser(input: &'static str) -> Option<CommandParser<'static>> {
Some(CommandParser::new(SourcelessCommandParser::new(input)?, 0))
impl<Editor> CommandRunner<Editor> {
fn new_all_enabled() -> Self {
Self {
help: Some(default_cf0),
info: Some(|_, _| CommandControlFlow::default()),
list: Some(|_, _| CommandControlFlow::default()),
clear: Some(|_| CommandControlFlow::default()),
save: Some(|_, _, _, _| CommandControlFlow::default()),
quit: Some(default_cf0),
}
}
}
let config = CommandRunner::<()>::new_all_enabled();
Some(CommandParser::new(
SourcelessCommandParser::new(input, &config)?,
0,
))
}

// can't be a function due to lifetimes/borrow checker
Expand Down Expand Up @@ -485,4 +616,29 @@ mod test {
assert_eq!(parse!("save .").unwrap(), Command::Save { dst: "." });
assert!(parse!("save arg1 arg2").is_err());
}

#[test]
fn test_config() {
fn parser<'a, Editor>(
input: &'a str,
config: &CommandRunner<Editor>,
) -> Option<CommandParser<'a>> {
Some(CommandParser::new(
SourcelessCommandParser::new(input, config)?,
0,
))
}

let config = CommandRunner::<()>::new_all_disabled()
.enable_help(default_cf0)
.enable_quit(default_cf0);

assert!(parser("help", &config).is_some());
assert!(parser("info", &config).is_none());
assert!(parser("list", &config).is_none());
assert!(parser("clear", &config).is_none());
assert!(parser("save", &config).is_none());
assert!(parser("quit", &config).is_some());
assert!(parser("exit", &config).is_some());
}
}

0 comments on commit 3610031

Please sign in to comment.