11import type { Writable } from 'node:stream' ;
22import { WriteStream } from 'node:tty' ;
3+ import { getColumns } from '@clack/core' ;
4+ import { wrapAnsi } from 'fast-wrap-ansi' ;
35import color from 'picocolors' ;
46import type { CommonOptions } from './common.js' ;
57
@@ -8,37 +10,129 @@ export interface LimitOptionsParams<TOption> extends CommonOptions {
810 maxItems : number | undefined ;
911 cursor : number ;
1012 style : ( option : TOption , active : boolean ) => string ;
13+ columnPadding ?: number ;
14+ rowPadding ?: number ;
1115}
1216
17+ const trimLines = (
18+ groups : Array < string [ ] > ,
19+ initialLineCount : number ,
20+ startIndex : number ,
21+ endIndex : number ,
22+ maxLines : number
23+ ) => {
24+ let lineCount = initialLineCount ;
25+ let removals = 0 ;
26+ for ( let i = startIndex ; i < endIndex ; i ++ ) {
27+ const group = groups [ i ] ;
28+ lineCount = lineCount - group . length ;
29+ removals ++ ;
30+ if ( lineCount <= maxLines ) {
31+ break ;
32+ }
33+ }
34+ return { lineCount, removals } ;
35+ } ;
36+
1337export const limitOptions = < TOption > ( params : LimitOptionsParams < TOption > ) : string [ ] => {
1438 const { cursor, options, style } = params ;
1539 const output : Writable = params . output ?? process . stdout ;
16- const rows = output instanceof WriteStream && output . rows !== undefined ? output . rows : 10 ;
40+ const columns = getColumns ( output ) ;
41+ const columnPadding = params . columnPadding ?? 0 ;
42+ const rowPadding = params . rowPadding ?? 4 ;
43+ const maxWidth = columns - columnPadding ;
44+ const rows = output instanceof WriteStream && output . rows !== undefined ? output . rows : 20 ;
1745 const overflowFormat = color . dim ( '...' ) ;
1846
1947 const paramMaxItems = params . maxItems ?? Number . POSITIVE_INFINITY ;
20- const outputMaxItems = Math . max ( rows - 4 , 0 ) ;
48+ const outputMaxItems = Math . max ( rows - rowPadding , 0 ) ;
2149 // We clamp to minimum 5 because anything less doesn't make sense UX wise
2250 const maxItems = Math . min ( outputMaxItems , Math . max ( paramMaxItems , 5 ) ) ;
2351 let slidingWindowLocation = 0 ;
2452
25- if ( cursor >= slidingWindowLocation + maxItems - 3 ) {
53+ if ( cursor >= maxItems - 3 ) {
2654 slidingWindowLocation = Math . max ( Math . min ( cursor - maxItems + 3 , options . length - maxItems ) , 0 ) ;
27- } else if ( cursor < slidingWindowLocation + 2 ) {
28- slidingWindowLocation = Math . max ( cursor - 2 , 0 ) ;
2955 }
3056
31- const shouldRenderTopEllipsis = maxItems < options . length && slidingWindowLocation > 0 ;
32- const shouldRenderBottomEllipsis =
57+ let shouldRenderTopEllipsis = maxItems < options . length && slidingWindowLocation > 0 ;
58+ let shouldRenderBottomEllipsis =
3359 maxItems < options . length && slidingWindowLocation + maxItems < options . length ;
3460
35- return options
36- . slice ( slidingWindowLocation , slidingWindowLocation + maxItems )
37- . map ( ( option , i , arr ) => {
38- const isTopLimit = i === 0 && shouldRenderTopEllipsis ;
39- const isBottomLimit = i === arr . length - 1 && shouldRenderBottomEllipsis ;
40- return isTopLimit || isBottomLimit
41- ? overflowFormat
42- : style ( option , i + slidingWindowLocation === cursor ) ;
43- } ) ;
61+ const slidingWindowLocationEnd = Math . min ( slidingWindowLocation + maxItems , options . length ) ;
62+ const lineGroups : Array < string [ ] > = [ ] ;
63+ let lineCount = 0 ;
64+ if ( shouldRenderTopEllipsis ) {
65+ lineCount ++ ;
66+ }
67+ if ( shouldRenderBottomEllipsis ) {
68+ lineCount ++ ;
69+ }
70+
71+ const slidingWindowLocationWithEllipsis =
72+ slidingWindowLocation + ( shouldRenderTopEllipsis ? 1 : 0 ) ;
73+ const slidingWindowLocationEndWithEllipsis =
74+ slidingWindowLocationEnd - ( shouldRenderBottomEllipsis ? 1 : 0 ) ;
75+
76+ for ( let i = slidingWindowLocationWithEllipsis ; i < slidingWindowLocationEndWithEllipsis ; i ++ ) {
77+ const wrappedLines = wrapAnsi ( style ( options [ i ] , i === cursor ) , maxWidth ) . split ( '\n' ) ;
78+ lineGroups . push ( wrappedLines ) ;
79+ lineCount += wrappedLines . length ;
80+ }
81+
82+ if ( lineCount > outputMaxItems ) {
83+ let precedingRemovals = 0 ;
84+ let followingRemovals = 0 ;
85+ let newLineCount = lineCount ;
86+ const cursorGroupIndex = cursor - slidingWindowLocationWithEllipsis ;
87+ const trimLinesLocal = ( startIndex : number , endIndex : number ) =>
88+ trimLines ( lineGroups , newLineCount , startIndex , endIndex , outputMaxItems ) ;
89+
90+ if ( shouldRenderTopEllipsis ) {
91+ ( { lineCount : newLineCount , removals : precedingRemovals } = trimLinesLocal (
92+ 0 ,
93+ cursorGroupIndex
94+ ) ) ;
95+ if ( newLineCount > outputMaxItems ) {
96+ ( { lineCount : newLineCount , removals : followingRemovals } = trimLinesLocal (
97+ cursorGroupIndex + 1 ,
98+ lineGroups . length
99+ ) ) ;
100+ }
101+ } else {
102+ ( { lineCount : newLineCount , removals : followingRemovals } = trimLinesLocal (
103+ cursorGroupIndex + 1 ,
104+ lineGroups . length
105+ ) ) ;
106+ if ( newLineCount > outputMaxItems ) {
107+ ( { lineCount : newLineCount , removals : precedingRemovals } = trimLinesLocal (
108+ 0 ,
109+ cursorGroupIndex
110+ ) ) ;
111+ }
112+ }
113+
114+ if ( precedingRemovals > 0 ) {
115+ shouldRenderTopEllipsis = true ;
116+ lineGroups . splice ( 0 , precedingRemovals ) ;
117+ }
118+ if ( followingRemovals > 0 ) {
119+ shouldRenderBottomEllipsis = true ;
120+ lineGroups . splice ( lineGroups . length - followingRemovals , followingRemovals ) ;
121+ }
122+ }
123+
124+ const result : string [ ] = [ ] ;
125+ if ( shouldRenderTopEllipsis ) {
126+ result . push ( overflowFormat ) ;
127+ }
128+ for ( const lineGroup of lineGroups ) {
129+ for ( const line of lineGroup ) {
130+ result . push ( line ) ;
131+ }
132+ }
133+ if ( shouldRenderBottomEllipsis ) {
134+ result . push ( overflowFormat ) ;
135+ }
136+
137+ return result ;
44138} ;
0 commit comments