use super::*;
use crate::repl::{Editing, EditingIndex, ReplData};
use cmdtree::{BuildError, Builder, BuilderChain, Commander};
use std::{
fs,
io::Write,
path::{Path, PathBuf},
};
pub use cmdtree::Builder as CommandBuilder;
pub type ReplDataAction<D> = Box<dyn Fn(&mut ReplData<D>, &mut dyn Write) -> String>;
pub type AppDataAction<D> = Box<dyn Fn(&mut D, &mut ReplData<D>, &mut dyn Write) -> String>;
pub enum CommandResult<D> {
BeginMutBlock,
EditAlter(EditingIndex),
EditReplace(EditingIndex, String),
SwitchModule(PathBuf),
ActionOnReplData(ReplDataAction<D>),
ActionOnAppData(AppDataAction<D>),
Empty,
}
impl<D> CommandResult<D> {
pub fn app_data_fn<F>(func: F) -> Self
where
F: 'static + Fn(&mut D, &mut ReplData<D>, &mut dyn Write) -> String,
{
CommandResult::ActionOnAppData(Box::new(func))
}
pub fn repl_data_fn<F>(func: F) -> Self
where
F: 'static + Fn(&mut ReplData<D>, &mut dyn Write) -> String,
{
CommandResult::ActionOnReplData(Box::new(func))
}
}
impl<D> ReplData<D> {
pub fn with_cmdtree_builder(
&mut self,
builder: Builder<CommandResult<D>>,
) -> Result<&mut Self, BuildError> {
self.cmdtree = papyrus_cmdr(builder)?;
Ok(self)
}
}
fn papyrus_cmdr<D>(
builder: Builder<CommandResult<D>>,
) -> Result<Commander<CommandResult<D>>, BuildError> {
builder
.root()
.add_action("mut", "Begin a mutable block of code", |_, _| {
CommandResult::BeginMutBlock
})
.begin_class("edit", "Edit previous input")
.begin_class("stmt", "Edit previous statements")
.add_action(
"alter",
"Alter statement contents. args: stmt-number",
|wtr, args| edit_alter_priv(args, wtr, Editing::Stmt),
)
.add_action(
"replace",
"Replace statement contents. args: stmt-number value",
|wtr, args| edit_replace_priv(args, wtr, Editing::Stmt),
)
.end_class()
.end_class()
.begin_class("mod", "Handle modules")
.add_action(
"switch",
"Switch to a module, creating one if necessary. switch path/to/module",
|wtr, args| switch_module_priv(args, wtr),
)
.add_action(
"clear",
"Clear previous input. args: mod-path or glob pattern",
|wtr, args| clear_modules(args, wtr),
)
.end_class()
.begin_class("static-files", "Handle static files")
.add_action(
"add",
"Import a static file. args: file-path or glob pattern",
|wtr, args| add_static_file(wtr, args),
)
.add_action(
"rm",
"Remove a static file. args: file-path or glob pattern",
|wtr, args| rm_static_file(wtr, args),
)
.add_action("ls", "List imported static files", |_, _| ls_static_files())
.end_class()
.into_commander()
}
fn switch_module_priv<D, W: Write>(args: &[&str], mut wtr: W) -> CommandResult<D> {
if let Some(path) = args.get(0) {
if let Some(path) = make_path(path) {
CommandResult::SwitchModule(path)
} else {
writeln!(wtr, "failed to parse {} into a valid module path", path).unwrap();
CommandResult::Empty
}
} else {
writeln!(wtr, "switch expects a path to module argument").unwrap();
CommandResult::Empty
}
}
fn make_all_parents(path: &Path) -> Vec<PathBuf> {
let components: Vec<_> = path.iter().collect();
(1..components.len())
.map(|idx| components[0..idx].iter().collect::<PathBuf>())
.collect()
}
fn make_path(path: &str) -> Option<PathBuf> {
let path = path.trim();
let path = path.replace(".rs", "").replace("mod", "").replace("-", "_");
if path == "lib" {
return Some(PathBuf::from("lib"));
}
let x: &[_] = &['/', '\\'];
let path = path.trim_matches(x);
if path.is_empty() {
return None;
}
Some(PathBuf::from(path))
}
fn edit_alter_priv<D, W: Write>(args: &[&str], mut wtr: W, t: Editing) -> CommandResult<D> {
if let Some(idx) = args.get(0) {
match parse_idx(idx, t) {
Ok(ei) => CommandResult::EditAlter(ei),
Err(e) => {
writeln!(wtr, "failed parsing {} as number: {}", idx, e).ok();
CommandResult::Empty
}
}
} else {
writeln!(wtr, "alter expects an index number").ok();
CommandResult::Empty
}
}
fn edit_replace_priv<D, W: Write>(args: &[&str], mut wtr: W, t: Editing) -> CommandResult<D> {
if let Some(idx) = args.get(0) {
match parse_idx(idx, t) {
Ok(ei) => CommandResult::EditReplace(ei, args[1..].iter().copied().collect::<String>()),
Err(e) => {
writeln!(wtr, "failed parsing {} as number: {}", idx, e).ok();
CommandResult::Empty
}
}
} else {
writeln!(wtr, "replace expects an index number").ok();
CommandResult::Empty
}
}
fn parse_idx(s: &str, editing: Editing) -> Result<EditingIndex, String> {
s.parse()
.map_err(|e| format!("{}", e))
.map(|index| EditingIndex { editing, index })
}
pub(crate) fn edit_alter<D>(data: &mut ReplData<D>, ei: EditingIndex) -> &'static str {
let src = data.current_src();
let len = match ei.editing {
Editing::Stmt => src.stmts.len(),
Editing::Item => src.items.len(),
Editing::Crate => src.crates.len(),
};
if ei.index >= len {
"index is outside of range"
} else {
data.editing = Some(ei);
""
}
}
pub(crate) fn switch_module<D>(data: &mut ReplData<D>, path: &Path) -> &'static str {
let mut all = make_all_parents(path);
all.push(path.to_path_buf());
for x in all {
data.mods_map.entry(x).or_default();
}
data.current_mod = path.to_path_buf();
""
}
fn clear_modules<D, W: Write>(args: &[&str], mut wtr: W) -> CommandResult<D> {
if let Some(pat) = args.get(0) {
match glob::Pattern::new(pat) {
Ok(pattern) => CommandResult::repl_data_fn(move |data, wtr| {
for (path, src_code) in &mut data.mods_map {
if pattern.matches_path(&path) {
src_code.clear();
writeln!(wtr, "cleared inputs in `{}`", path.display()).ok();
}
}
String::from("cleared all previous inputs")
}),
Err(e) => {
writeln!(wtr, "unrecognisable pattern: {}", e).ok();
CommandResult::Empty
}
}
} else {
CommandResult::repl_data_fn(move |data, _| {
let p = data.current_mod().to_owned();
if let Some(src) = data.mods_map.get_mut(&p) {
src.clear()
}
format!("cleared previous input in `{}`", p.display())
})
}
}
fn add_static_file<D>(wtr: &mut dyn Write, args: &[&str]) -> CommandResult<D> {
if let Some(&path) = args.get(0) {
let glob = path.to_string();
CommandResult::repl_data_fn(move |data, wtr| {
foreach_glob_path(&glob, wtr, |path, wtr| {
match fs::read_to_string(&path) {
Ok(s) => match data.add_static_file(path.clone(), &s) {
Ok(_) => {
writeln!(wtr, "imported/overwrote static file: `{}`", path.display())
}
Err(e) => writeln!(wtr, "failed to add `{}`: {}", path.display(), e),
},
Err(e) => writeln!(wtr, "failed to read `{}`: {}", path.display(), e),
}
.ok();
});
String::new()
})
} else {
writeln!(wtr, "add expects a file path or glob pattern").ok();
CommandResult::Empty
}
}
fn rm_static_file<D>(wtr: &mut dyn Write, args: &[&str]) -> CommandResult<D> {
if let Some(&path) = args.get(0) {
let glob = path.to_string();
CommandResult::repl_data_fn(move |data, wtr| {
foreach_glob_path(&glob, wtr, |path, wtr| {
if data.remove_static_file(&path) {
writeln!(wtr, "removed static file `{}`", path.display()).ok();
}
});
String::from("removed static files")
})
} else {
writeln!(wtr, "rm expects a file path or glob pattern").ok();
CommandResult::Empty
}
}
fn ls_static_files<D>() -> CommandResult<D> {
CommandResult::repl_data_fn(|data, wtr| {
let sfs = data.static_files();
if sfs.is_empty() {
writeln!(wtr, "no static files imported").ok();
} else {
for sf in data.static_files() {
write!(wtr, "{}", sf.path.display()).ok();
if let Some(name) = crate::code::static_file_mod_name(&sf.path) {
write!(wtr, " -> {}", name).ok();
}
writeln!(wtr).ok();
}
}
String::new()
})
}
fn foreach_glob_path<F>(glob: &str, wtr: &mut dyn Write, mut f: F)
where
F: FnMut(PathBuf, &mut dyn Write),
{
match glob::glob(glob) {
Ok(iter) => {
for path in iter.filter_map(Result::ok) {
f(path, wtr)
}
}
Err(e) => {
writeln!(wtr, "reading `{}` failed: {}", glob, e).ok();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn make_path_test() {
assert_eq!(make_path(" "), None);
assert_eq!(make_path("lib"), Some(PathBuf::from("lib")));
assert_eq!(make_path("lib.rs"), Some(PathBuf::from("lib")));
assert_eq!(make_path("test"), Some(PathBuf::from("test")));
assert_eq!(make_path("test/inner"), Some(PathBuf::from("test/inner")));
assert_eq!(make_path("inner/test"), Some(PathBuf::from("inner/test")));
assert_eq!(make_path("//"), None);
assert_eq!(make_path("\\hello\\"), Some(PathBuf::from("hello")));
}
#[test]
fn make_all_parents_test() {
assert_eq!(make_all_parents(Path::new("")), Vec::<PathBuf>::new());
assert_eq!(make_all_parents(Path::new("test")), Vec::<PathBuf>::new());
assert_eq!(
make_all_parents(Path::new("test/inner")),
vec![PathBuf::from("test")]
);
assert_eq!(
make_all_parents(Path::new("test/inner/deep")),
vec![PathBuf::from("test"), PathBuf::from("test/inner")]
);
}
#[test]
fn test_switch_module_priv() {
let mut buf = Vec::new();
switch_module_priv::<(), _>(&[], &mut buf);
assert_eq!(
buf.as_slice(),
&b"switch expects a path to module argument\n"[..]
);
buf.clear();
switch_module_priv::<(), _>(&["foo"], &mut buf);
assert_eq!(buf.as_slice(), &b""[..]);
buf.clear();
switch_module_priv::<(), _>(&[""], &mut buf);
println!("{:?}", std::str::from_utf8(&buf));
assert_eq!(
buf.as_slice(),
&b"failed to parse into a valid module path\n"[..]
);
}
#[test]
fn test_static_file_interface() {
let mut buf = Vec::new();
add_static_file::<()>(&mut buf, &[]);
println!("{:?}", std::str::from_utf8(&buf));
assert_eq!(
buf.as_slice(),
&b"add expects a file path or glob pattern\n"[..]
);
buf.clear();
rm_static_file::<()>(&mut buf, &[]);
println!("{:?}", std::str::from_utf8(&buf));
assert_eq!(
buf.as_slice(),
&b"rm expects a file path or glob pattern\n"[..]
);
buf.clear();
rm_static_file::<()>(&mut buf, &["what"]);
}
}