Skip to content

Commit 75fb09a

Browse files
committed
collapse char reads into one file
1 parent 8269f3a commit 75fb09a

File tree

5 files changed

+124
-215
lines changed

5 files changed

+124
-215
lines changed

src/core/tools/__tests__/readFileTool.spec.ts

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as path from "path"
44

55
import { countFileLines } from "../../../integrations/misc/line-counter"
66
import { readLines } from "../../../integrations/misc/read-lines"
7-
import { readPartialContent } from "../../../integrations/misc/read-partial-content"
87
import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../../integrations/misc/extract-text"
98
import { parseSourceCodeDefinitionsForFile } from "../../../services/tree-sitter"
109
import { isBinaryFile } from "isbinaryfile"
@@ -35,17 +34,6 @@ vi.mock("../../../integrations/misc/line-counter")
3534
vi.mock("../../../integrations/misc/read-lines", () => ({
3635
readLines: vi.fn().mockResolvedValue("mocked line content"),
3736
}))
38-
vi.mock("../../../integrations/misc/read-partial-content", () => ({
39-
readPartialSingleLineContent: vi.fn().mockResolvedValue("mocked partial content"),
40-
readPartialContent: vi.fn().mockResolvedValue({
41-
content: "mocked partial content",
42-
charactersRead: 100,
43-
totalCharacters: 1000,
44-
linesRead: 5,
45-
totalLines: 50,
46-
lastLineRead: 5,
47-
}),
48-
}))
4937
vi.mock("../contextValidator")
5038

5139
// Mock fs/promises readFile for image tests
@@ -1379,15 +1367,8 @@ describe("read_file tool XML output structure", () => {
13791367
reason: "This is a partial read - the remaining content cannot be accessed due to context limitations.",
13801368
})
13811369

1382-
// Mock readPartialContent to return truncated content
1383-
vi.mocked(readPartialContent).mockResolvedValue({
1384-
content: "Line 1\nLine 2\n...truncated...",
1385-
charactersRead: 2000,
1386-
totalCharacters: 500000,
1387-
linesRead: 100,
1388-
totalLines: 10000,
1389-
lastLineRead: 100,
1390-
})
1370+
// Mock readLines to return truncated content with maxChars
1371+
vi.mocked(readLines).mockResolvedValue("Line 1\nLine 2\n...truncated...")
13911372

