Add back stdio builtin and pretty error formatting

This commit is contained in:
Filip Tibell 2023-08-19 19:13:50 -05:00
parent e4cf40789c
commit 73361d5a52
8 changed files with 783 additions and 4 deletions

View file

@ -2,11 +2,13 @@ use std::str::FromStr;
use mlua::prelude::*;
mod stdio;
mod task;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum LuneBuiltin {
Task,
Stdio,
}
impl<'lua> LuneBuiltin
@ -16,15 +18,17 @@ where
pub fn name(&self) -> &'static str {
match self {
Self::Task => "task",
Self::Stdio => "stdio",
}
}
pub fn create(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
let res = match self {
Self::Task => task::create(lua),
Self::Stdio => stdio::create(lua),
};
match res {
Ok(v) => Ok(v.into_lua_multi(lua)?),
Ok(v) => v.into_lua_multi(lua),
Err(e) => Err(e.context(format!(
"Failed to create builtin library '{}'",
self.name()
@ -38,6 +42,7 @@ impl FromStr for LuneBuiltin {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"task" => Ok(Self::Task),
"stdio" => Ok(Self::Stdio),
_ => Err(format!("Unknown builtin library '{s}'")),
}
}

View file

@ -0,0 +1,108 @@
use mlua::prelude::*;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
use tokio::{
io::{self, AsyncWriteExt},
task,
};
use crate::lune::util::{
formatting::{
format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str,
},
TableBuilder,
};
mod prompt;
use prompt::{PromptKind, PromptOptions, PromptResult};
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable<'_>> {
TableBuilder::new(lua)?
.with_function("color", stdio_color)?
.with_function("style", stdio_style)?
.with_function("format", stdio_format)?
.with_async_function("write", stdio_write)?
.with_async_function("ewrite", stdio_ewrite)?
.with_async_function("prompt", stdio_prompt)?
.build_readonly()
}
fn stdio_color(_: &Lua, color: String) -> LuaResult<String> {
let ansi_string = format_style(style_from_color_str(&color)?);
Ok(ansi_string)
}
fn stdio_style(_: &Lua, color: String) -> LuaResult<String> {
let ansi_string = format_style(style_from_style_str(&color)?);
Ok(ansi_string)
}
fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult<String> {
pretty_format_multi_value(&args)
}
async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
let mut stdout = io::stdout();
stdout.write_all(s.as_bytes()).await?;
stdout.flush().await?;
Ok(())
}
async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
let mut stderr = io::stderr();
stderr.write_all(s.as_bytes()).await?;
stderr.flush().await?;
Ok(())
}
async fn stdio_prompt(_: &Lua, options: PromptOptions) -> LuaResult<PromptResult> {
task::spawn_blocking(move || prompt(options))
.await
.into_lua_err()?
}
fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
let theme = ColorfulTheme::default();
match options.kind {
PromptKind::Text => {
let input: String = Input::with_theme(&theme)
.allow_empty(true)
.with_prompt(options.text.unwrap_or_default())
.with_initial_text(options.default_string.unwrap_or_default())
.interact_text()?;
Ok(PromptResult::String(input))
}
PromptKind::Confirm => {
let mut prompt = Confirm::with_theme(&theme);
if let Some(b) = options.default_bool {
prompt.default(b);
};
let result = prompt
.with_prompt(&options.text.expect("Missing text in prompt options"))
.interact()?;
Ok(PromptResult::Boolean(result))
}
PromptKind::Select => {
let chosen = Select::with_theme(&theme)
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()?;
Ok(match chosen {
Some(idx) => PromptResult::Index(idx + 1),
None => PromptResult::None,
})
}
PromptKind::MultiSelect => {
let chosen = MultiSelect::with_theme(&theme)
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()?;
Ok(match chosen {
None => PromptResult::None,
Some(indices) => {
PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect())
}
})
}
}
}

View file

