11import type { Writable } from 'node:stream' ;
2- import { WriteStream } from 'node:tty' ;
2+ import { getColumns , getRows } from '@clack/core' ;
3+ import { wrapAnsi } from 'fast-wrap-ansi' ;
34import color from 'picocolors' ;
45import type { CommonOptions } from './common.js' ;
56
@@ -8,37 +9,129 @@ export interface LimitOptionsParams<TOption> extends CommonOptions {
89 maxItems : number | undefined ;
910 cursor : number ;
1011 style : ( option : TOption , active : boolean ) => string ;
12+ columnPadding ?: number ;
13+ rowPadding ?: number ;
1114}
1215
16+ const trimLines = (
17+ groups : Array < string [ ] > ,
18+ initialLineCount : number ,
19+ startIndex : number ,
20+ endIndex : number ,
21+ maxLines : number
22+ ) => {
23+ let lineCount = initialLineCount ;
24+ let removals = 0 ;
25+ for ( let i = startIndex ; i < endIndex ; i ++ ) {
26+ const group = groups [ i ] ;
27+ lineCount = lineCount - group . length ;
28+ removals ++ ;
29+ if ( lineCount <= maxLines ) {
30+ break ;
31+ }
32+ }
33+ return { lineCount, removals } ;
34+ } ;
35+
1336export const limitOptions = < TOption > ( params : LimitOptionsParams < TOption > ) : string [ ] => {
1437 const { cursor, options, style } = params ;
1538 const output : Writable = params . output ?? process . stdout ;
16- const rows = output instanceof WriteStream && output . rows !== undefined ? output . rows : 10 ;
39+ const columns = getColumns ( output ) ;
40+ const columnPadding = params . columnPadding ?? 0 ;
41+ const rowPadding = params . rowPadding ?? 4 ;
42+ const maxWidth = columns - columnPadding ;
43+ const rows = getRows ( output ) ;
1744 const overflowFormat = color . dim ( '...' ) ;
1845
1946 const paramMaxItems = params . maxItems ?? Number . POSITIVE_INFINITY ;
20- const outputMaxItems = Math . max ( rows - 4 , 0 ) ;
47+ const outputMaxItems = Math . max ( rows - rowPadding , 0 ) ;
2148 // We clamp to minimum 5 because anything less doesn't make sense UX wise
22- const maxItems = Math . min ( outputMaxItems , Math . max ( paramMaxItems , 5 ) ) ;
49+ const maxItems = Math . max ( paramMaxItems , 5 ) ;
2350 let slidingWindowLocation = 0 ;
2451
25- if ( cursor >= slidingWindowLocation + maxItems - 3 ) {
52+ if ( cursor >= maxItems - 3 ) {
2653 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 ) ;
2954 }
3055
31- const shouldRenderTopEllipsis = maxItems < options . length && slidingWindowLocation > 0 ;
32- const shouldRenderBottomEllipsis =
56+ let shouldRenderTopEllipsis = maxItems < options . length && slidingWindowLocation > 0 ;
57+ let shouldRenderBottomEllipsis =
3358 maxItems < options . length && slidingWindowLocation + maxItems < options . length ;
3459
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- } ) ;
60+ const slidingWindowLocationEnd = Math . min ( slidingWindowLocation + maxItems , options . length ) ;
61+ const lineGroups : Array < string [ ] > = [ ] ;
62+ let lineCount = 0 ;
63+ if ( shouldRenderTopEllipsis ) {
64+ lineCount ++ ;
65+ }
66+ if ( shouldRenderBottomEllipsis ) {
67+ lineCount ++ ;
68+ }
69+
70+ const slidingWindowLocationWithEllipsis =
71+ slidingWindowLocation + ( shouldRenderTopEllipsis ? 1 : 0 ) ;
72+ const slidingWindowLocationEndWithEllipsis =
73+ slidingWindowLocationEnd - ( shouldRenderBottomEllipsis ? 1 : 0 ) ;
74+
75+ for ( let i = slidingWindowLocationWithEllipsis ; i < slidingWindowLocationEndWithEllipsis ; i ++ ) {
76+ const wrappedLines = wrapAnsi ( style ( options [ i ] , i === cursor ) , maxWidth ) . split ( '\n' ) ;
77+ lineGroups . push ( wrappedLines ) ;
78+ lineCount += wrappedLines . length ;
79+ }
80+
81+ if ( lineCount > outputMaxItems ) {
82+ let precedingRemovals = 0 ;
83+ let followingRemovals = 0 ;
84+ let newLineCount = lineCount ;
85+ const cursorGroupIndex = cursor - slidingWindowLocationWithEllipsis ;
86+ const trimLinesLocal = ( startIndex : number , endIndex : number ) =>
87+ trimLines ( lineGroups , newLineCount , startIndex , endIndex , outputMaxItems ) ;
88+
89+ if ( shouldRenderTopEllipsis ) {
90+ ( { lineCount : newLineCount , removals : precedingRemovals } = trimLinesLocal (
91+ 0 ,
92+ cursorGroupIndex
93+ ) ) ;
94+ if ( newLineCount > outputMaxItems ) {
95+ ( { lineCount : newLineCount , removals : followingRemovals } = trimLinesLocal (
96+ cursorGroupIndex + 1 ,
97+ lineGroups . length
98+ ) ) ;
99+ }
100+ } else {
101+ ( { lineCount : newLineCount , removals : followingRemovals } = trimLinesLocal (
102+ cursorGroupIndex + 1 ,
103+ lineGroups . length
104+ ) ) ;
105+ if ( newLineCount > outputMaxItems ) {
106+ ( { lineCount : newLineCount , removals : precedingRemovals } = trimLinesLocal (
107+ 0 ,
108+ cursorGroupIndex
109+ ) ) ;
110+ }
111+ }
112+
113+ if ( precedingRemovals > 0 ) {
114+ shouldRenderTopEllipsis = true ;
115+ lineGroups . splice ( 0 , precedingRemovals ) ;
116+ }
117+ if ( followingRemovals > 0 ) {
118+ shouldRenderBottomEllipsis = true ;
119+ lineGroups . splice ( lineGroups . length - followingRemovals , followingRemovals ) ;
120+ }
121+ }
122+
123+ const result : string [ ] = [ ] ;
124+ if ( shouldRenderTopEllipsis ) {
125+ result . push ( overflowFormat ) ;
126+ }
127+ for ( const lineGroup of lineGroups ) {
128+ for ( const line of lineGroup ) {
129+ result . push ( line ) ;
130+ }
131+ }
132+ if ( shouldRenderBottomEllipsis ) {
133+ result . push ( overflowFormat ) ;
134+ }
135+
136+ return result ;
44137} ;
0 commit comments