11import {
22 formatMessage ,
33 type Message ,
4+ type MessageFormatOptions ,
45 type MessageTerm ,
56 text ,
67} from "./message.ts" ;
@@ -349,14 +350,16 @@ export function formatDocPage(
349350 : options . maxWidth - termIndent ,
350351 } ) ;
351352
353+ const descColumnWidth = options . maxWidth == null
354+ ? undefined
355+ : options . maxWidth - termIndent - termWidth - 2 ;
356+
352357 let description = entry . description == null
353358 ? ""
354359 : formatMessage ( entry . description , {
355360 colors : options . colors ,
356361 quotes : ! options . colors ,
357- maxWidth : options . maxWidth == null
358- ? undefined
359- : options . maxWidth - termIndent - termWidth - 2 ,
362+ maxWidth : descColumnWidth ,
360363 } ) ;
361364
362365 // Append default value if showDefault is enabled and default exists
@@ -367,12 +370,39 @@ export function formatDocPage(
367370 const suffix = typeof options . showDefault === "object"
368371 ? options . showDefault . suffix ?? "]"
369372 : "]" ;
370- const defaultText = `${ prefix } ${
371- formatMessage ( entry . default , {
372- colors : options . colors ? { resetSuffix : "\x1b[2m" } : false ,
373- quotes : ! options . colors ,
374- } )
375- } ${ suffix } `;
373+
374+ // Determine startWidth so that word-wrapping in the default value
375+ // continues correctly from the current line position.
376+ let defaultStartWidth : number | undefined ;
377+ if ( descColumnWidth != null ) {
378+ const lastW = lastLineVisibleLength ( description ) ;
379+ if ( lastW + prefix . length >= descColumnWidth ) {
380+ description += "\n" ;
381+ defaultStartWidth = prefix . length ;
382+ } else {
383+ defaultStartWidth = lastW + prefix . length ;
384+ }
385+ }
386+
387+ // `startWidth` is accepted by the formatMessage() implementation but
388+ // is absent from the public MessageFormatOptions type. The inline
389+ // intersection type makes TypeScript accept the field here while
390+ // keeping it out of the public API. Because the intersection type is
391+ // a subtype of MessageFormatOptions, the call below remains
392+ // type-safe.
393+ const defaultFormatOptions : MessageFormatOptions & {
394+ readonly startWidth ?: number ;
395+ } = {
396+ colors : options . colors ? { resetSuffix : "\x1b[2m" } : false ,
397+ quotes : ! options . colors ,
398+ maxWidth : descColumnWidth ,
399+ startWidth : defaultStartWidth ,
400+ } ;
401+ const defaultContent = formatMessage (
402+ entry . default ,
403+ defaultFormatOptions ,
404+ ) ;
405+ const defaultText = `${ prefix } ${ defaultContent } ${ suffix } ` ;
376406 const formattedDefault = options . colors
377407 ? `\x1b[2m${ defaultText } \x1b[0m`
378408 : defaultText ;
@@ -418,10 +448,35 @@ export function formatDocPage(
418448 ] ;
419449 }
420450 }
421- const choicesDisplay = formatMessage ( truncatedTerms , {
451+ // Determine startWidth so that word-wrapping in the choices list
452+ // continues correctly from the current line position.
453+ let choicesStartWidth : number | undefined ;
454+ if ( descColumnWidth != null ) {
455+ const lastW = lastLineVisibleLength ( description ) ;
456+ const prefixLabelLen = prefix . length + label . length ;
457+ if ( lastW + prefixLabelLen >= descColumnWidth ) {
458+ description += "\n" ;
459+ choicesStartWidth = prefixLabelLen ;
460+ } else {
461+ choicesStartWidth = lastW + prefixLabelLen ;
462+ }
463+ }
464+
465+ // See the comment above the defaultFormatOptions variable for why
466+ // startWidth is passed via a typed variable rather than an inline
467+ // object literal.
468+ const choicesFormatOptions : MessageFormatOptions & {
469+ readonly startWidth ?: number ;
470+ } = {
422471 colors : options . colors ? { resetSuffix : "\x1b[2m" } : false ,
423472 quotes : false ,
424- } ) ;
473+ maxWidth : descColumnWidth ,
474+ startWidth : choicesStartWidth ,
475+ } ;
476+ const choicesDisplay = formatMessage (
477+ truncatedTerms ,
478+ choicesFormatOptions ,
479+ ) ;
425480 const choicesText = `${ prefix } ${ label } ${ choicesDisplay } ${ suffix } ` ;
426481 const formattedChoices = options . colors
427482 ? `\x1b[2m${ choicesText } \x1b[0m`
@@ -494,16 +549,23 @@ function indentLines(text: string, indent: number): string {
494549 return text . split ( "\n" ) . join ( "\n" + " " . repeat ( indent ) ) ;
495550}
496551
552+ // deno-lint-ignore no-control-regex
553+ const ansiEscapeCodeRegex = / \x1B \[ [ 0 - 9 ; ] * [ a - z A - Z ] / g;
554+
497555function ansiAwareRightPad (
498556 text : string ,
499557 length : number ,
500558 char : string = " " ,
501559) : string {
502- // deno-lint-ignore no-control-regex
503- const ansiEscapeCodeRegex = / \x1B \[ [ 0 - 9 ; ] * [ a - z A - Z ] / g;
504560 const strippedText = text . replace ( ansiEscapeCodeRegex , "" ) ;
505561 if ( strippedText . length >= length ) {
506562 return text ;
507563 }
508564 return text + char . repeat ( length - strippedText . length ) ;
509565}
566+
567+ function lastLineVisibleLength ( text : string ) : number {
568+ const lastNewline = text . lastIndexOf ( "\n" ) ;
569+ const lastLine = lastNewline === - 1 ? text : text . slice ( lastNewline + 1 ) ;
570+ return lastLine . replace ( ansiEscapeCodeRegex , "" ) . length ;
571+ }
0 commit comments