@ -0,0 +1,192 @@
use std::fmt;
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub enum PromptKind {
Text,
Confirm,
Select,
MultiSelect,
}
impl PromptKind {
fn get_all() -> Vec<Self> {
vec![Self::Text, Self::Confirm, Self::Select, Self::MultiSelect]
}
}
impl Default for PromptKind {
fn default() -> Self {
Self::Text
}
}
impl fmt::Display for PromptKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Text => "Text",
Self::Confirm => "Confirm",
Self::Select => "Select",
Self::MultiSelect => "MultiSelect",
}
)
}
}
impl<'lua> FromLua<'lua> for PromptKind {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::Nil = value {
Ok(Self::default())
} else if let LuaValue::String(s) = value {
let s = s.to_str()?;
/*
If the user only typed the prompt kind slightly wrong, meaning
it has some kind of space in it, a weird character, or an uppercase
character, we should try to be permissive as possible and still work
Not everyone is using an IDE with proper Luau type definitions
installed, and Luau is still a permissive scripting language
even though it has a strict (but optional) type system
*/
let s = s
.chars()
.filter_map(|c| {
if c.is_ascii_alphabetic() {
Some(c.to_ascii_lowercase())
} else {
None
}
})
.collect::<String>();
// If the prompt kind is still invalid we will
// show the user a descriptive error message
match s.as_ref() {
"text" => Ok(Self::Text),
"confirm" => Ok(Self::Confirm),
"select" => Ok(Self::Select),
"multiselect" => Ok(Self::MultiSelect),
s => Err(LuaError::FromLuaConversionError {
from: "string",
to: "PromptKind",
message: Some(format!(
"Invalid prompt kind '{s}', valid kinds are:\n{}",
PromptKind::get_all()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptKind",
message: None,
})
}
}
}
pub struct PromptOptions {
pub kind: PromptKind,
pub text: Option<String>,
pub default_string: Option<String>,
pub default_bool: Option<bool>,
pub options: Option<Vec<String>>,
}
impl<'lua> FromLuaMulti<'lua> for PromptOptions {
fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
// Argument #1 - prompt kind (optional)
let kind = values
.pop_front()
.map(|value| PromptKind::from_lua(value, lua))
.transpose()?
.unwrap_or_default();
// Argument #2 - prompt text (optional)
let text = values
.pop_front()
.map(|text| String::from_lua(text, lua))
.transpose()?;
// Argument #3 - default value / options,
// this is different per each prompt kind
let (default_bool, default_string, options) = match values.pop_front() {
None => (None, None, None),
Some(options) => match options {
LuaValue::Nil => (None, None, None),
LuaValue::Boolean(b) => (Some(b), None, None),
LuaValue::String(s) => (
None,
Some(String::from_lua(LuaValue::String(s), lua)?),
None,
),
LuaValue::Table(t) => (
None,
None,
Some(Vec::<String>::from_lua(LuaValue::Table(t), lua)?),
),
value => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "PromptOptions",
message: Some("Argument #3 must be a boolean, table, or nil".to_string()),
})
}
},
};
/*
Make sure we got the required values for the specific prompt kind:
- "Confirm" requires a message to be present so the user knows what they are confirming
- "Select" and "MultiSelect" both require a table of options to choose from
*/
if matches!(kind, PromptKind::Confirm) && text.is_none() {
return Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptOptions",
message: Some("Argument #2 missing or nil".to_string()),
});
}
if matches!(kind, PromptKind::Select | PromptKind::MultiSelect) && options.is_none() {
return Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptOptions",
message: Some("Argument #3 missing or nil".to_string()),
});
}
// All good, return the prompt options
Ok(Self {
kind,
text,
default_bool,
default_string,
options,
})
}
}
#[derive(Debug, Clone)]
pub enum PromptResult {
String(String),
Boolean(bool),
Index(usize),
Indices(Vec<usize>),
None,
}
impl<'lua> IntoLua<'lua> for PromptResult {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
Ok(match self {
Self::String(s) => LuaValue::String(lua.create_string(&s)?),
Self::Boolean(b) => LuaValue::Boolean(b),
Self::Index(i) => LuaValue::Number(i as f64),
Self::Indices(v) => v.into_lua(lua)?,
Self::None => LuaValue::Nil,
})
}
}

