From 3433fb4c4fb54119407912d0c4c8ecd892eb33f0 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Mon, 22 Apr 2024 18:11:32 +0200 Subject: [PATCH] Initial implementation of new value formatter --- crates/lune-utils/src/fmt/error/mod.rs | 3 + .../lune-utils/src/fmt/{ => error}/tests.rs | 0 crates/lune-utils/src/fmt/mod.rs | 5 +- crates/lune-utils/src/fmt/value/basic.rs | 73 +++++++++++++++ crates/lune-utils/src/fmt/value/config.rs | 48 ++++++++++ .../lune-utils/src/fmt/value/metamethods.rs | 29 ++++++ crates/lune-utils/src/fmt/value/mod.rs | 88 +++++++++++++++++++ crates/lune-utils/src/fmt/value/style.rs | 9 ++ 8 files changed, 252 insertions(+), 3 deletions(-) rename crates/lune-utils/src/fmt/{ => error}/tests.rs (100%) create mode 100644 crates/lune-utils/src/fmt/value/basic.rs create mode 100644 crates/lune-utils/src/fmt/value/config.rs create mode 100644 crates/lune-utils/src/fmt/value/metamethods.rs create mode 100644 crates/lune-utils/src/fmt/value/mod.rs create mode 100644 crates/lune-utils/src/fmt/value/style.rs diff --git a/crates/lune-utils/src/fmt/error/mod.rs b/crates/lune-utils/src/fmt/error/mod.rs index 57ac11e..00d0658 100644 --- a/crates/lune-utils/src/fmt/error/mod.rs +++ b/crates/lune-utils/src/fmt/error/mod.rs @@ -1,5 +1,8 @@ mod components; mod stack_trace; +#[cfg(test)] +mod tests; + pub use self::components::ErrorComponents; pub use self::stack_trace::{StackTrace, StackTraceLine, StackTraceSource}; diff --git a/crates/lune-utils/src/fmt/tests.rs b/crates/lune-utils/src/fmt/error/tests.rs similarity index 100% rename from crates/lune-utils/src/fmt/tests.rs rename to crates/lune-utils/src/fmt/error/tests.rs diff --git a/crates/lune-utils/src/fmt/mod.rs b/crates/lune-utils/src/fmt/mod.rs index d3e3311..0011feb 100644 --- a/crates/lune-utils/src/fmt/mod.rs +++ b/crates/lune-utils/src/fmt/mod.rs @@ -1,8 +1,7 @@ mod error; mod label; - -#[cfg(test)] -mod tests; +mod value; pub use self::error::{ErrorComponents, StackTrace, StackTraceLine, StackTraceSource}; pub use self::label::Label; +pub use self::value::{pretty_format_multi_value, pretty_format_value, ValueFormatConfig}; diff --git a/crates/lune-utils/src/fmt/value/basic.rs b/crates/lune-utils/src/fmt/value/basic.rs new file mode 100644 index 0000000..5c392ce --- /dev/null +++ b/crates/lune-utils/src/fmt/value/basic.rs @@ -0,0 +1,73 @@ +use mlua::prelude::*; + +use super::{ + metamethods::{call_table_tostring_metamethod, call_userdata_tostring_metamethod}, + style::{COLOR_CYAN, COLOR_GREEN, COLOR_MAGENTA, COLOR_YELLOW}, +}; + +const STRING_REPLACEMENTS: &[(&str, &str)] = + &[("\"", r#"\""#), ("\t", r"\t"), ("\r", r"\r"), ("\n", r"\n")]; + +/** + Tries to return the given value as a plain string key. + + A plain string key must: + + - Start with an alphabetic character. + - Only contain alphanumeric characters and underscores. +*/ +pub(crate) fn lua_value_as_plain_string_key(value: &LuaValue) -> Option { + if let LuaValue::String(s) = value { + if let Ok(s) = s.to_str() { + let first_valid = s.chars().next().is_some_and(|c| c.is_ascii_alphabetic()); + let all_valid = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + if first_valid && all_valid { + return Some(s.to_string()); + } + } + } + None +} + +/** + Formats a Lua value into a pretty string. + + This does not recursively format tables. +*/ +pub(crate) fn format_value_styled(value: &LuaValue) -> String { + match value { + LuaValue::Nil => COLOR_YELLOW.apply_to("nil").to_string(), + LuaValue::Boolean(true) => COLOR_YELLOW.apply_to("true").to_string(), + LuaValue::Boolean(false) => COLOR_YELLOW.apply_to("false").to_string(), + LuaValue::Number(n) => COLOR_CYAN.apply_to(n).to_string(), + LuaValue::Integer(i) => COLOR_CYAN.apply_to(i).to_string(), + LuaValue::String(s) => COLOR_GREEN + .apply_to({ + let mut s = s.to_string_lossy().to_string(); + for (from, to) in STRING_REPLACEMENTS { + s = s.replace(from, to); + } + format!(r#""{s}""#) + }) + .to_string(), + LuaValue::Vector(_) => COLOR_MAGENTA.apply_to("").to_string(), + LuaValue::Thread(_) => COLOR_MAGENTA.apply_to("").to_string(), + LuaValue::Function(_) => COLOR_MAGENTA.apply_to("").to_string(), + LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("").to_string(), + LuaValue::UserData(u) => { + if let Some(s) = call_userdata_tostring_metamethod(u) { + s + } else { + COLOR_MAGENTA.apply_to("").to_string() + } + } + LuaValue::Table(t) => { + if let Some(s) = call_table_tostring_metamethod(t) { + s + } else { + COLOR_MAGENTA.apply_to("").to_string() + } + } + _ => COLOR_MAGENTA.apply_to("").to_string(), + } +} diff --git a/crates/lune-utils/src/fmt/value/config.rs b/crates/lune-utils/src/fmt/value/config.rs new file mode 100644 index 0000000..eb7b238 --- /dev/null +++ b/crates/lune-utils/src/fmt/value/config.rs @@ -0,0 +1,48 @@ +/** + Configuration for formatting values. +*/ +#[derive(Debug, Clone, Copy)] +pub struct ValueFormatConfig { + pub(super) max_depth: usize, + pub(super) colors_enabled: bool, +} + +impl ValueFormatConfig { + /** + Creates a new config with default values. + */ + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /** + Sets the maximum depth to which tables will be formatted. + */ + #[must_use] + pub fn with_max_depth(self, max_depth: usize) -> Self { + Self { max_depth, ..self } + } + + /** + Sets whether colors should be enabled. + + Colors are disabled by default. + */ + #[must_use] + pub fn with_colors_enabled(self, colors_enabled: bool) -> Self { + Self { + colors_enabled, + ..self + } + } +} + +impl Default for ValueFormatConfig { + fn default() -> Self { + Self { + max_depth: 3, + colors_enabled: false, + } + } +} diff --git a/crates/lune-utils/src/fmt/value/metamethods.rs b/crates/lune-utils/src/fmt/value/metamethods.rs new file mode 100644 index 0000000..8b00b1a --- /dev/null +++ b/crates/lune-utils/src/fmt/value/metamethods.rs @@ -0,0 +1,29 @@ +use mlua::prelude::*; + +pub fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option { + let f = match tab.get_metatable() { + None => None, + Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) { + Ok(method) => Some(method), + Err(_) => None, + }, + }?; + match f.call::<_, String>(()) { + Ok(res) => Some(res), + Err(_) => None, + } +} + +pub fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option { + let f = match tab.get_metatable() { + Err(_) => None, + Ok(meta) => match meta.get::(LuaMetaMethod::ToString.name()) { + Ok(method) => Some(method), + Err(_) => None, + }, + }?; + match f.call::<_, String>(()) { + Ok(res) => Some(res), + Err(_) => None, + } +} diff --git a/crates/lune-utils/src/fmt/value/mod.rs b/crates/lune-utils/src/fmt/value/mod.rs new file mode 100644 index 0000000..8fb2e24 --- /dev/null +++ b/crates/lune-utils/src/fmt/value/mod.rs @@ -0,0 +1,88 @@ +use std::fmt::{self, Write as _}; + +use console::{colors_enabled, set_colors_enabled}; +use mlua::prelude::*; + +mod basic; +mod config; +mod metamethods; +mod style; + +pub use self::config::ValueFormatConfig; + +use self::basic::{format_value_styled, lua_value_as_plain_string_key}; +use self::style::STYLE_DIM; + +type FmtResult = Result; + +fn format_value_inner(value: &LuaValue, config: &ValueFormatConfig, depth: usize) -> FmtResult { + let mut buffer = String::new(); + + // TODO: Rewrite this section to not be recursive and + // keep track of any recursive references to tables. + if let LuaValue::Table(ref t) = value { + if depth >= config.max_depth { + write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?; + } else { + writeln!(buffer, "{}", STYLE_DIM.apply_to("{"))?; + + for res in t.clone().pairs::() { + let (key, value) = res.expect("conversion to LuaValue should never fail"); + let formatted = if let Some(plain_key) = lua_value_as_plain_string_key(&key) { + format!( + "{} {} {}{}", + plain_key, + STYLE_DIM.apply_to("="), + format_value_inner(&value, config, depth + 1)?, + STYLE_DIM.apply_to(","), + ) + } else { + format!( + "{}{}{} {} {}{}", + STYLE_DIM.apply_to("["), + format_value_inner(&key, config, depth + 1)?, + STYLE_DIM.apply_to("]"), + STYLE_DIM.apply_to("="), + format_value_inner(&value, config, depth + 1)?, + STYLE_DIM.apply_to(","), + ) + }; + buffer.push_str(&formatted); + } + + writeln!(buffer, "{}", STYLE_DIM.apply_to("}"))?; + } + } else { + write!(buffer, "{}", format_value_styled(value))?; + } + + Ok(buffer) +} + +/** + Formats a Lua value into a pretty string using the given config. +*/ +#[must_use] +#[allow(clippy::missing_panics_doc)] +pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String { + let colors_were_enabled = colors_enabled(); + set_colors_enabled(config.colors_enabled); + let res = format_value_inner(value, config, 0); + set_colors_enabled(colors_were_enabled); + res.expect("using fmt for writing into strings should never fail") +} + +/** + Formats a Lua multi-value into a pretty string using the given config. + + Each value will be separated by a space. +*/ +#[must_use] +#[allow(clippy::missing_panics_doc)] +pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String { + values + .into_iter() + .map(|value| pretty_format_value(value, config)) + .collect::>() + .join(" ") +} diff --git a/crates/lune-utils/src/fmt/value/style.rs b/crates/lune-utils/src/fmt/value/style.rs new file mode 100644 index 0000000..0a4dbe4 --- /dev/null +++ b/crates/lune-utils/src/fmt/value/style.rs @@ -0,0 +1,9 @@ +use console::Style; +use once_cell::sync::Lazy; + +pub static COLOR_GREEN: Lazy