mirror of
https://github.com/lune-org/lune.git
synced 2024-12-13 13:30:38 +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::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
mod stdio;
|
||||||
mod task;
|
mod task;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
pub enum LuneBuiltin {
|
pub enum LuneBuiltin {
|
||||||
Task,
|
Task,
|
||||||
|
Stdio,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'lua> LuneBuiltin
|
impl<'lua> LuneBuiltin
|
||||||
|
@ -16,15 +18,17 @@ where
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Task => "task",
|
Self::Task => "task",
|
||||||
|
Self::Stdio => "stdio",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
|
pub fn create(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
|
||||||
let res = match self {
|
let res = match self {
|
||||||
Self::Task => task::create(lua),
|
Self::Task => task::create(lua),
|
||||||
|
Self::Stdio => stdio::create(lua),
|
||||||
};
|
};
|
||||||
match res {
|
match res {
|
||||||
Ok(v) => Ok(v.into_lua_multi(lua)?),
|
Ok(v) => v.into_lua_multi(lua),
|
||||||
Err(e) => Err(e.context(format!(
|
Err(e) => Err(e.context(format!(
|
||||||
"Failed to create builtin library '{}'",
|
"Failed to create builtin library '{}'",
|
||||||
self.name()
|
self.name()
|
||||||
|
@ -38,6 +42,7 @@ impl FromStr for LuneBuiltin {
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.trim().to_ascii_lowercase().as_str() {
|
match s.trim().to_ascii_lowercase().as_str() {
|
||||||
"task" => Ok(Self::Task),
|
"task" => Ok(Self::Task),
|
||||||
|
"stdio" => Ok(Self::Stdio),
|
||||||
_ => Err(format!("Unknown builtin library '{s}'")),
|
_ => 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;
|
mod tof;
|
||||||
use tof::LuaThreadOrFunction;
|
use tof::LuaThreadOrFunction;
|
||||||
|
|
||||||
pub fn create(lua: &'static Lua) -> LuaResult<impl IntoLuaMulti<'_>> {
|
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable<'_>> {
|
||||||
TableBuilder::new(lua)?
|
TableBuilder::new(lua)?
|
||||||
.with_function("cancel", task_cancel)?
|
.with_function("cancel", task_cancel)?
|
||||||
.with_function("defer", task_defer)?
|
.with_function("defer", task_defer)?
|
||||||
|
|
|
@ -5,6 +5,8 @@ use std::{
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
use crate::lune::util::formatting::pretty_format_luau_error;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
An opaque error type for formatted lua errors.
|
An opaque error type for formatted lua errors.
|
||||||
*/
|
*/
|
||||||
|
@ -73,7 +75,11 @@ impl From<&LuaError> for LuneError {
|
||||||
|
|
||||||
impl Display for LuneError {
|
impl Display for LuneError {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
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 {
|
if let Err(err) = &res {
|
||||||
self.state.increment_error_count();
|
self.state.increment_error_count();
|
||||||
// NOTE: LuneError will pretty-format this error
|
// NOTE: LuneError will pretty-format this error
|
||||||
eprint!("{}", LuneError::from(err));
|
eprintln!("{}", LuneError::from(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send results of resuming this thread to any listeners
|
// 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;
|
mod table_builder;
|
||||||
|
|
||||||
|
pub mod formatting;
|
||||||
|
|
||||||
pub use table_builder::TableBuilder;
|
pub use table_builder::TableBuilder;
|
||||||
|
|
Loading…
Reference in a new issue