diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index cd11a475..575c6fbc 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -21,6 +21,18 @@ public class HelpText { private const int BuilderCapacity = 128; private const int DefaultMaximumLength = 80; // default console width + /// + /// The number of spaces between an option and its associated help text + /// + private const int OptionToHelpTextSeparatorWidth = 4; + /// + /// The width of the option prefix (either "--" or " " + /// + private const int OptionPrefixWidth = 2; + /// + /// The total amount of extra space that needs to accounted for when indenting Option help text + /// + private const int TotalOptionPadding = OptionToHelpTextSeparatorWidth + OptionPrefixWidth; private readonly StringBuilder preOptionsHelp; private readonly StringBuilder postOptionsHelp; private readonly SentenceBuilder sentenceBuilder; @@ -608,7 +620,7 @@ public static IEnumerable RenderUsageTextAsLines(ParserResult pars var styles = example.GetFormatStylesOrDefault(); foreach (var s in styles) { - var commandLine = new StringBuilder(2.Spaces()) + var commandLine = new StringBuilder(OptionPrefixWidth.Spaces()) .Append(appAlias) .Append(' ') .Append(Parser.Default.FormatCommandLine(example.Sample, @@ -665,37 +677,7 @@ internal static void AddLine(StringBuilder builder, string value, int maximumLen value = value.TrimEnd(); builder.AppendWhen(builder.Length > 0, Environment.NewLine); - do - { - var wordBuffer = 0; - var words = value.Split(' '); - for (var i = 0; i < words.Length; i++) - { - if (words[i].Length < (maximumLength - wordBuffer)) - { - builder.Append(words[i]); - wordBuffer += words[i].Length; - if ((maximumLength - wordBuffer) > 1 && i != words.Length - 1) - { - builder.Append(" "); - wordBuffer++; - } - } - else if (words[i].Length >= maximumLength && wordBuffer == 0) - { - builder.Append(words[i].Substring(0, maximumLength)); - wordBuffer = maximumLength; - break; - } - else - break; - } - value = value.Substring(Math.Min(wordBuffer, value.Length)); - builder.AppendWhen(value.Length > 0, Environment.NewLine); - } - while (value.Length > maximumLength); - - builder.Append(value); + builder.Append(TextWrapper.WrapAndIndentText(value, 0, maximumLength)); } private IEnumerable GetSpecificationsFromType(Type type) @@ -757,7 +739,7 @@ private HelpText AddOptionsImpl( optionsHelp = new StringBuilder(BuilderCapacity); - var remainingSpace = maximumLength - (maxLength + 6); + var remainingSpace = maximumLength - (maxLength + TotalOptionPadding); specifications.ForEach( option => @@ -809,7 +791,7 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe optionsHelp .Append(name.Length < maxLength ? name.ToString().PadRight(maxLength) : name.ToString()) - .Append(" "); + .Append(OptionToHelpTextSeparatorWidth.Spaces()); var optionHelpText = specification.HelpText; @@ -821,44 +803,13 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe if (specification.Required) optionHelpText = "{0} ".FormatInvariant(requiredWord) + optionHelpText; - - if (!string.IsNullOrEmpty(optionHelpText)) - { - do - { - var wordBuffer = 0; - var words = optionHelpText.Split(' '); - for (var i = 0; i < words.Length; i++) - { - if (words[i].Length < (widthOfHelpText - wordBuffer)) - { - optionsHelp.Append(words[i]); - wordBuffer += words[i].Length; - if ((widthOfHelpText - wordBuffer) > 1 && i != words.Length - 1) - { - optionsHelp.Append(" "); - wordBuffer++; - } - } - else if (words[i].Length >= widthOfHelpText && wordBuffer == 0) - { - optionsHelp.Append(words[i].Substring(0, widthOfHelpText)); - wordBuffer = widthOfHelpText; - break; - } - else - break; - } - - optionHelpText = optionHelpText.Substring(Math.Min(wordBuffer, optionHelpText.Length)).Trim(); - optionsHelp.AppendWhen(optionHelpText.Length > 0, Environment.NewLine, - new string(' ', maxLength + 6)); - } - while (optionHelpText.Length > widthOfHelpText); - } - + + //note that we need to indent trim the start of the string because it's going to be + //appended to an existing line that is as long as the indent-level + var indented = TextWrapper.WrapAndIndentText(optionHelpText, maxLength+TotalOptionPadding, widthOfHelpText).TrimStart(); + optionsHelp - .Append(optionHelpText) + .Append(indented) .Append(Environment.NewLine) .AppendWhen(additionalNewLineAfterOption, Environment.NewLine); @@ -944,13 +895,13 @@ private int GetMaxOptionLength(OptionSpecification spec) { specLength += spec.LongName.Length; if (AddDashesToOption) - specLength += 2; + specLength += OptionPrefixWidth; specLength += metaLength; } if (hasShort && hasLong) - specLength += 2; // ", " + specLength += OptionPrefixWidth; return specLength; } @@ -997,5 +948,10 @@ private static string FormatDefaultValue(T value) ? builder.ToString(0, builder.Length - 1) : string.Empty; } + + + } -} \ No newline at end of file +} + + diff --git a/src/CommandLine/Text/TextWrapper.cs b/src/CommandLine/Text/TextWrapper.cs new file mode 100644 index 00000000..19a93f15 --- /dev/null +++ b/src/CommandLine/Text/TextWrapper.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CommandLine.Infrastructure; + +namespace CommandLine.Text +{ + /// + /// A utility class to word-wrap and indent blocks of text + /// + public class TextWrapper + { + private string[] lines; + public TextWrapper(string input) + { + //start by splitting at newlines and then reinserting the newline as a separate word + //Note that on the input side, we can't assume the line-break style at run time so we have to + //be able to handle both. We can't use Environment.NewLine because that changes at + //_runtime_ and may not match the line-break style that was compiled in + lines = input + .Replace("\r","") + .Split(new[] {'\n'}, StringSplitOptions.None); + } + + /// + /// Splits a string into a words and performs wrapping while also preserving line-breaks and sub-indentation + /// + /// The number of characters we can use for text + /// + /// This method attempts to wrap text without breaking words + /// For example, if columnWidth is 10 , the input + /// "a string for wrapping 01234567890123" + /// would return + /// "a string + /// "for + /// "wrapping + /// "0123456789 + /// "0123" + /// + /// this + public TextWrapper WordWrap(int columnWidth) + { + //ensure we always use at least 1 column even if the client has told us there's no space available + columnWidth = Math.Max(1, columnWidth); + lines= lines + .SelectMany(line => WordWrapLine(line, columnWidth)) + .ToArray(); + return this; + } + + /// + /// Indent all lines in the TextWrapper by the desired number of spaces + /// + /// The number of spaces to indent by + /// this + public TextWrapper Indent(int numberOfSpaces) + { + lines = lines + .Select(line => numberOfSpaces.Spaces() + line) + .ToArray(); + return this; + } + + /// + /// Returns the current state of the TextWrapper as a string + /// + /// + public string ToText() + { + //return the whole thing as a single string + return string.Join(Environment.NewLine,lines); + } + + /// + /// Convenience method to wraps and indent a string in a single operation + /// + /// The string to operate on + /// The number of spaces to indent by + /// The width of the column used for wrapping + /// + /// The string is wrapped _then_ indented so the columnWidth is the width of the + /// usable text block, and does NOT include the indentLevel. + /// + /// the processed string + public static string WrapAndIndentText(string input, int indentLevel,int columnWidth) + { + return new TextWrapper(input) + .WordWrap(columnWidth) + .Indent(indentLevel) + .ToText(); + } + + + private string [] WordWrapLine(string line,int columnWidth) + { + //create a list of individual lines generated from the supplied line + + //When handling sub-indentation we must always reserve at least one column for text! + var unindentedLine = line.TrimStart(); + var currentIndentLevel = Math.Min(line.Length - unindentedLine.Length,columnWidth-1) ; + columnWidth -= currentIndentLevel; + + return unindentedLine.Split(' ') + .Aggregate( + new List(), + (lineList, word) => AddWordToLastLineOrCreateNewLineIfNecessary(lineList, word, columnWidth) + ) + .Select(builder => currentIndentLevel.Spaces()+builder.ToString().TrimEnd()) + .ToArray(); + } + + /// + /// When presented with a word, either append to the last line in the list or start a new line + /// + /// A list of StringBuilders containing results so far + /// The individual word to append + /// The usable text space + /// + /// The 'word' can actually be an empty string. It's important to keep these - + /// empty strings allow us to preserve indentation and extra spaces within a line. + /// + /// The same list as is passed in + private static List AddWordToLastLineOrCreateNewLineIfNecessary(List lines, string word,int columnWidth) + { + //The current indentation level is based on the previous line but we need to be careful + var previousLine = lines.LastOrDefault()?.ToString() ??string.Empty; + + var wouldWrap = !lines.Any() || (word.Length>0 && previousLine.Length + word.Length > columnWidth); + + if (!wouldWrap) + { + //The usual case is we just append the 'word' and a space to the current line + //Note that trailing spaces will get removed later when we turn the line list + //into a single string + lines.Last().Append(word + ' '); + } + else + { + //The 'while' here is to take account of the possibility of someone providing a word + //which just can't fit in the current column. In that case we just split it at the + //column end. + //That's a rare case though - most of the time we'll succeed in a single pass without + //having to split + //Note that we always do at least one pass even if the 'word' is empty in order to + //honour sub-indentation and extra spaces within strings + do + { + var availableCharacters = Math.Min(columnWidth, word.Length); + var segmentToAdd = LeftString(word,availableCharacters) + ' '; + lines.Add(new StringBuilder(segmentToAdd)); + word = RightString(word,availableCharacters); + } while (word.Length > 0); + } + return lines; + } + + + /// + /// Return the right part of a string in a way that compensates for Substring's deficiencies + /// + private static string RightString(string str,int n) + { + return (n >= str.Length || str.Length==0) + ? string.Empty + : str.Substring(n); + } + /// + /// Return the left part of a string in a way that compensates for Substring's deficiencies + /// + private static string LeftString(string str,int n) + { + + return (n >= str.Length || str.Length==0) + ? str + : str.Substring(0,n); + } + } +} diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs new file mode 100644 index 00000000..afa77f3a --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs @@ -0,0 +1,13 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithLineBreaksAndSubIndentation_Options + { + + [Option(HelpText = @"This is a help text description where we want: + * The left pad after a linebreak to be honoured and the indentation to be preserved across to the next line + * The ability to return to no indent. +Like this.")] + public string StringValue { get; set; } + + } +} \ No newline at end of file diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs new file mode 100644 index 00000000..c93e73aa --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs @@ -0,0 +1,23 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithLineBreaks_Options + { + [Option(HelpText = + @"This is a help text description. +It has multiple lines. +We also want to ensure that indentation is correct.")] + public string StringValue { get; set; } + + + [Option(HelpText = @"This is a help text description where we want + The left pad after a linebreak to be honoured so that + we can sub-indent within a description.")] + public string StringValu2 { get; set; } + + + [Option(HelpText = @"This is a help text description where we want + The left pad after a linebreak to be honoured and the indentation to be preserved across to the next line in a way that looks pleasing")] + public string StringValu3 { get; set; } + + } +} diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs new file mode 100644 index 00000000..9950dbc7 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs @@ -0,0 +1,9 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithMixedLineBreaks_Options + { + [Option(HelpText = + "This is a help text description\n It has multiple lines.\r\n Third line")] + public string StringValue { get; set; } + } +} diff --git a/tests/CommandLine.Tests/StringExtensions.cs b/tests/CommandLine.Tests/StringExtensions.cs index e3830b0c..1ea18538 100644 --- a/tests/CommandLine.Tests/StringExtensions.cs +++ b/tests/CommandLine.Tests/StringExtensions.cs @@ -13,6 +13,11 @@ public static string[] ToNotEmptyLines(this string value) return value.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); } + public static string[] ToLines(this string value) + { + return value.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + } + public static string[] TrimStringArray(this IEnumerable array) { return array.Select(item => item.Trim()).ToArray(); diff --git a/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs new file mode 100644 index 00000000..1db7497e --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs @@ -0,0 +1,197 @@ +using System; +using System.Linq; +using CommandLine.Text; +using FluentAssertions; +using Xunit; + +namespace CommandLine.Tests.Unit.Core +{ + public class TextWrapperTests + { + private string NormalizeLineBreaks(string str) + { + return str.Replace("\r", ""); + } + + private void EnsureEquivalent(string a, string b) + { + //workaround build system line-end inconsistencies + NormalizeLineBreaks(a).Should().Be(NormalizeLineBreaks(b)); + } + + + [Fact] + public void ExtraSpacesAreTreatedAsNonBreaking() + { + var input = + "here is some text with some extra spacing"; + var expected = @"here is some text +with some extra +spacing"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + + [Fact] + public void IndentWorksCorrectly() + { + var input = + @"line1 +line2"; + var expected = @" line1 + line2"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.Indent(2).ToText(), expected); + } + + [Fact] + public void LongWordsAreBroken() + { + var input = + "here is some text that contains a veryLongWordThatWontFitOnASingleLine"; + var expected = @"here is some text +that contains a +veryLongWordThatWont +FitOnASingleLine"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void NegativeColumnWidthStillProducesOutput() + { + var input = @"test"; + var expected = string.Join(Environment.NewLine, input.Select(c => c.ToString())); + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(-1).ToText(), expected); + } + + [Fact] + public void SimpleWrappingIsAsExpected() + { + var input = + @"here is some text that needs wrapping"; + var expected = @"here is +some text +that needs +wrapping"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(10).ToText(), expected); + } + + [Fact] + public void SingleColumnStillProducesOutputForSubIndentation() + { + var input = @"test + ind"; + + var expected = @"t +e +s +t +i +n +d"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(-1).ToText(), expected); + } + + [Fact] + public void SpacesWithinStringAreRespected() + { + var input = + "here is some text with some extra spacing"; + var expected = @"here is some +text with some extra +spacing"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void SubIndentationCorrectlyWrapsWhenColumnWidthRequiresIt() + { + var input = @"test + indented"; + var expected = @"test + in + de + nt + ed"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(6).ToText(), expected); + } + + [Fact] + public void SubIndentationIsPreservedWhenBreakingWords() + { + var input = + "here is some text that contains \n a veryLongWordThatWontFitOnASingleLine"; + var expected = @"here is some text +that contains + a + veryLongWordThatWo + ntFitOnASingleLine"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void WrappingAvoidsBreakingWords() + { + var input = + @"here hippopotamus is some text that needs wrapping"; + var expected = @"here +hippopotamus is +some text that +needs wrapping"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(15).ToText(), expected); + } + + + [Fact] + public void WrappingExtraSpacesObeySubIndent() + { + var input = + "here is some\n text with some extra spacing"; + var expected = @"here is some + text + with some extra + spacing"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void WrappingObeysLineBreaksOfAllStyles() + { + var input = + "here is some text\nthat needs\r\nwrapping"; + var expected = @"here is some text +that needs +wrapping"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + + [Fact] + public void WrappingPreservesSubIndentation() + { + var input = + "here is some text\n that needs wrapping where we want the wrapped part to preserve indentation\nand this part to not be indented"; + var expected = @"here is some text + that needs + wrapping where we + want the wrapped + part to preserve + indentation +and this part to not +be indented"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/ParserTests.cs b/tests/CommandLine.Tests/Unit/ParserTests.cs index 9d6e56fb..23cf0f32 100644 --- a/tests/CommandLine.Tests/Unit/ParserTests.cs +++ b/tests/CommandLine.Tests/Unit/ParserTests.cs @@ -803,5 +803,25 @@ public void Parse_options_with_shuffled_index_values() Assert.Equal("two", args.Arg2); }); } + + + [Fact] + public void Blank_lines_are_inserted_between_verbs() + { + // Fixture setup + var help = new StringWriter(); + var sut = new Parser(config => config.HelpWriter = help); + + // Exercize system + sut.ParseArguments(new string[] { }); + var result = help.ToString(); + + // Verify outcome + var lines = result.ToLines().TrimStringArray(); + lines[6].Should().BeEquivalentTo("add Add file contents to the index."); + lines[8].Should().BeEquivalentTo("help Display more information on a specific command."); + lines[10].Should().BeEquivalentTo("version Display version information."); + // Teardown + } } } diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index bd2148a2..1dd8d45f 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -154,14 +154,12 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); lines[2].Should().BeEquivalentTo(" v, verbose This is the description"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; - lines[3].Should().BeEquivalentTo(" of the verbosity to "); - lines[4].Should().BeEquivalentTo(" test out the wrapping "); - lines[5].Should().BeEquivalentTo(" capabilities of the "); - lines[6].Should().BeEquivalentTo(" Help Text."); + lines[3].Should().BeEquivalentTo(" of the verbosity to test"); + lines[4].Should().BeEquivalentTo(" out the wrapping"); + lines[5].Should().BeEquivalentTo(" capabilities of the Help"); + lines[6].Should().BeEquivalentTo(" Text."); // Teardown } - - [Fact] public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_column_given_width_of_100() @@ -176,7 +174,7 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c // Verify outcome var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); - lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the wrapping capabilities of "); //"The first line should have the arguments and the start of the Help Text."); + lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the wrapping capabilities of"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; lines[3].Should().BeEquivalentTo(" the Help Text."); // Teardown @@ -196,7 +194,7 @@ public void When_help_text_has_hidden_option_it_should_not_be_added_to_help_text // Verify outcome var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); - lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the "); //"The first line should have the arguments and the start of the Help Text."); + lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; lines[3].Should().BeEquivalentTo(" wrapping capabilities of the Help Text."); // Teardown @@ -216,10 +214,10 @@ public void Long_help_text_without_spaces() // Verify outcome var lines = sut.ToString().ToNotEmptyLines(); - lines[1].Should().BeEquivalentTo(" v, verbose Before "); + lines[1].Should().BeEquivalentTo(" v, verbose Before"); lines[2].Should().BeEquivalentTo(" 012345678901234567890123"); lines[3].Should().BeEquivalentTo(" After"); - lines[4].Should().BeEquivalentTo(" input-file Before "); + lines[4].Should().BeEquivalentTo(" input-file Before"); lines[5].Should().BeEquivalentTo(" 012345678901234567890123"); lines[6].Should().BeEquivalentTo(" 456789 After"); // Teardown @@ -238,12 +236,12 @@ public void Long_pre_and_post_lines_without_spaces() // Verify outcome var lines = sut.ToString().ToNotEmptyLines(); - lines[1].Should().BeEquivalentTo("Before "); + lines[1].Should().BeEquivalentTo("Before"); lines[2].Should().BeEquivalentTo("0123456789012345678901234567890123456789"); lines[3].Should().BeEquivalentTo("012 After"); - lines[lines.Length - 3].Should().BeEquivalentTo("Before "); + lines[lines.Length - 3].Should().BeEquivalentTo("Before"); lines[lines.Length - 2].Should().BeEquivalentTo("0123456789012345678901234567890123456789"); - lines[lines.Length - 1].Should().BeEquivalentTo(" After"); + lines[lines.Length - 1].Should().BeEquivalentTo("After"); // Teardown } @@ -296,7 +294,6 @@ public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text( // Verify outcome var lines = errorsText.ToNotEmptyLines(); - lines[0].Should().BeEquivalentTo(" ERR badtoken"); lines[1].Should().BeEquivalentTo(" ERR x, switch"); lines[2].Should().BeEquivalentTo(" ERR unknown"); @@ -324,7 +321,6 @@ public void Invoke_AutoBuild_for_Options_returns_appropriate_formatted_text() // Verify outcome var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); - lines[0].Should().Be(HeadingInfo.Default.ToString()); lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("ERROR(S):"); @@ -381,7 +377,6 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo // Verify outcome var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); - lines[0].Should().Be(HeadingInfo.Default.ToString()); lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which changes to commit."); @@ -517,7 +512,6 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte // Teardown } -#if !PLATFORM_DOTNET [Fact] public void Default_set_to_sequence_should_be_properly_printed() { @@ -543,7 +537,6 @@ public void Default_set_to_sequence_should_be_properly_printed() // Teardown } -#endif [Fact] public void AutoBuild_when_no_assembly_attributes() @@ -590,7 +583,6 @@ public void AutoBuild_with_assembly_title_and_version_attributes_only() actualResult.Heading.Should().Be(string.Format("{0} {1}", expectedTitle, expectedVersion)); } - [Fact] public void AutoBuild_with_assembly_company_attribute_only() { @@ -624,6 +616,128 @@ public void Add_line_with_two_empty_spaces_at_the_end() Assert.Equal("T" + Environment.NewLine + "e" + Environment.NewLine + "s" + Environment.NewLine + "t", b.ToString()); } - + + [Fact] + public void HelpTextHonoursLineBreaks() + { + // Fixture setup + // Exercize system + var sut = new HelpText {AddDashesToOption = true} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description."); + lines[1].Should().BeEquivalentTo(" It has multiple lines."); + lines[2].Should().BeEquivalentTo(" We also want to ensure that indentation is correct."); + + // Teardown + } + + [Fact] + public void HelpTextHonoursIndentationAfterLineBreaks() + { + // Fixture setup + // Exercize system + var sut = new HelpText {AddDashesToOption = true} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[3].Should().BeEquivalentTo(" --stringvalu2 This is a help text description where we want"); + lines[4].Should().BeEquivalentTo(" the left pad after a linebreak to be honoured so that"); + lines[5].Should().BeEquivalentTo(" we can sub-indent within a description."); + + // Teardown + } + + [Fact] + public void HelpTextPreservesIndentationAcrossWordWrap() + { + // Fixture setup + // Exercise system + var sut = new HelpText {AddDashesToOption = true,MaximumDisplayWidth = 60} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaksAndSubIndentation_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description where we"); + lines[1].Should().BeEquivalentTo(" want:"); + lines[2].Should().BeEquivalentTo(" * The left pad after a linebreak to"); + lines[3].Should().BeEquivalentTo(" be honoured and the indentation to be"); + lines[4].Should().BeEquivalentTo(" preserved across to the next line"); + lines[5].Should().BeEquivalentTo(" * The ability to return to no indent."); + lines[6].Should().BeEquivalentTo(" Like this."); + + // Teardown + } + + [Fact] + public void HelpTextIsConsitentRegardlessOfCompileTimeLineStyle() + { + // Fixture setup + // Exercize system + var sut = new HelpText {AddDashesToOption = true} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithMixedLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description"); + lines[1].Should().BeEquivalentTo(" It has multiple lines."); + lines[2].Should().BeEquivalentTo(" Third line"); + + // Teardown + } + + [Fact] + public void HelpTextPreservesIndentationAcrossWordWrapWithSmallMaximumDisplayWidth() + { + // Fixture setup + // Exercise system + var sut = new HelpText {AddDashesToOption = true,MaximumDisplayWidth = 10} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaksAndSubIndentation_Options)), + Enumerable.Empty())); + + // Verify outcome + + Assert.True(sut.ToString().Length>0); + + // Teardown + } + + [Fact] + public void Options_should_be_separated_by_spaces() + { + // Fixture setup + var handlers = new CultureInfo("en-US").MakeCultureHandlers(); + var fakeResult = + new NotParsed( + typeof(Options_With_Default_Set_To_Sequence).ToTypeInfo(), + Enumerable.Empty() + ); + + // Exercize system + handlers.ChangeCulture(); + var helpText = HelpText.AutoBuild(fakeResult); + handlers.ResetCulture(); + + // Verify outcome + var text = helpText.ToString(); + var lines = text.ToLines().TrimStringArray(); + Console.WriteLine(text); + lines[3].Should().Be("-z, --strseq (Default: a b c)"); + lines[5].Should().Be("-y, --intseq (Default: 1 2 3)"); + lines[7].Should().Be("-q, --dblseq (Default: 1.1 2.2 3.3)"); + + // Teardown + } } }