View file

@ -9,7 +9,7 @@ use crate::lune::{scheduler::Scheduler, util::TableBuilder};
mod tof;
use tof::LuaThreadOrFunction;
pub fn create(lua: &'static Lua) -> LuaResult<impl IntoLuaMulti<'_>> {
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable<'_>> {
TableBuilder::new(lua)?
.with_function("cancel", task_cancel)?
.with_function("defer", task_defer)?

View file

@ -5,6 +5,8 @@ use std::{
use mlua::prelude::*;
use crate::lune::util::formatting::pretty_format_luau_error;
/**
An opaque error type for formatted lua errors.
*/
@ -73,7 +75,11 @@ impl From<&LuaError> for LuneError {
impl Display for LuneError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.error) // TODO: Pretty formatting
write!(
f,
"{}",
pretty_format_luau_error(&self.error, !self.disable_colors)
)
}
}

View file

@ -54,7 +54,7 @@ where
if let Err(err) = &res {
self.state.increment_error_count();
// NOTE: LuneError will pretty-format this error
eprint!("{}", LuneError::from(err));
eprintln!("{}", LuneError::from(err));
}
// Send results of resuming this thread to any listeners

466
src/lune/util/formatting.rs Normal file
View file

@ -0,0 +1,466 @@
use std::fmt::Write;
use console::{colors_enabled, set_colors_enabled, style, Style};
use mlua::prelude::*;
use once_cell::sync::Lazy;
const MAX_FORMAT_DEPTH: usize = 4;
const INDENT: &str = " ";
pub const STYLE_RESET_STR: &str = "\x1b[0m";
// Colors
pub static COLOR_BLACK: Lazy<Style> = Lazy::new(|| Style::new().black());
pub static COLOR_RED: Lazy<Style> = Lazy::new(|| Style::new().red());
pub static COLOR_GREEN: Lazy<Style> = Lazy::new(|| Style::new().green());
pub static COLOR_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow());
pub static COLOR_BLUE: Lazy<Style> = Lazy::new(|| Style::new().blue());
pub static COLOR_PURPLE: Lazy<Style> = Lazy::new(|| Style::new().magenta());
pub static COLOR_CYAN: Lazy<Style> = Lazy::new(|| Style::new().cyan());
pub static COLOR_WHITE: Lazy<Style> = Lazy::new(|| Style::new().white());
// Styles
pub static STYLE_BOLD: Lazy<Style> = Lazy::new(|| Style::new().bold());
pub static STYLE_DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
fn can_be_plain_lua_table_key(s: &LuaString) -> bool {
let str = s.to_string_lossy().to_string();
let first_char = str.chars().next().unwrap();
if first_char.is_alphabetic() {
str.chars().all(|c| c == '_' || c.is_alphanumeric())
} else {
false
}
}
pub fn format_label<S: AsRef<str>>(s: S) -> String {
format!(
"{}{}{} ",
style("[").dim(),
match s.as_ref().to_ascii_lowercase().as_str() {
"info" => style("INFO").blue(),
"warn" => style("WARN").yellow(),
"error" => style("ERROR").red(),
_ => style(""),
},
style("]").dim()
)
}
pub fn format_style(style: Option<&'static Style>) -> String {
if cfg!(test) {
"".to_string()
} else if let Some(style) = style {
// HACK: We have no direct way of referencing the ansi color code
// of the style that console::Style provides, and we also know for
// sure that styles always include the reset sequence at the end,
// unless we are in a CI environment on non-interactive terminal
style
.apply_to("")
.to_string()
.trim_end_matches(STYLE_RESET_STR)
.to_string()
} else {
STYLE_RESET_STR.to_string()
}
}
pub fn style_from_color_str<S: AsRef<str>>(s: S) -> LuaResult<Option<&'static Style>> {
Ok(match s.as_ref() {
"reset" => None,
"black" => Some(&COLOR_BLACK),
"red" => Some(&COLOR_RED),
"green" => Some(&COLOR_GREEN),
"yellow" => Some(&COLOR_YELLOW),
"blue" => Some(&COLOR_BLUE),
"purple" => Some(&COLOR_PURPLE),
"cyan" => Some(&COLOR_CYAN),
"white" => Some(&COLOR_WHITE),
_ => {
return Err(LuaError::RuntimeError(format!(
"The color '{}' is not a valid color name",
s.as_ref()
)));
}
})
}
pub fn style_from_style_str<S: AsRef<str>>(s: S) -> LuaResult<Option<&'static Style>> {
Ok(match s.as_ref() {
"reset" => None,
"bold" => Some(&STYLE_BOLD),
"dim" => Some(&STYLE_DIM),
_ => {
return Err(LuaError::RuntimeError(format!(
"The style '{}' is not a valid style name",
s.as_ref()
)));
}
})
}
pub fn pretty_format_value(
buffer: &mut String,
value: &LuaValue,
depth: usize,
) -> std::fmt::Result {
// TODO: Handle tables with cyclic references
match &value {
LuaValue::Nil => write!(buffer, "nil")?,
LuaValue::Boolean(true) => write!(buffer, "{}", COLOR_YELLOW.apply_to("true"))?,
LuaValue::Boolean(false) => write!(buffer, "{}", COLOR_YELLOW.apply_to("false"))?,
LuaValue::Number(n) => write!(buffer, "{}", COLOR_CYAN.apply_to(format!("{n}")))?,
LuaValue::Integer(i) => write!(buffer, "{}", COLOR_CYAN.apply_to(format!("{i}")))?,
LuaValue::String(s) => write!(
buffer,
"\"{}\"",
COLOR_GREEN.apply_to(
s.to_string_lossy()
.replace('"', r#"\""#)
.replace('\r', r#"\r"#)
.replace('\n', r#"\n"#)
)
)?,
LuaValue::Table(ref tab) => {
if depth >= MAX_FORMAT_DEPTH {
write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?;
} else if let Some(s) = call_table_tostring_metamethod(tab) {
write!(buffer, "{s}")?;
} else {
let mut is_empty = false;
let depth_indent = INDENT.repeat(depth);
write!(buffer, "{}", STYLE_DIM.apply_to("{"))?;
for pair in tab.clone().pairs::<LuaValue, LuaValue>() {
let (key, value) = pair.unwrap();
match &key {
LuaValue::String(s) if can_be_plain_lua_table_key(s) => write!(
buffer,
"\n{}{}{} {} ",
depth_indent,
INDENT,
s.to_string_lossy(),
STYLE_DIM.apply_to("=")
)?,
_ => {
write!(buffer, "\n{depth_indent}{INDENT}[")?;
pretty_format_value(buffer, &key, depth)?;
write!(buffer, "] {} ", STYLE_DIM.apply_to("="))?;
}
}
pretty_format_value(buffer, &value, depth + 1)?;
write!(buffer, "{}", STYLE_DIM.apply_to(","))?;
is_empty = false;
}
if is_empty {
write!(buffer, "{}", STYLE_DIM.apply_to(" }"))?;
} else {
write!(buffer, "\n{depth_indent}{}", STYLE_DIM.apply_to("}"))?;
}
}
}
LuaValue::Vector(v) => write!(
buffer,
"{}",
COLOR_PURPLE.apply_to(format!(
"<vector({x}, {y}, {z})>",
x = v.x(),
y = v.y(),
z = v.z()
))
)?,
LuaValue::Thread(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<thread>"))?,
LuaValue::Function(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<function>"))?,
LuaValue::UserData(u) => {
if let Some(s) = call_userdata_tostring_metamethod(u) {
write!(buffer, "{s}")?
} else {
write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?
}
}
LuaValue::LightUserData(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?,
LuaValue::Error(e) => write!(buffer, "{}", pretty_format_luau_error(e, false),)?,
}
Ok(())
}
pub fn pretty_format_multi_value(multi: &LuaMultiValue) -> LuaResult<String> {
let mut buffer = String::new();
let mut counter = 0;
for value in multi {
counter += 1;
if let LuaValue::String(s) = value {
write!(buffer, "{}", s.to_string_lossy()).into_lua_err()?;
} else {
pretty_format_value(&mut buffer, value, 0).into_lua_err()?;
}
if counter < multi.len() {
write!(&mut buffer, " ").into_lua_err()?;
}
}
Ok(buffer)
}
pub fn pretty_format_luau_error(e: &LuaError, colorized: bool) -> String {
let previous_colors_enabled = if !colorized {
set_colors_enabled(false);
Some(colors_enabled())
} else {
None
};
let stack_begin = format!("[{}]", COLOR_BLUE.apply_to("Stack Begin"));
let stack_end = format!("[{}]", COLOR_BLUE.apply_to("Stack End"));
let err_string = match e {
LuaError::RuntimeError(e) => {
// Remove unnecessary prefix
let mut err_string = e.to_string();
if let Some(no_prefix) = err_string.strip_prefix("runtime error: ") {
err_string = no_prefix.to_string();
}
// Add "Stack Begin" instead of default stack traceback string
let mut err_lines = err_string
.lines()
.map(|s| s.to_string())
.collect::<Vec<String>>();
let mut found_stack_begin = false;
for (index, line) in err_lines.clone().iter().enumerate().rev() {
if *line == "stack traceback:" {
err_lines[index] = stack_begin.clone();
found_stack_begin = true;
break;
}
}
// Add "Stack End" to the very end of the stack trace for symmetry
if found_stack_begin {
err_lines.push(stack_end.clone());
}
err_lines.join("\n")
}
LuaError::CallbackError { traceback, cause } => {
// Find the best traceback (most lines) and the root error message
// The traceback may also start with "override traceback:" which
// means it was passed from somewhere that wants a custom trace,
// so we should then respect that and get the best override instead
let mut full_trace = traceback.to_string();
let mut root_cause = cause.as_ref();
let mut trace_override = false;
while let LuaError::CallbackError { cause, traceback } = root_cause {
let is_override = traceback.starts_with("override traceback:");
if is_override {
if !trace_override || traceback.lines().count() > full_trace.len() {
full_trace = traceback
.trim_start_matches("override traceback:")
.to_string();
trace_override = true;
}
} else if !trace_override {
full_trace = format!("{traceback}\n{full_trace}");
}
root_cause = cause;
}
// If we got a runtime error with an embedded traceback, we should
// use that instead since it generally contains more information
if matches!(root_cause, LuaError::RuntimeError(e) if e.contains("stack traceback:")) {
pretty_format_luau_error(root_cause, colorized)
} else {
// Otherwise we format whatever root error we got using
// the same error formatting as for above runtime errors
format!(
"{}\n{}\n{}\n{}",
pretty_format_luau_error(root_cause, colorized),
stack_begin,
full_trace.trim_start_matches("stack traceback:\n"),
stack_end
)
}
}
LuaError::BadArgument { pos, cause, .. } => match cause.as_ref() {
// TODO: Add more detail to this error message
LuaError::FromLuaConversionError { from, to, .. } => {
format!("Argument #{pos} must be of type '{to}', got '{from}'")
}
c => format!(
"Bad argument #{pos}\n{}",
pretty_format_luau_error(c, colorized)
),
},
e => format!("{e}"),
};
// Re-enable colors if they were previously enabled
if let Some(true) = previous_colors_enabled {
set_colors_enabled(true)
}
// Remove the script path from the error message
// itself, it can be found in the stack trace
let mut err_lines = err_string.lines().collect::<Vec<_>>();
if let Some(first_line) = err_lines.first() {
if first_line.starts_with("[string \"") {
if let Some(closing_bracket) = first_line.find("]:") {
let after_closing_bracket = &first_line[closing_bracket + 2..first_line.len()];
if let Some(last_colon) = after_closing_bracket.find(": ") {
err_lines[0] = &after_closing_bracket
[last_colon + 2..first_line.len() - closing_bracket - 2];
} else {
err_lines[0] = after_closing_bracket
}
}
}
}
// Find where the stack trace stars and ends
let stack_begin_idx =
err_lines.iter().enumerate().find_map(
|(i, line)| {
if *line == stack_begin {
Some(i)
} else {
None
}
},
);
let stack_end_idx =
err_lines.iter().enumerate().find_map(
|(i, line)| {
if *line == stack_end {
Some(i)
} else {
None
}
},
);
// If we have a stack trace, we should transform the formatting from the
// default mlua formatting into something more friendly, similar to Roblox
if let (Some(idx_start), Some(idx_end)) = (stack_begin_idx, stack_end_idx) {
let stack_lines = err_lines
.iter()
.enumerate()
// Filter out stack lines
.filter_map(|(idx, line)| {
if idx > idx_start && idx < idx_end {
Some(*line)
} else {
None
}
})
// Transform from mlua format into friendly format, while also
// ensuring that leading whitespace / indentation is consistent
.map(transform_stack_line)
.collect::<Vec<_>>();
fix_error_nitpicks(format!(
"{}\n{}\n{}\n{}",
err_lines
.iter()
.take(idx_start)
.copied()
.collect::<Vec<_>>()
.join("\n"),
stack_begin,
stack_lines.join("\n"),
stack_end,
))
} else {
fix_error_nitpicks(err_string)
}
}
fn transform_stack_line(line: &str) -> String {
match (line.find('['), line.find(']')) {
(Some(idx_start), Some(idx_end)) => {
let name = line[idx_start..idx_end + 1]
.trim_start_matches('[')
.trim_start_matches("string ")
.trim_start_matches('"')
.trim_end_matches(']')
.trim_end_matches('"');
let after_name = &line[idx_end + 1..];
let line_num = match after_name.find(':') {
Some(lineno_start) => match after_name[lineno_start + 1..].find(':') {
Some(lineno_end) => &after_name[lineno_start + 1..lineno_end + 1],
None => match after_name.contains("in function") || after_name.contains("in ?")
{
false => &after_name[lineno_start + 1..],
true => "",
},
},
None => "",
};
let func_name = match after_name.find("in function ") {
Some(func_start) => after_name[func_start + 12..]
.trim()
.trim_end_matches('\'')
.trim_start_matches('\'')
.trim_start_matches("_G."),
None => "",
};
let mut result = String::new();
write!(
result,
" Script '{}'",
match name {
"C" => "[C]",
name => name,
},
)
.unwrap();
if !line_num.is_empty() {
write!(result, ", Line {line_num}").unwrap();
}
if !func_name.is_empty() {
write!(result, " - function {func_name}").unwrap();
}
result
}
(_, _) => line.to_string(),
}
}
fn fix_error_nitpicks(full_message: String) -> String {
full_message
// Hacky fix for our custom require appearing as a normal script
// TODO: It's probably better to pull in the regex crate here ..
.replace("'require', Line 5", "'[C]' - function require")
.replace("'require', Line 7", "'[C]' - function require")
.replace("'require', Line 8", "'[C]' - function require")
// Same thing here for our async script
.replace("'async', Line 2", "'[C]'")
.replace("'async', Line 3", "'[C]'")
// Fix error calls in custom script chunks coming through
.replace(
"'[C]' - function error\n Script '[C]' - function require",
"'[C]' - function require",
)
// Fix strange double require
.replace(
"'[C]' - function require - function require",
"'[C]' - function require",
)
// Fix strange double C
.replace("'[C]'\n Script '[C]'", "'[C]'")
}
fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
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,
}
}
fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
let f = match tab.get_metatable() {
Err(_) => None,
Ok(meta) => match meta.get::<LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
}

View file

@ -1,3 +1,5 @@
mod table_builder;
pub mod formatting;
pub use table_builder::TableBuilder;