diff --git a/src/common.rs b/src/common.rs index f29c924c..1fa2da75 100644 --- a/src/common.rs +++ b/src/common.rs @@ -31,9 +31,8 @@ pub(crate) use crate::testing; // functions pub(crate) use crate::{ - load_dotenv::load_dotenv, - misc::{default, empty}, - output::output, + default::default, empty::empty, load_dotenv::load_dotenv, output::output, + write_message_context::write_message_context, }; // structs and enums @@ -41,12 +40,13 @@ pub(crate) use crate::{ alias::Alias, alias_resolver::AliasResolver, assignment_evaluator::AssignmentEvaluator, assignment_resolver::AssignmentResolver, color::Color, compilation_error::CompilationError, compilation_error_kind::CompilationErrorKind, config::Config, config_error::ConfigError, - expression::Expression, fragment::Fragment, function::Function, - function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard, - interrupt_handler::InterruptHandler, justfile::Justfile, lexer::Lexer, output_error::OutputError, - parameter::Parameter, parser::Parser, platform::Platform, position::Position, recipe::Recipe, - recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError, - search_error::SearchError, shebang::Shebang, state::State, string_literal::StringLiteral, + count::Count, enclosure::Enclosure, expression::Expression, fragment::Fragment, + function::Function, function_context::FunctionContext, functions::Functions, + interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, justfile::Justfile, + lexer::Lexer, list::List, output_error::OutputError, parameter::Parameter, parser::Parser, + platform::Platform, position::Position, recipe::Recipe, recipe_context::RecipeContext, + recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search_error::SearchError, + shebang::Shebang, show_whitespace::ShowWhitespace, state::State, string_literal::StringLiteral, subcommand::Subcommand, token::Token, token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning, }; diff --git a/src/compilation_error.rs b/src/compilation_error.rs index b203c778..06b106c5 100644 --- a/src/compilation_error.rs +++ b/src/compilation_error.rs @@ -1,7 +1,5 @@ use crate::common::*; -use crate::misc::{maybe_s, show_whitespace, write_message_context, Or}; - #[derive(Debug, PartialEq)] pub(crate) struct CompilationError<'a> { pub(crate) text: &'a str, @@ -82,7 +80,7 @@ impl<'a> Display for CompilationError<'a> { ref expected, found, } => { - writeln!(f, "Expected {}, but found {}", Or(expected), found)?; + writeln!(f, "Expected {}, but found {}", List::or(expected), found)?; } DuplicateAlias { alias, first } => { writeln!( @@ -139,7 +137,7 @@ impl<'a> Display for CompilationError<'a> { f, "Found a mix of tabs and spaces in leading whitespace: `{}`\n\ Leading whitespace may consist of tabs or spaces, but not both", - show_whitespace(whitespace) + ShowWhitespace(whitespace) )?; } ExtraLeadingWhitespace => { @@ -152,10 +150,10 @@ impl<'a> Display for CompilationError<'a> { } => { writeln!( f, - "Function `{}` called with {} argument{} but takes {}", + "Function `{}` called with {} {} but takes {}", function, found, - maybe_s(found), + Count("argument", found), expected )?; } @@ -164,8 +162,8 @@ impl<'a> Display for CompilationError<'a> { f, "Recipe line has inconsistent leading whitespace. \ Recipe started with `{}` but found line with `{}`", - show_whitespace(expected), - show_whitespace(found) + ShowWhitespace(expected), + ShowWhitespace(found) )?; } UnknownAliasTarget { alias, target } => { diff --git a/src/count.rs b/src/count.rs new file mode 100644 index 00000000..84f52e65 --- /dev/null +++ b/src/count.rs @@ -0,0 +1,25 @@ +use crate::common::*; + +pub struct Count(pub T, pub usize); + +impl Display for Count { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if self.1 == 1 { + write!(f, "{}", self.0) + } else { + write!(f, "{}s", self.0) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn count() { + assert_eq!(Count("dog", 0).to_string(), "dogs"); + assert_eq!(Count("dog", 1).to_string(), "dog"); + assert_eq!(Count("dog", 2).to_string(), "dogs"); + } +} diff --git a/src/default.rs b/src/default.rs new file mode 100644 index 00000000..8a5bc093 --- /dev/null +++ b/src/default.rs @@ -0,0 +1,3 @@ +pub(crate) fn default() -> T { + Default::default() +} diff --git a/src/empty.rs b/src/empty.rs new file mode 100644 index 00000000..51e4695e --- /dev/null +++ b/src/empty.rs @@ -0,0 +1,5 @@ +use crate::common::*; + +pub(crate) fn empty>() -> C { + iter::empty().collect() +} diff --git a/src/enclosure.rs b/src/enclosure.rs new file mode 100644 index 00000000..edeb6f83 --- /dev/null +++ b/src/enclosure.rs @@ -0,0 +1,31 @@ +use crate::common::*; + +pub struct Enclosure { + enclosure: &'static str, + value: T, +} + +impl Enclosure { + pub fn tick(value: T) -> Enclosure { + Enclosure { + enclosure: "`", + value, + } + } +} + +impl Display for Enclosure { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}{}{}", self.enclosure, self.value, self.enclosure) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tick() { + assert_eq!(Enclosure::tick("foo").to_string(), "`foo`") + } +} diff --git a/src/lib.rs b/src/lib.rs index 6c485459..2c1cdf18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,10 @@ mod compilation_error; mod compilation_error_kind; mod config; mod config_error; +mod count; +mod default; +mod empty; +mod enclosure; mod expression; mod fragment; mod function; @@ -31,8 +35,8 @@ mod interrupt_guard; mod interrupt_handler; mod justfile; mod lexer; +mod list; mod load_dotenv; -mod misc; mod ordinal; mod output; mod output_error; @@ -50,6 +54,7 @@ mod runtime_error; mod search; mod search_error; mod shebang; +mod show_whitespace; mod state; mod string_literal; mod subcommand; @@ -59,6 +64,7 @@ mod use_color; mod variables; mod verbosity; mod warning; +mod write_message_context; pub use crate::run::run; diff --git a/src/list.rs b/src/list.rs new file mode 100644 index 00000000..37e949bc --- /dev/null +++ b/src/list.rs @@ -0,0 +1,123 @@ +use crate::common::*; + +pub struct List + Clone> { + conjunction: &'static str, + values: I, +} + +impl + Clone> List { + pub fn or>(values: II) -> List { + List { + conjunction: "or", + values: values.into_iter(), + } + } + + pub fn and>(values: II) -> List { + List { + conjunction: "and", + values: values.into_iter(), + } + } + + pub fn or_ticked>( + values: II, + ) -> List, impl Iterator> + Clone> { + List::or(values.into_iter().map(Enclosure::tick)) + } + + pub fn and_ticked>( + values: II, + ) -> List, impl Iterator> + Clone> { + List::and(values.into_iter().map(Enclosure::tick)) + } +} + +impl + Clone> Display for List { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let mut values = self.values.clone().fuse(); + + if let Some(first) = values.next() { + write!(f, "{}", first)?; + } else { + return Ok(()); + } + + let second = values.next(); + + if second.is_none() { + return Ok(()); + } + + let third = values.next(); + + if let (Some(second), None) = (second.as_ref(), third.as_ref()) { + write!(f, " {} {}", self.conjunction, second)?; + return Ok(()); + } + + let mut current = second; + let mut next = third; + + loop { + match (current, next) { + (Some(c), Some(n)) => { + write!(f, ", {}", c)?; + current = Some(n); + next = values.next(); + } + (Some(c), None) => { + write!(f, ", {} {}", self.conjunction, c)?; + return Ok(()); + } + _ => panic!("Iterator was fused, but returned Some after None"), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn or() { + assert_eq!("1", List::or(&[1]).to_string()); + assert_eq!("1 or 2", List::or(&[1, 2]).to_string()); + assert_eq!("1, 2, or 3", List::or(&[1, 2, 3]).to_string()); + assert_eq!("1, 2, 3, or 4", List::or(&[1, 2, 3, 4]).to_string()); + } + + #[test] + fn and() { + assert_eq!("1", List::and(&[1]).to_string()); + assert_eq!("1 and 2", List::and(&[1, 2]).to_string()); + assert_eq!("1, 2, and 3", List::and(&[1, 2, 3]).to_string()); + assert_eq!("1, 2, 3, and 4", List::and(&[1, 2, 3, 4]).to_string()); + } + + #[test] + fn or_ticked() { + assert_eq!("`1`", List::or_ticked(&[1]).to_string()); + assert_eq!("`1` or `2`", List::or_ticked(&[1, 2]).to_string()); + assert_eq!("`1`, `2`, or `3`", List::or_ticked(&[1, 2, 3]).to_string()); + assert_eq!( + "`1`, `2`, `3`, or `4`", + List::or_ticked(&[1, 2, 3, 4]).to_string() + ); + } + + #[test] + fn and_ticked() { + assert_eq!("`1`", List::and_ticked(&[1]).to_string()); + assert_eq!("`1` and `2`", List::and_ticked(&[1, 2]).to_string()); + assert_eq!( + "`1`, `2`, and `3`", + List::and_ticked(&[1, 2, 3]).to_string() + ); + assert_eq!( + "`1`, `2`, `3`, and `4`", + List::and_ticked(&[1, 2, 3, 4]).to_string() + ); + } +} diff --git a/src/misc.rs b/src/misc.rs deleted file mode 100644 index d9f388f7..00000000 --- a/src/misc.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::common::*; - -pub(crate) fn show_whitespace(text: &str) -> String { - text - .chars() - .map(|c| match c { - '\t' => '␉', - ' ' => '␠', - _ => c, - }) - .collect() -} - -pub(crate) fn default() -> T { - Default::default() -} - -pub(crate) fn empty>() -> C { - iter::empty().collect() -} - -pub(crate) fn ticks(ts: &[T]) -> Vec> { - ts.iter().map(Tick).collect() -} - -pub(crate) fn maybe_s(n: usize) -> &'static str { - if n == 1 { - "" - } else { - "s" - } -} - -pub(crate) fn conjoin( - f: &mut Formatter, - values: &[T], - conjunction: &str, -) -> Result<(), fmt::Error> { - match values.len() { - 0 => {} - 1 => write!(f, "{}", values[0])?, - 2 => write!(f, "{} {} {}", values[0], conjunction, values[1])?, - _ => { - for (i, item) in values.iter().enumerate() { - write!(f, "{}", item)?; - if i == values.len() - 1 { - } else if i == values.len() - 2 { - write!(f, ", {} ", conjunction)?; - } else { - write!(f, ", ")? - } - } - } - } - Ok(()) -} - -pub(crate) fn write_message_context( - f: &mut Formatter, - color: Color, - text: &str, - offset: usize, - line: usize, - column: usize, - width: usize, -) -> Result<(), fmt::Error> { - let width = if width == 0 { 1 } else { width }; - - let line_number = line.ordinal(); - match text.lines().nth(line) { - Some(line) => { - let mut i = 0; - let mut space_column = 0; - let mut space_line = String::new(); - let mut space_width = 0; - for c in line.chars() { - if c == '\t' { - space_line.push_str(" "); - if i < column { - space_column += 4; - } - if i >= column && i < column + width { - space_width += 4; - } - } else { - if i < column { - space_column += UnicodeWidthChar::width(c).unwrap_or(0); - } - if i >= column && i < column + width { - space_width += UnicodeWidthChar::width(c).unwrap_or(0); - } - space_line.push(c); - } - i += c.len_utf8(); - } - let line_number_width = line_number.to_string().len(); - writeln!(f, "{0:1$} |", "", line_number_width)?; - writeln!(f, "{} | {}", line_number, space_line)?; - write!(f, "{0:1$} |", "", line_number_width)?; - write!( - f, - " {0:1$}{2}{3:^<4$}{5}", - "", - space_column, - color.prefix(), - "", - space_width, - color.suffix() - )?; - } - None => { - if offset != text.len() { - write!( - f, - "internal error: Error has invalid line number: {}", - line_number - )? - } - } - } - Ok(()) -} - -pub(crate) struct And<'a, T: 'a + Display>(pub(crate) &'a [T]); - -impl<'a, T: Display> Display for And<'a, T> { - fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { - conjoin(f, self.0, "and") - } -} - -pub(crate) struct Or<'a, T: 'a + Display>(pub(crate) &'a [T]); - -impl<'a, T: Display> Display for Or<'a, T> { - fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { - conjoin(f, self.0, "or") - } -} - -pub(crate) struct Tick<'a, T: 'a + Display>(pub(crate) &'a T); - -impl<'a, T: Display> Display for Tick<'a, T> { - fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { - write!(f, "`{}`", self.0) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn conjoin_or() { - assert_eq!("1", Or(&[1]).to_string()); - assert_eq!("1 or 2", Or(&[1, 2]).to_string()); - assert_eq!("1, 2, or 3", Or(&[1, 2, 3]).to_string()); - assert_eq!("1, 2, 3, or 4", Or(&[1, 2, 3, 4]).to_string()); - } - - #[test] - fn conjoin_and() { - assert_eq!("1", And(&[1]).to_string()); - assert_eq!("1 and 2", And(&[1, 2]).to_string()); - assert_eq!("1, 2, and 3", And(&[1, 2, 3]).to_string()); - assert_eq!("1, 2, 3, and 4", And(&[1, 2, 3, 4]).to_string()); - } -} diff --git a/src/run.rs b/src/run.rs index 4cfaffef..9c303e55 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,6 +1,6 @@ use crate::common::*; -use crate::{interrupt_handler::InterruptHandler, misc::maybe_s}; +use crate::interrupt_handler::InterruptHandler; use unicode_width::UnicodeWidthStr; fn edit>(path: P) -> Result<(), i32> { @@ -285,10 +285,10 @@ pub fn run() -> Result<(), i32> { let min_arguments = recipe.min_arguments(); if min_arguments > 0 { die!( - "Recipe `{}` cannot be used as default recipe since it requires at least {} argument{}.", + "Recipe `{}` cannot be used as default recipe since it requires at least {} {}.", recipe.name, min_arguments, - maybe_s(min_arguments) + Count("argument", min_arguments), ); } vec![recipe.name] diff --git a/src/runtime_error.rs b/src/runtime_error.rs index 337900d6..7735b937 100644 --- a/src/runtime_error.rs +++ b/src/runtime_error.rs @@ -1,7 +1,5 @@ use crate::common::*; -use crate::misc::{maybe_s, ticks, write_message_context, And, Or, Tick}; - #[derive(Debug)] pub(crate) enum RuntimeError<'a> { ArgumentCountMismatch { @@ -102,9 +100,9 @@ impl<'a> Display for RuntimeError<'a> { } => { write!( f, - "Justfile does not contain recipe{} {}.", - maybe_s(recipes.len()), - Or(&ticks(recipes)) + "Justfile does not contain {} {}.", + Count("recipe", recipes.len()), + List::or_ticked(recipes), )?; if let Some(suggestion) = *suggestion { write!(f, "\nDid you mean `{}`?", suggestion)?; @@ -113,9 +111,9 @@ impl<'a> Display for RuntimeError<'a> { UnknownOverrides { ref overrides } => { write!( f, - "Variable{} {} overridden on the command line but not present in justfile", - maybe_s(overrides.len()), - And(&overrides.iter().map(Tick).collect::>()) + "{} {} overridden on the command line but not present in justfile", + Count("Variable", overrides.len()), + List::and_ticked(overrides), )?; } ArgumentCountMismatch { @@ -129,29 +127,29 @@ impl<'a> Display for RuntimeError<'a> { let expected = min; write!( f, - "Recipe `{}` got {} argument{} but {}takes {}", + "Recipe `{}` got {} {} but {}takes {}", recipe, found, - maybe_s(found), + Count("argument", found), if expected < found { "only " } else { "" }, expected )?; } else if found < min { write!( f, - "Recipe `{}` got {} argument{} but takes at least {}", + "Recipe `{}` got {} {} but takes at least {}", recipe, found, - maybe_s(found), + Count("argument", found), min )?; } else if found > max { write!( f, - "Recipe `{}` got {} argument{} but takes at most {}", + "Recipe `{}` got {} {} but takes at most {}", recipe, found, - maybe_s(found), + Count("argument", found), max )?; } diff --git a/src/search_error.rs b/src/search_error.rs index dd226ece..1373d160 100644 --- a/src/search_error.rs +++ b/src/search_error.rs @@ -1,6 +1,4 @@ -use std::{fmt, io, path::PathBuf}; - -use crate::misc::And; +use crate::common::*; pub(crate) enum SearchError { MultipleCandidates { @@ -29,11 +27,10 @@ impl fmt::Display for SearchError { f, "Multiple candidate justfiles found in `{}`: {}", candidates[0].parent().unwrap().display(), - And( - &candidates + List::and_ticked( + candidates .iter() - .map(|candidate| format!("`{}`", candidate.file_name().unwrap().to_string_lossy())) - .collect::>() + .map(|candidate| candidate.file_name().unwrap().to_string_lossy()) ), ), SearchError::NotFound => write!(f, "No justfile found"), diff --git a/src/show_whitespace.rs b/src/show_whitespace.rs new file mode 100644 index 00000000..dd24f07f --- /dev/null +++ b/src/show_whitespace.rs @@ -0,0 +1,18 @@ +use crate::common::*; + +/// String wrapper that uses nonblank characters to display spaces and tabs +pub struct ShowWhitespace<'str>(pub &'str str); + +impl<'str> Display for ShowWhitespace<'str> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + for c in self.0.chars() { + match c { + '\t' => write!(f, "␉")?, + ' ' => write!(f, "␠")?, + _ => write!(f, "{}", c)?, + }; + } + + Ok(()) + } +} diff --git a/src/warning.rs b/src/warning.rs index 32d8c385..38d3c52d 100644 --- a/src/warning.rs +++ b/src/warning.rs @@ -1,5 +1,4 @@ use crate::common::*; -use crate::misc::write_message_context; use Warning::*; diff --git a/src/write_message_context.rs b/src/write_message_context.rs new file mode 100644 index 00000000..99741057 --- /dev/null +++ b/src/write_message_context.rs @@ -0,0 +1,67 @@ +use crate::common::*; + +pub(crate) fn write_message_context( + f: &mut Formatter, + color: Color, + text: &str, + offset: usize, + line: usize, + column: usize, + width: usize, +) -> Result<(), fmt::Error> { + let width = if width == 0 { 1 } else { width }; + + let line_number = line.ordinal(); + match text.lines().nth(line) { + Some(line) => { + let mut i = 0; + let mut space_column = 0; + let mut space_line = String::new(); + let mut space_width = 0; + for c in line.chars() { + if c == '\t' { + space_line.push_str(" "); + if i < column { + space_column += 4; + } + if i >= column && i < column + width { + space_width += 4; + } + } else { + if i < column { + space_column += UnicodeWidthChar::width(c).unwrap_or(0); + } + if i >= column && i < column + width { + space_width += UnicodeWidthChar::width(c).unwrap_or(0); + } + space_line.push(c); + } + i += c.len_utf8(); + } + let line_number_width = line_number.to_string().len(); + writeln!(f, "{0:1$} |", "", line_number_width)?; + writeln!(f, "{} | {}", line_number, space_line)?; + write!(f, "{0:1$} |", "", line_number_width)?; + write!( + f, + " {0:1$}{2}{3:^<4$}{5}", + "", + space_column, + color.prefix(), + "", + space_width, + color.suffix() + )?; + } + None => { + if offset != text.len() { + write!( + f, + "internal error: Error has invalid line number: {}", + line_number + )? + } + } + } + Ok(()) +}