13921373
const result = await executeReadFileTool(
13931374
{ args: `<file><path>large-file.ts</path></file>` },
@@ -1430,15 +1411,8 @@ describe("read_file tool XML output structure", () => {
14301411
reason: "This is a partial read - the remaining content cannot be accessed due to context limitations.",
14311412
})
14321413

1433-
// Mock readPartialContent to return truncated content for single-line file
1434-
vi.mocked(readPartialContent).mockResolvedValue({
1435-
content: "const a=1;const b=2;...truncated",
1436-
charactersRead: 5000,
1437-
totalCharacters: 10000,
1438-
linesRead: 1,
1439-
totalLines: 1,
1440-
lastLineRead: 1,
1441-
})
1414+
// Mock readLines to return truncated content for single-line file with maxChars
1415+
vi.mocked(readLines).mockResolvedValue("const a=1;const b=2;...truncated")
14421416

14431417
const result = await executeReadFileTool(
14441418
{ args: `<file><path>minified.js</path></file>` },
@@ -1463,15 +1437,8 @@ describe("read_file tool XML output structure", () => {
14631437
reason: "This is a partial read - the remaining content cannot be accessed due to context limitations.",
14641438
})
14651439

1466-
// Mock readPartialContent to return truncated content
1467-
vi.mocked(readPartialContent).mockResolvedValue({
1468-
content: "Line 1\nLine 2\n...truncated...",
1469-
charactersRead: 50000,
1470-
totalCharacters: 250000,
1471-
linesRead: 1000,
1472-
totalLines: 5000,
1473-
lastLineRead: 1000,
1474-
})
1440+
// Mock readLines to return truncated content with maxChars
1441+
vi.mocked(readLines).mockResolvedValue("Line 1\nLine 2\n...truncated...")
14751442

14761443
const result = await executeReadFileTool(
14771444
{ args: `<file><path>large-file.ts</path></file>` },
@@ -1496,15 +1463,8 @@ describe("read_file tool XML output structure", () => {
14961463
reason: "This is a partial read - the remaining content cannot be accessed due to context limitations.",
14971464
})
14981465

1499-
// Mock readPartialContent for single-line file
1500-
vi.mocked(readPartialContent).mockResolvedValue({
1501-
content: "const a=1;const b=2;const c=3;",
1502-
charactersRead: 8000,
1503-
totalCharacters: 10000,
1504-
linesRead: 1,
1505-
totalLines: 1,
1506-
lastLineRead: 1,
1507-
})
1466+
// Mock readLines for single-line file with maxChars
1467+
vi.mocked(readLines).mockResolvedValue("const a=1;const b=2;const c=3;")
15081468

15091469
const result = await executeReadFileTool(
15101470
{ args: `<file><path>semi-large.js</path></file>` },

src/core/tools/readFileTool.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { isPathOutsideWorkspace } from "../../utils/pathUtils"
1212
import { getReadablePath } from "../../utils/path"
1313
import { countFileLines } from "../../integrations/misc/line-counter"
1414
import { readLines } from "../../integrations/misc/read-lines"
15-
import { readPartialContent } from "../../integrations/misc/read-partial-content"
1615
import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text"
1716
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
1817
import { parseXml } from "../../utils/xml"
@@ -578,12 +577,15 @@ export async function readFileTool(
578577

579578
// Handle files with validation limits (character-based reading)
580579
if (shouldApplyValidation) {
581-
const result = await readPartialContent(fullPath, validation.safeContentLimit)
580+
const partialContent = await readLines(fullPath, undefined, undefined, validation.safeContentLimit)
581+
582+
// Count lines in the partial content
583+
const linesRead = partialContent ? (partialContent.match(/\n/g) || []).length + 1 : 0
582584

583585
// Generate line range attribute based on what was read
584-
const lineRangeAttr = result.linesRead === 1 ? ` lines="1"` : ` lines="1-${result.lastLineRead}"`
586+
const lineRangeAttr = linesRead === 1 ? ` lines="1"` : ` lines="1-${linesRead}"`
585587

586-
const content = addLineNumbers(result.content, 1)
588+
const content = addLineNumbers(partialContent, 1)
587589
let xmlInfo = `<content${lineRangeAttr}>\n${content}</content>\n`
588590

589591
// Add simple notice about partial read

src/integrations/misc/__tests__/read-lines.spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,79 @@ describe("nthline", () => {
128128
expect(lines).toEqual("\n\n\n")
129129
})
130130
})
131+
132+
describe("maxChars parameter", () => {
133+
it("should limit output to maxChars when reading entire file", async () => {
134+
const content = await readLines(testFile, undefined, undefined, 20)
135+
expect(content).toEqual("Line 1\nLine 2\nLine 3")
136+
expect(content.length).toBe(20)
137+
})
138+
139+
it("should limit output to maxChars when reading a range", async () => {
140+
const content = await readLines(testFile, 5, 1, 15)
141+
// When maxChars cuts off in the middle of a line, we get partial content
142+
expect(content).toEqual("Line 2\nLine 3\nL")
143+
expect(content.length).toBe(15)
144+
})
145+
146+
it("should return empty string when maxChars is 0", async () => {
147+
const content = await readLines(testFile, undefined, undefined, 0)
148+
expect(content).toEqual("")
149+
})
150+
151+
it("should handle maxChars smaller than first line", async () => {
152+
const content = await readLines(testFile, undefined, undefined, 3)
153+
expect(content).toEqual("Lin")
154+
expect(content.length).toBe(3)
155+
})
156+
157+
it("should handle maxChars that cuts off in the middle of a line", async () => {
158+
const content = await readLines(testFile, 2, 0, 10)
159+
expect(content).toEqual("Line 1\nLin")
160+
expect(content.length).toBe(10)
161+
})
162+
163+
it("should respect both line limits and maxChars", async () => {
164+
// This should read lines 2-4, but stop at 25 chars
165+
const content = await readLines(testFile, 3, 1, 25)
166+
expect(content).toEqual("Line 2\nLine 3\nLine 4\n")
167+
expect(content.length).toBeLessThanOrEqual(25)
168+
})
169+
170+
it("should handle maxChars with single line file", async () => {
171+
await withTempFile(
172+
"single-line-maxchars.txt",
173+
"This is a long single line of text",
174+
async (filepath) => {
175+
const content = await readLines(filepath, undefined, undefined, 10)
176+
expect(content).toEqual("This is a ")
177+
expect(content.length).toBe(10)
178+
},
179+
)
180+
})
181+
182+
it("should handle maxChars with Unicode characters", async () => {
183+
await withTempFile("unicode-maxchars.txt", "Hello 😀 World\nLine 2", async (filepath) => {
184+
// Note: The emoji counts as 2 chars in JavaScript strings
185+
const content = await readLines(filepath, undefined, undefined, 10)
186+
expect(content).toEqual("Hello 😀 W")
187+
expect(content.length).toBe(10)
188+
})
189+
})
190+
191+
it("should handle maxChars larger than file size", async () => {
192+
const content = await readLines(testFile, undefined, undefined, 1000)
193+
const fullContent = await readLines(testFile)
194+
expect(content).toEqual(fullContent)
195+
})
196+
197+
it("should handle maxChars with empty lines", async () => {
198+
await withTempFile("empty-lines-maxchars.txt", "Line 1\n\n\nLine 4\n", async (filepath) => {
199+
const content = await readLines(filepath, undefined, undefined, 10)
200+
expect(content).toEqual("Line 1\n\n\nL")
201+
expect(content.length).toBe(10)
202+
})
203+
})
204+
})
131205
})
132206
})

