mirror of
https://github.com/lune-org/lune.git
synced 2024-12-13 05:20:37 +00:00
Add back stdio builtin and pretty error formatting
This commit is contained in:
parent
e4cf40789c
commit
73361d5a52
8 changed files with 783 additions and 4 deletions
|
@ -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}'")),
|
||||
}
|
||||
}
|
||||
|
|
108
src/lune/builtins/stdio/mod.rs
Normal file
108
src/lune/builtins/stdio/mod.rs
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
192
src/lune/builtins/stdio/prompt.rs
Normal file
192
src/lune/builtins/stdio/prompt.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)?
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
466
src/lune/util/formatting.rs
Normal 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,
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
mod table_builder;
|
||||
|
||||
pub mod formatting;
|
||||
|
||||
pub use table_builder::TableBuilder;
|
||||
|
|
Loading…
Reference in a new issue