Skip to content

Commit 98485e8

Browse files
committed
Bound lines array in getLastNVisualLines to prevent unbounded memory growth
1 parent 59cca53 commit 98485e8

2 files changed

Lines changed: 44 additions & 5 deletions

File tree

cli/src/utils/__tests__/text-layout.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, test, expect } from 'bun:test'
22

3-
import { computeInputLayoutMetrics } from '../text-layout'
3+
import { computeInputLayoutMetrics, getLastNVisualLines } from '../text-layout'
44

55
describe('computeInputLayoutMetrics', () => {
66
test('single-line content keeps height at 1 without gutter', () => {
@@ -80,3 +80,39 @@ describe('computeInputLayoutMetrics', () => {
8080
expect(metrics.gutterEnabled).toBe(false)
8181
})
8282
})
83+
84+
describe('getLastNVisualLines', () => {
85+
test('returns empty array when n or cols <= 0 or text is empty', () => {
86+
expect(getLastNVisualLines('', 40, 5)).toEqual({ lines: [], hasMore: false })
87+
expect(getLastNVisualLines('hello', 0, 5)).toEqual({ lines: [], hasMore: false })
88+
expect(getLastNVisualLines('hello', 40, 0)).toEqual({ lines: [], hasMore: false })
89+
})
90+
91+
test('returns all lines without hasMore when line count <= n', () => {
92+
const text = 'line 1\nline 2\nline 3'
93+
const result = getLastNVisualLines(text, 40, 5)
94+
expect(result.lines).toEqual(['line 1', 'line 2', 'line 3'])
95+
expect(result.hasMore).toBe(false)
96+
})
97+
98+
test('returns only last n lines with hasMore = true when line count > n', () => {
99+
const text = 'line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7'
100+
const result = getLastNVisualLines(text, 40, 3)
101+
expect(result.lines).toEqual(['line 5', 'line 6', 'line 7'])
102+
expect(result.hasMore).toBe(true)
103+
})
104+
105+
test('correctly bounds array when text has hundreds of wrapped lines', () => {
106+
const text = Array.from({ length: 100 }, (_, i) => `Line ${i}`).join('\n')
107+
const result = getLastNVisualLines(text, 40, 4)
108+
expect(result.lines).toEqual(['Line 96', 'Line 97', 'Line 98', 'Line 99'])
109+
expect(result.hasMore).toBe(true)
110+
})
111+
112+
test('wraps long lines exceeding column width', () => {
113+
const text = 'supercalifragilisticexpialidocious'
114+
const result = getLastNVisualLines(text, 10, 2)
115+
expect(result.lines.length).toBe(2)
116+
expect(result.hasMore).toBe(true)
117+
})
118+
})

cli/src/utils/text-layout.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,16 @@ export function getLastNVisualLines(text: string, cols: number, n: number): { li
6262
const lines: string[] = []
6363
if (!text) return { lines, hasMore: false }
6464

65+
let hasMore = false
6566
const tokens = text.split(/(\s+)/)
6667
let current = ''
6768
let currentWidth = 0
6869

6970
const pushLine = () => {
71+
if (lines.length === n) {
72+
hasMore = true
73+
lines.shift()
74+
}
7075
lines.push(current)
7176
current = ''
7277
currentWidth = 0
@@ -104,10 +109,8 @@ export function getLastNVisualLines(text: string, cols: number, n: number): { li
104109
appendSegment(token)
105110
}
106111

107-
if (current.length > 0 || lines.length === 0) pushLine()
108-
const hasMore = lines.length > n
109-
const lastLines = lines.slice(-n)
110-
return { lines: lastLines, hasMore }
112+
if (current.length > 0 || (!hasMore && lines.length === 0)) pushLine()
113+
return { lines, hasMore }
111114
}
112115

113116
export function computeInputLayoutMetrics({

0 commit comments

Comments
 (0)