src/integrations/misc/read-lines.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ const outOfRangeError = (filepath: string, n: number) => {
1818
* @param filepath - Path to the file to read
1919
* @param endLine - Optional. The line number to stop reading at (inclusive). If undefined, reads to the end of file.
2020
* @param startLine - Optional. The line number to start reading from (inclusive). If undefined, starts from line 0.
21+
* @param maxChars - Optional. Maximum number of characters to read. If specified, reading stops when this limit is reached.
2122
* @returns Promise resolving to a string containing the read lines joined with newlines
2223
* @throws {RangeError} If line numbers are invalid or out of range
2324
*/
24-
export function readLines(filepath: string, endLine?: number, startLine?: number): Promise<string> {
25+
export function readLines(filepath: string, endLine?: number, startLine?: number, maxChars?: number): Promise<string> {
2526
return new Promise((resolve, reject) => {
2627
// Reject if startLine is defined but not a number
2728
if (startLine !== undefined && typeof startLine !== "number") {
@@ -52,11 +53,16 @@ export function readLines(filepath: string, endLine?: number, startLine?: number
5253
)
5354
}
5455

55-
// Set up stream
56-
const input = createReadStream(filepath)
56+
// Set up stream - only add 'end' option when maxChars is specified to avoid reading entire file
57+
const streamOptions =
58+
maxChars !== undefined
59+
? { end: Math.min(maxChars * 2, maxChars + 1024 * 1024) } // Read at most 2x maxChars or maxChars + 1MB
60+
: undefined
61+
const input = createReadStream(filepath, streamOptions)
5762
let buffer = ""
5863
let lineCount = 0
5964
let result = ""
65+
let totalCharsRead = 0
6066

6167
// Handle errors
6268
input.on("error", reject)
@@ -73,7 +79,24 @@ export function readLines(filepath: string, endLine?: number, startLine?: number
7379
while (nextNewline !== -1) {
7480
// If we're in the target range, add this line to the result
7581
if (lineCount >= effectiveStartLine && (endLine === undefined || lineCount <= endLine)) {
76-
result += buffer.substring(pos, nextNewline + 1) // Include the newline
82+
const lineToAdd = buffer.substring(pos, nextNewline + 1) // Include the newline
83+
84+
// Check if adding this line would exceed maxChars (only if maxChars is specified)
85+
if (maxChars !== undefined && totalCharsRead + lineToAdd.length > maxChars) {
86+
// Add only the portion that fits within maxChars
87+
const remainingChars = maxChars - totalCharsRead
88+
if (remainingChars > 0) {
89+
result += lineToAdd.substring(0, remainingChars)
90+
}
91+
input.destroy()
92+
resolve(result)
93+
return
94+
}
95+
96+
result += lineToAdd
97+
if (maxChars !== undefined) {
98+
totalCharsRead += lineToAdd.length
99+
}
77100
}
78101

79102
// Move position and increment line counter
@@ -100,7 +123,15 @@ export function readLines(filepath: string, endLine?: number, startLine?: number
100123
// Process any remaining data in buffer (last line without newline)
101124
if (buffer.length > 0) {
102125
if (lineCount >= effectiveStartLine && (endLine === undefined || lineCount <= endLine)) {
103-
result += buffer
126+
// Check if adding this would exceed maxChars (only if maxChars is specified)
127+
if (maxChars !== undefined && totalCharsRead + buffer.length > maxChars) {
128+
const remainingChars = maxChars - totalCharsRead
129+
if (remainingChars > 0) {
130+
result += buffer.substring(0, remainingChars)
131+
}
132+
} else {
133+
result += buffer
134+
}
104135
}
105136
lineCount++
106137
}

0 commit comments

Comments
 (0)