diff --git a/Cargo.lock b/Cargo.lock index 1cc7a5b..b88ae8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1293,6 +1293,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tui-widget-list", + "winnow", ] [[package]] @@ -2013,6 +2014,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 8c3d386..87ccbe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ nix = {version = "0.31", features = ["process", "signal"]} regex = "1" crossterm = "*" dumpster = "2.1" +winnow = {version="1", features=["parser"]} diff --git a/src/format_debug_output.rs b/src/format_debug_output.rs new file mode 100644 index 0000000..8e54432 --- /dev/null +++ b/src/format_debug_output.rs @@ -0,0 +1,569 @@ +use std::{ + borrow::Cow, + fmt::{self, Display}, +}; + +use winnow::{ + Parser, + ascii::dec_uint, + combinator::impls::ByRef, + error::{FromExternalError, ParserError}, + stream, +}; + +#[derive(Copy, Clone, Debug)] +pub enum Separator { + Eq, + Colon, +} + +impl Separator { + fn parser<'a, E: ParserError<&'a str>>(input: &mut &'a str) -> Result { + use winnow::{combinator::*, prelude::*, token::*}; + + alt(( + literal('=').value(Self::Eq), + literal(':').value(Self::Colon), + )) + .parse_next(input) + } +} + +impl Display for Separator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Separator::Eq => write!(f, "="), + Separator::Colon => write!(f, ":"), + } + } +} + +#[derive(Copy, Clone, Debug)] +pub enum QuoteType { + Single, + Double, + Backtick, +} + +impl QuoteType { + fn parse<'a, E: ParserError<&'a str>>(input: &mut &'a str) -> Result { + use winnow::{combinator::*, prelude::*, token::*}; + + alt(( + literal('\'').value(Self::Single), + literal('\"').value(Self::Double), + literal('`').value(Self::Backtick), + )) + .parse_next(input) + } +} + +impl Display for QuoteType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + QuoteType::Single => write!(f, "\""), + QuoteType::Double => write!(f, "\'"), + QuoteType::Backtick => write!(f, "`"), + } + } +} + +#[derive(Clone, Debug)] +pub enum Delimiter { + Paren, + Bracket, + Brace, + Angle, +} + +impl Delimiter { + fn fmt_start(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Delimiter::Paren => write!(f, "("), + Delimiter::Bracket => write!(f, "["), + Delimiter::Brace => write!(f, "{{"), + Delimiter::Angle => write!(f, "<"), + } + } + + fn fmt_end(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Delimiter::Paren => write!(f, ")"), + Delimiter::Bracket => write!(f, "]"), + Delimiter::Brace => write!(f, "}}"), + Delimiter::Angle => write!(f, ">"), + } + } +} + +#[derive(Clone, Debug)] +pub struct AnyString<'a> { + prefix: Cow<'a, str>, + ty: QuoteType, + contents: Cow<'a, str>, + num_hashtags: usize, + suffix: Cow<'a, str>, +} + +impl<'a> AnyString<'a> { + fn parse>() -> impl Parser<&'a str, Self, E> { + use winnow::{combinator::*, prelude::*, token::*}; + + // let (prefix, num_hashtags, quote) = + let preamble = ( + take_while(0.., |b: char| !b.is_whitespace()), + take_while(0.., |c| c == '#').map(|i: &'a str| i.len()), + alt(( + '`'.value(QuoteType::Backtick), + '\''.value(QuoteType::Single), + '\"'.value(QuoteType::Double), + )), + ); + + preamble.flat_map( + |(prefix, num_hashtags, quote): (&'a str, usize, QuoteType)| { + let end = ( + match quote { + QuoteType::Single => '\'', + QuoteType::Double => '\"', + QuoteType::Backtick => '`', + }, + repeat::<_, _, Cow<'a, str>, _, _>(num_hashtags..=num_hashtags, literal("#")), + ); + + let contents = repeat_till(0.., any, end).map(|(contents, _)| contents); + let suffix = take_while(0.., |b: char| !b.is_whitespace()); + + (contents, suffix).map(move |(contents, suffix): (Cow<'a, str>, &'a str)| Self { + prefix: prefix.into(), + ty: quote, + contents, + num_hashtags, + suffix: suffix.into(), + }) + }, + ) + } +} + +impl<'a> Display for AnyString<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.prefix)?; + for _ in 0..self.num_hashtags { + write!(f, "#")?; + } + write!(f, "{}", self.ty)?; + write!(f, "{}", self.contents)?; + + for _ in 0..self.num_hashtags { + write!(f, "#")?; + } + write!(f, "{}", self.suffix) + } +} + +#[derive(Clone, Debug)] +pub struct Space<'a>(Cow<'a, str>); + +impl<'a> Space<'a> { + fn parse>() -> impl Parser<&'a str, Self, E> { + use winnow::{prelude::*, token::*}; + + take_while(0.., |b: char| b.is_whitespace()).map(|i: &'a str| Self(i.into())) + } +} + +impl<'a> Display for Space<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum PathSep { + Slash, + Backslash, +} + +impl PathSep { + fn parse<'a, E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> { + use winnow::{combinator::*, prelude::*}; + + alt(('/'.value(Self::Slash), '\\'.value(Self::Backslash))) + } +} + +impl Display for PathSep { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PathSep::Slash => write!(f, "/"), + PathSep::Backslash => write!(f, "\\"), + } + } +} + +#[derive(Clone, Debug)] +pub struct PathSegment<'a> { + leading_separator: PathSep, + segment: Cow<'a, str>, +} + +impl<'a> Display for PathSegment<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.leading_separator)?; + write!(f, "{}", self.segment) + } +} + +#[derive(Clone, Debug)] +pub struct FileLocation<'a> { + line: Cow<'a, str>, + offset: Option>, +} + +impl<'a> Display for FileLocation<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.line)?; + if let Some(offset) = &self.offset { + write!(f, "{offset}")?; + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct FileName<'a> { + leading_separator: PathSep, + segment: Cow<'a, str>, + ext_excluding_dot: Option>, + location: Option>, +} + +impl<'a> FileName<'a> { + fn parse(segment: PathSegment<'a>) -> Self { + fn rsplit<'a>( + input: Cow<'a, str>, + delimiter: char, + ) -> Option<(Cow<'a, str>, Cow<'a, str>)> { + match input { + Cow::Borrowed(s) => s + .rsplit_once(delimiter) + .map(|(a, b)| (Cow::Borrowed(a), Cow::Borrowed(b))), + Cow::Owned(s) => s + .rsplit_once(delimiter) + .map(|(a, b)| (Cow::Owned(a.to_string()), Cow::Owned(b.to_string()))), + } + } + + let (rest, location) = if let Some((before, offset)) = rsplit(segment.segment.clone(), ':') + { + if let Some((before, line)) = rsplit(before.clone(), ':') { + ( + before, + Some(FileLocation { + line, + offset: Some(offset), + }), + ) + } else { + ( + before, + Some(FileLocation { + line: offset, + offset: None, + }), + ) + } + } else { + (segment.segment, None) + }; + + let (new_segment, ext_excluding_dot) = + if let Some((segment, ext_excluding_dot)) = rsplit(rest.clone(), '.') { + (segment, Some(ext_excluding_dot)) + } else { + (rest, None) + }; + + Self { + leading_separator: segment.leading_separator, + segment: new_segment, + ext_excluding_dot, + location, + } + } +} + +impl<'a> Display for FileName<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.leading_separator)?; + write!(f, "{}", self.segment)?; + if let Some(ext) = &self.ext_excluding_dot { + write!(f, ".{ext}")?; + } + if let Some(loc) = &self.location { + write!(f, "{loc}")?; + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct Path<'a> { + drive_excluding_colon: Option, + segments: Vec>, + filename: FileName<'a>, +} + +impl<'a> Path<'a> { + fn parse>() -> impl Parser<&'a str, Self, E> { + use winnow::{combinator::*, prelude::*, token::*}; + + let till_next_sep = repeat_till( + 0.., + any::<&'a str, E>.verify(|i: &char| !(*i).is_whitespace()), + PathSep::parse(), + ) + .map(|(segment, _)| segment); + + let sep_and_next = + (PathSep::parse(), till_next_sep).map(|(leading_separator, segment)| PathSegment { + leading_separator, + segment, + }); + let drive = opt(( + any::<&'a str, E>.verify(|x: &char| matches!(*x, 'A'..='Z')), + ':', + )) + .map(|i| i.map(|(letter, _): (char, char)| letter)); + let drive_and_segments = ( + drive, + repeat_till( + 1.., + sep_and_next, + any.verify(|i: &char| (*i).is_whitespace()), + ) + .map(|(segments, _): (Vec, _)| { + let (rest, last) = { + let mut segments = segments; + let last = segments.pop().unwrap(); + (segments, last) + }; + + (rest, FileName::parse(last)) + }), + ); + + drive_and_segments.map(|(drive, (segments, filename))| Self { + drive_excluding_colon: drive, + segments, + filename, + }) + } +} + +impl<'a> Display for Path<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(drive) = &self.drive_excluding_colon { + write!(f, "{drive}:")?; + } + for segment in &self.segments { + write!(f, "{segment}:")?; + } + write!(f, "{}", self.filename) + } +} + +#[derive(Clone, Debug)] +pub struct Number<'a>(Cow<'a, str>); + +impl<'a> Number<'a> { + fn parse>() -> impl Parser<&'a str, Self, E> { + use winnow::{ascii::*, combinator::*, prelude::*}; + + alt((float::<_, f64, _>.take(), dec_int::<_, i64, _>.take())).map(|i: &str| Self(i.into())) + } +} + +impl<'a> Display for Number<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Anything that doesn't contain spaces, and that can be a prefix of `Delimited`. +/// i.e. an english word, or rust `::`-separated Path +#[derive(Clone, Debug)] +pub enum Atom<'a> { + Text(Cow<'a, str>), +} + +impl<'a> Display for Atom<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Atom::Text(text) => write!(f, "{text}"), + } + } +} + +impl<'a> Atom<'a> { + fn parse>() -> impl Parser<&'a str, Self, E> { + use winnow::{combinator::*, prelude::*, token::*}; + + alt((repeat(0.., any.verify(|i: &char| !(*i).is_whitespace())).map(Self::Text),)) + } +} + +#[derive(Clone, Debug)] +pub enum Token<'a> { + True, + False, + None, + + Path(Path<'a>), + String(AnyString<'a>), + Number(Number<'a>), + + // TODO: RustPath + Separated { + before: Box>, + space_before: Space<'a>, + separator: Separator, + after: Box>, + }, + Delimited(Delimited<'a>), + + Atom(Atom<'a>), +} + +impl<'a> Token<'a> { + fn parse>() -> impl Parser<&'a str, Self, E> { + use winnow::{combinator::*, prelude::*}; + + alt(( + "true".value(Self::True), + "false".value(Self::False), + "None".value(Self::None), + Path::parse().map(Self::Path), + AnyString::parse().map(Self::String), + Number::parse().map(Self::Number), + )) + } +} + +impl<'a> Display for Token<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Token::Number(number) => write!(f, "{number}"), + Token::True => write!(f, "true"), + Token::False => write!(f, "false"), + Token::None => write!(f, "None"), + Token::Atom(atom) => write!(f, "{atom}"), + Token::Path(path) => write!(f, "{path}"), + Token::Separated { + before, + space_before, + separator, + after, + } => write!(f, "{before}{space_before}{separator}{after}"), + Token::Delimited(delimited) => write!(f, "{delimited}"), + Token::String(s) => write!(f, "{s}"), + } + } +} + +#[derive(Clone, Debug)] +pub struct Delimited<'a> { + prefix: Atom<'a>, + delimiter: Delimiter, + contents: Segments<'a>, +} + +impl<'a> Delimited<'a> { + fn parse>() -> impl Parser<&'a str, Self, E> { + use winnow::{combinator::*, prelude::*, token::*}; + + ( + Atom::parse(), + alt(( + literal('(').map(|_| literal(')').value(Delimiter::Paren)), + literal('[').map(|_| literal(']').value(Delimiter::Bracket)), + literal('{').map(|_| literal('}').value(Delimiter::Brace)), + )) + .flat_map(|end| Segments::parse(end)), + ) + .map(|(prefix, (contents, delimiter))| Self { + prefix, + delimiter, + contents, + }) + } +} + +impl<'a> Display for Delimited<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.prefix)?; + self.delimiter.fmt_start(f)?; + write!(f, "{}", self.contents)?; + self.delimiter.fmt_end(f) + } +} + +#[derive(Clone, Debug)] +pub struct Segment<'a> { + leading_space: Space<'a>, + token: Token<'a>, +} + +impl<'a> Segment<'a> { + fn parse>() -> impl Parser<&'a str, Self, E> { + use winnow::prelude::*; + + (Space::parse(), Token::parse()).map(|(leading_space, token)| Self { + leading_space, + token, + }) + } +} + +impl<'a> Display for Segment<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.leading_space)?; + write!(f, "{}", self.token) + } +} + +#[derive(Clone, Debug)] +pub struct Segments<'a> { + segments: Vec>, + trailing_space: Space<'a>, +} + +impl<'a> Segments<'a> { + fn parse, End: 'a>( + end: impl Parser<&'a str, End, E>, + ) -> impl Parser<&'a str, (Self, End), E> { + use winnow::{combinator::*, prelude::*}; + + repeat_till(0.., Segment::parse(), (Space::parse(), end)).map( + |(segments, (trailing_space, end)): (Vec<_>, _)| { + ( + Self { + segments, + trailing_space, + }, + end, + ) + }, + ) + } +} + +impl<'a> Display for Segments<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for segment in &self.segments { + write!(f, "{segment}")?; + } + write!(f, "{}", self.trailing_space) + } +} diff --git a/src/main.rs b/src/main.rs index 432f20f..995bcfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use std::{ str::FromStr, }; +mod format_debug_output; mod tui; use clap::{Parser, Subcommand, ValueEnum, builder::PossibleValue};