sued 0.24.2

shut up editor - a minimalist line-based text editor written in Rust
Documentation
use crate::exit_status::ExitStatus;
use crate::EditorState;
use crate::command::{Command, CommandAction};

use mlua::Lua;

/// Initialises the Lua state into a Lua environment.
/// 
/// This is lazy-loaded, and instantiation is deferred on [evaluate_lua]'s
/// first call in the session.
pub fn init(lua: &Lua) -> ExitStatus {
    let main_lua = include_str!("lua/main.lua");

    if let Err(e) = lua.load(main_lua).exec() {
        return ExitStatus::Failure(e.to_string());
    }

    ExitStatus::Success(String::new())
}

/// Evaluates the given Lua code and returns the result as a string.
///
/// The state's buffer contents are exposed as a global variable named `sued_buffer`.
/// The state's `run_sued_command` function is also exposed as `run_sued_command`.
/// If `feedback` is true, the return value is also printed to stdout.
///
/// After running the script, the buffer contents are synchronised back to the editor
/// state if they were modified.
pub fn evaluate_lua(code: String, state: &mut EditorState, feedback: bool) -> String {
    if state.lua.is_none() {
        state.lua = Some(mlua::Lua::new());
        init(state.lua.as_ref().unwrap());
    }

    let lua = state.lua.as_ref().unwrap().clone();

    if let Err(e) = lua.globals().set("sued_buffer", state.buffer.contents.clone()) {
        return format!("failed to set buffer contents as lua global: {}", e);
    }

    if let Err(e) = lua.globals().set("sued_file_path", state.buffer.file_path.clone()) {
        return format!("failed to set file path as lua global: {}", e);
    }

    let mut is_error: bool = false;

    let result = match lua.load(&code).eval::<mlua::Value>() {
        Ok(value) => match value {
            mlua::Value::Nil => "nil".to_string(),
            mlua::Value::Boolean(b) => b.to_string(),
            mlua::Value::Integer(n) => n.to_string(),
            mlua::Value::Number(n) => n.to_string(),
            mlua::Value::String(s) => {
                match s.to_str() {
                    Ok(s) => s.to_string(),
                    Err(_) => format!("invalid string {:?}", s),
                }
            }
            mlua::Value::Table(t) => {
                let table = t.pairs::<String, String>();
                let table_output = table.map(|result| match result {
                    Ok((_k, v)) => v,
                    Err(e) => format!("invalid table entry: {}", e),
                });
                format!("[{}]", table_output.collect::<Vec<String>>().join(", "))
            },
            mlua::Value::Error(e) => {
                is_error = true;
                format!("error: {}", e)
            },
            _ => {
                let return_type = value.type_name();
                format!("unsupported return type: {}", return_type)
            }
        },
        Err(e) => e.to_string(),
    };

    match lua.globals().get::<Vec<String>>("sued_buffer") {
        Ok(buffer) => {
            if buffer != state.buffer.contents {
                println!("modifications synchronised");
                state.buffer.contents = buffer;
            }
        },
        Err(_) => println!("couldn't read back buffer contents"),
    }

    if !feedback {
        if is_error {
            return format!("error: {result}");
        }
        return "eval finished".to_string();
    }

    format!("=> {result}")
}

pub fn eval() -> Command {
    Command {
        name: "eval",
        arguments: vec!["lua_code"],
        description: "evaluate lua",
        documentation: "this command evaluates lua code from the arguments \
                        and displays the result, if any\n\
                        the buffer contents are added as a lua global, `sued_buffer` \
                        and like with ~runhere, any modifications made to the buffer \
                        are synchronised back",
        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
            if args.len() < 2 {
                return format!(
                    "eval what? try {}eval [lua_code]\n\
                    if you want to run the buffer as lua, use {}script this instead\n\
                    or if you want to run a lua file, {}script [filename]",
                    state.prefix, state.prefix, state.prefix
                );
            }

            let code = args[1..].join(" ");

            evaluate_lua(code, state, true)
        }),
        ..Command::default()
    }
}

pub fn script() -> Command {
    Command {
        name: "script",
        arguments: vec!["filename"],
        description: "run a lua script",
        documentation: "this command imports and runs a lua script at filename\n\
                        if `this` is specified instead of the filename, it'll \
                        treat the buffer contents as a lua script\n\
                        like ~eval, `sued_buffer` is added as a lua global and \
                        any modifications made to the buffer are synchronised back\n\
                        unlike ~eval, the return value is not printed unless \
                        it's an error",
        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
            if args.len() < 2 {
                return format!(
                    "what script? try {}script [filename] or {}script this",
                    state.prefix, state.prefix
                );
            }

            let filename = args[1..].join(" ");

            let code = if filename == "this" {
                state.buffer.contents.join("\n")
            }
            else {
                match std::fs::read_to_string(&filename) {
                    Ok(contents) => contents,
                    Err(e) => {
                        match e.kind() {
                            std::io::ErrorKind::NotFound => {
                                return format!("{} doesn't exist\n\
                                                if you want to run a lua expression, use {}eval [expr] instead", 
                                                &filename, state.prefix)
                            },
                            _ => {
                                return format!("failed to read file: {}", e)
                            }
                        }
                    }
                }
            };

            evaluate_lua(code, state, true)
        }),
        ..Command::default()
    }
}