-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework Help.wrap()
#1904
Rework Help.wrap()
#1904
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
There is a pretty epic amount of information here. Thanks for the detail on the changes. A word of warning. If your end goal is an accepted Pull Request, I suggest you ask some questions before launching into major changes and rewrites, to avoid disappointment. However, if your end-goal is sharing some interesting work you did that might be accepted or used in some way, that is fine. I look forward to looking through it! I suspect it might be beyond what I want to pull into Commander, but is still likely to have some interesting ideas that we might want to use in smaller changes. The existing For interesting related reading, @cravler has made lots of suggestions and good feedback for Commander, driven by making it easier to do custom (and fancy) help: https://github.com/tj/commander.js/pulls?q=is%3Apr+involves%3Acravler |
typings/index.d.ts
Outdated
*/ | ||
wrap(str: string, width: number, indent: number, minColumnWidth?: number): string; | ||
wrap(str: string, width: number, leadingStrLength?: number, minWidthGuideline?: number, overflowShift?: number, globalIndent?: number, columnGap?: number, preformatted?: boolean): string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a nicer of handling multiple optional parameters is to pass them in an options "bag". This both allows specifying any one of the optional values (instead of all the values up to the one desired), and provides some self-documentation in the call.
e.g.
help.wrap(fullTest, 23, { minWidthGuideline: 20 })
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, all the indents simply listed one after the other can be quite confusing, so I agree with you, except maybe preformatted
should be additionally available as a separate argument, just like minWidthGuideline
is now. Both are special because they are in essence universal settings that are supposed to have the same value in each call in any reasonable application, so ideally, they should only be used directly when overwriting wrap()
in a subclass. Because of that similarity and since minWidthGuideline
is already available as a separate argument, I think preformatted
should be, too. We could also make them both properties of the Help
class instead, so that the values could be configured with Command.configureHelp()
.
Anyway, I got stuck trying to write JSDocs for the overloads with an options
parameter. Apparently, VS Code does not understand overloaded functions well, so it led me to create this issue: https://github.com/microsoft/vscode/issues/187663.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My goal is both, actually. I would not be spending so much time on it if it was not fulfilling on its own, but of course it would be nice to see the PR or at least some of the changes accepted into the project :) This time, I really did not know where it was going. I started with just a couple small experiments for myself that were not supposed to become anything big, and then just kept adding new stuff because something always felt missing, and I kept doing that until it got to this point where the method looks like it's got all it needs. I guess I just got inspired and decided to keep going instead of asking questions. This PR is totally not helping to make I see now that I put too much emphasis on the word "table" when naming variables and writing JSDoc and this PR. It makes the suggested changes seem to be a bigger deal than they actually are, so I have now tried to get rid of as many table references as possible. The fact is that the table functionality was just a very natural extension of code I ended up with, so I could not reject the idea of adding those 11 extra lines. It is definitely one of the easiest to implement and one of the least important new features. so I will not be disappointed at all if it is not accepted. Can't say that about the changes that do not introduce any new features, though. All changes I have now marked with [2] and [3] in the PR description seem very reasonable to me and I really cannot think of a reason why they should be rejected, except maybe the suggested regex structure is debatable. The new feature marked with [4] can be summarized in the following four classes:
|
Oops, I accidentally posted too early again, but I was almost finished anyway. I put this feature class list together to reiterate that the changes I am suggesting are not actually such a big deal, even if the full list looks a bit scary. As for their importance, I think there really needs to be a way to control the pre-formatting assumption, at least by means of subclassing, so I would rank |
The basic idea of preserving the pre-formatted text was to preserve text that author carefully formatted themselves, in particular before wrapping was introduced. Terminal width 80.
and with pre-formatted parameter in PR set to
Example code used: const program = require("commander");
program
.option("--pre-formatted <value>", `long description follows which
Commander guesses is pre-formatted, perhaps to make use of full
terminal width to fit on the target display nicely`)
.option("-w, --wrap", `long description follows:
1) Commander wraps but preserves the line breaks, which may create some short lines
2) but does at least start the lines where desired`)
program.parse(process.argv); |
I think the most likely response to displaying the help in a small terminal window is going to be drag the window bigger. This is partly why the formatting is abandoned if too narrow. If the text is fully wrapped in a narrow window, it will need to be displayed again when window is made more sensible size. I had a look at what man does, and it has an interesting related but different fallback. It wraps the text to the smallest supported width. That way when when you drag the window bigger, the text shows up as nicely wrapped when get to or past that minimum width. I think that is better than unformatted for the window-resize usage case. |
I don't think it is common to type commands in a narrow terminal window and find out it is too narrow only after the output has been displayed, so the real use for
Do you think this is what we should do if even the full width does not reach width = Math.max(width, minWidthGuideline); But then the default value for |
I stumbled across an error when the window is narrower than the first column:
|
Overall I feel this is a lot of new code with a lot more knobs and dials available to subclassing. There are a variety of ideas and controls for formatting. But there have not been only a few issues opened about wrapping, and as a fallback people who want custom behaviour can subclass and do what they want themselves. I'm happy to leave the PR open for a month and think about it some more, and see if any feedback or related issues come up, but currently leaning towards no. (It has only been open for a couple of days, but I didn't want you to put in further work under the expectation it would be accepted.) I appreciate that you thought about and commented on what changes would be breaking. |
A musing from some research tonight.
|
It turned out to be really easily solvable by a recursive const term = '--extraordinarily-long-option-name <and-an-unexpectedly-big-option-argument-name> ';
const description = 'DESCRIPTION that is also quite big so it overflows';
console.log(new Help().wrap(
term + description,
process.stdout.columns,
term.length,
0,
{ globalIndent: 4, columnGap: 4 }
)); With console output being
with the new code, and
with the old when the width is 25. |
I just want to point out one more time that the changes marked with [2] and [3] are just reasonable defaults, with ones marked with [3] currently requiring rewriting the entire method when subclassing, which no one would want to do. Same applies to rewriting the entire method just to get rid of the manual formatting assumptions.
Okay, perfect, I will not hurry with adding tests then. And I would be happy to hear feedback from other people during the next month. We cannot be the only ones reading all of this :)
And I appreciate you investing your time in reviewing the changes, even if the PR does not end up accepted. |
Closing after a month as per: #1904 (comment) This sort of flexibility might go into a library on its own, which for interest is an approach Yargs has taken: https://github.com/yargs/cliui |
Pull Request
Problem
I first thought of reworking
wrap()
when I wanted to wrap lines of text in a custom help message in such a way that only the first output line is indented. (At that moment, I did not think of manually indenting the lines before passing them to the function for some reason.)I also did not like the fact text was not wrapped whenever the decision was made to not indent it due to insufficient width – I see no reason why the all-or-nothing principle had to be applied here.
Not being able to apply wrapping just because some lines are indented manually by even just a small amount also seemed pretty lame.
Solution
An attempt to make just one change to solve my problem ended up in a (more or less) complete rework of
wrap()
. The new function can be used to format entire tables, while remaining for the most part compatible with the current implementation.(Updated) new method signature
with my made up "skippable" argument syntax to fit everything in one signature.
Example
Despite table support and many other changes not being essential for internal use in Commander, they make
wrap()
a quite powerful library function that can be used to format custom help messages and more, while at the same time preserving the original functionality and not introducing any performance penalties (although I haven't measured it, getting rid of lazy quantifiers in regular expressions might have even improved it).Footnotes used in change descriptions
Changes include:
width
can now be infinite and is considered such ifundefined
is passed (could easily happen informatHelp()
if it did not resort to 80)indent
has been renamed toleadingStrLength
to better reflect the argument's true meaning and avoid confusion with other values affecting indentationleadingStrLength
now defaults to 0leadingStr.split('\n').length
lines the same way only the very first line is treated in the current implementation (whereleadingStr = str.slice(0, leadingStrLength)
). Indentation amount is equal to the length of the longest line inleadingStr
(leadingStrWidth
), which degenerates toleadingStrLength
(or currentlyindent
) if there is only one lineoverflowShift
can be specified to shift overflowing column text by some amount. Negative values are allowed, and therefore it can be used to solve my original problem of indenting only the first line, see details belowminColumnWidth
has been renamed tominWidthGuideline
, again, to better reflect its meaning:Column
omitted because only overflowing text width is compared to this value, and it is not entirely clear whether the overflow can be considered part of the column when it is shiftedGuideline
added because even when no indentation is used, the width could still be insufficient, and we would not be able to do anything about itOverflow
not added instead ofColumn
because the fact we use it just for overflow does not mean the value of this guideline is bound to this context. (For example, it could be used to wrap entire text columns to next "column line" when the remaining width for next column rendering is insufficient – check CSSflex-wrap
property to see what I mean)globalIndent
can be specified to indent all non-empty lines by some amount, except when there is not enough space to display overflowing lines adequately, in which case no indentation to such lines is applied. As a result,formatHelp()
does not have to postprocess the lines by prepending 2 spaces anymore, and the actual terminal width can be passed towrap()
without modfications. But the fact only non-empty lines are indented has the following breaking change as a consequence:formatHelp()
now relies onwrap()
for applying indentation, empty lines in pre-formatted argument/option/command descriptions are not indented by 2 spaces anymore (test updated)columnGap
can be specified to adjust distance between text column defined byleadingStr
and one defined by the remaining part ofstr
(in the traditional use case, it would be the distance between term and description). Again,formatHelp()
can now simply pass the item separator towrap()
instead of having to deal with it by itselfpreformatted
can be specified to request treatment of text as (not) manually formatted (opens possibilities to extend API that could solve Wrapping the second and following line in a multiline description doesn't work if the line starts from space #1822)leadingStrLength
, since it is exclusively used in calls to strings'slice()
method which does not fail on negativesglobalIndent + leadingStrWidth + columnGap + overflowShift
is negative, the non-overflowing output part will be indented by the absolute value of this number in addition toglobalIndent
. This is required to make room for a negativeoverflowShift
, so this is the solution to the original line indentation problem. For example, let's sayleadingStrLength = 0
,overflowShift = -10
,globalIndent = 3
andcolumnGap = 0
, then the first line will be indenten by-(3 - 10) = 7
additional spaces so that it would appear that all other lines are shifted by -10g
flag dropped,y
added. Some breaking changes, too:width - 1
non-breaking characters. As a result, overflowing words are now wrapped (tests added)wrap('a b', 2) === 'a\nb'
(test added)overflowShift
is negative, the algorithm will try to place it in overflow instead of splittingAdditional infos
formatHelp()
code did not actually have to be altered and would work the same as before (and the first mentioned breaking change would not be there), but I decided to letwrap()
do the job of inserting the "item indent" and "item separator" becausewidth
argument value is the entire available width which is niceTo-dos
preformatted
property toHelp
class insteadQuestions
width-1
is replaced withwidth
in the regular expressions? If not, I think it would make sense to do the change so that the function behavior is more intuitive.Footnotes
Cosmetic change ↩ ↩2 ↩3
Minor functionality change ↩ ↩2 ↩3 ↩4
(Relating to a) breaking change ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
(Relating to a) new feature – all backwards compatible ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11