Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions locales/ca/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/de/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/es/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/fr/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/hi/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/it/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/ja/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/ko/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/pl/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/pt-BR/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/tr/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/vi/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/zh-CN/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions locales/zh-TW/README.md

Large diffs are not rendered by default.

99 changes: 97 additions & 2 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import {
stripLineNumbers,
everyLineHasLineNumbers,
} from "../integrations/misc/extract-text"
import { countFileLines } from "../integrations/misc/line-counter"
import { ExitCodeDetails } from "../integrations/terminal/TerminalProcess"
import { Terminal } from "../integrations/terminal/Terminal"
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
import { listFiles } from "../services/glob/list-files"
import { regexSearchFiles } from "../services/ripgrep"
import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
import { parseSourceCodeDefinitionsForFile, parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
import { CheckpointStorage } from "../shared/checkpoints"
import { ApiConfiguration } from "../shared/api"
import { findLastIndex } from "../shared/array"
Expand Down Expand Up @@ -78,6 +79,7 @@ import { DiffStrategy, getDiffStrategy } from "./diff/DiffStrategy"
import { insertGroups } from "./diff/insert-groups"
import { telemetryService } from "../services/telemetry/TelemetryService"
import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validator"
import { readLines } from "../integrations/misc/read-lines"
import { getWorkspacePath } from "../utils/path"

type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
Expand Down Expand Up @@ -2225,6 +2227,8 @@ export class Cline extends EventEmitter<ClineEvents> {

case "read_file": {
const relPath: string | undefined = block.params.path
const startLineStr: string | undefined = block.params.start_line
const endLineStr: string | undefined = block.params.end_line
const sharedMessageProps: ClineSayTool = {
tool: "readFile",
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
Expand All @@ -2244,6 +2248,45 @@ export class Cline extends EventEmitter<ClineEvents> {
break
}

// Check if we're doing a line range read
let isRangeRead = false
let startLine: number | undefined = undefined
let endLine: number | undefined = undefined

// Check if we have either range parameter
if (startLineStr || endLineStr) {
isRangeRead = true
}

// Parse start_line if provided
if (startLineStr) {
startLine = parseInt(startLineStr)
if (isNaN(startLine)) {
// Invalid start_line
this.consecutiveMistakeCount++
await this.say("error", `Failed to parse start_line: ${startLineStr}`)
pushToolResult(formatResponse.toolError("Invalid start_line value"))
break
}
startLine -= 1 // Convert to 0-based index
}

// Parse end_line if provided
if (endLineStr) {
endLine = parseInt(endLineStr)

if (isNaN(endLine)) {
// Invalid end_line
this.consecutiveMistakeCount++
await this.say("error", `Failed to parse end_line: ${endLineStr}`)
pushToolResult(formatResponse.toolError("Invalid end_line value"))
break
}

// Convert to 0-based index
endLine -= 1
}

const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
if (!accessAllowed) {
await this.say("rooignore_error", relPath)
Expand All @@ -2258,12 +2301,63 @@ export class Cline extends EventEmitter<ClineEvents> {
...sharedMessageProps,
content: absolutePath,
} satisfies ClineSayTool)

const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
break
}

// Get the maxReadFileLine setting
const { maxReadFileLine } = (await this.providerRef.deref()?.getState()) ?? {}

// Count total lines in the file
let totalLines = 0
try {
totalLines = await countFileLines(absolutePath)
} catch (error) {
console.error(`Error counting lines in file ${absolutePath}:`, error)
}

// now execute the tool like normal
const content = await extractTextFromFile(absolutePath)
let content: string
let isFileTruncated = false
let sourceCodeDef = ""

if (isRangeRead) {
// Read specific lines (startLine is guaranteed to be defined if isRangeRead is true)
console.log("Reading specific lines", startLine, endLine, startLineStr, endLineStr)
if (startLine === undefined) {
content = addLineNumbers(await readLines(absolutePath, endLine, startLine))
} else {
content = addLineNumbers(
await readLines(absolutePath, endLine, startLine),
startLine,
)
}
} else if (totalLines > maxReadFileLine) {
// If file is too large, only read the first maxReadFileLine lines
isFileTruncated = true

const res = await Promise.all([
readLines(absolutePath, maxReadFileLine - 1, 0),
parseSourceCodeDefinitionsForFile(absolutePath, this.rooIgnoreController),
])

content = addLineNumbers(res[0])
const result = res[1]
if (result) {
sourceCodeDef = `\n\n${result}`
}
} else {
// Read entire file
content = await extractTextFromFile(absolutePath)
}

// Add truncation notice if applicable
if (isFileTruncated) {
content += `\n\n[File truncated: showing ${maxReadFileLine} of ${totalLines} total lines. Use start_line and end_line if you need to read more.].${sourceCodeDef}`
}

pushToolResult(content)
break
}
Expand All @@ -2272,6 +2366,7 @@ export class Cline extends EventEmitter<ClineEvents> {
break
}
}

case "list_files": {
const relDirPath: string | undefined = block.params.path
const recursiveRaw: string | undefined = block.params.recursive
Expand Down
138 changes: 138 additions & 0 deletions src/core/__tests__/read-file-tool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as path from "path"
import { countFileLines } from "../../integrations/misc/line-counter"
import { readLines } from "../../integrations/misc/read-lines"
import { extractTextFromFile, addLineNumbers } from "../../integrations/misc/extract-text"

// Mock the required functions
jest.mock("../../integrations/misc/line-counter")
jest.mock("../../integrations/misc/read-lines")
jest.mock("../../integrations/misc/extract-text")

describe("read_file tool with maxReadFileLine setting", () => {
// Mock original implementation first to use in tests
const originalCountFileLines = jest.requireActual("../../integrations/misc/line-counter").countFileLines
const originalReadLines = jest.requireActual("../../integrations/misc/read-lines").readLines
const originalExtractTextFromFile = jest.requireActual("../../integrations/misc/extract-text").extractTextFromFile
const originalAddLineNumbers = jest.requireActual("../../integrations/misc/extract-text").addLineNumbers

beforeEach(() => {
jest.resetAllMocks()
// Reset mocks to simulate original behavior
;(countFileLines as jest.Mock).mockImplementation(originalCountFileLines)
;(readLines as jest.Mock).mockImplementation(originalReadLines)
;(extractTextFromFile as jest.Mock).mockImplementation(originalExtractTextFromFile)
;(addLineNumbers as jest.Mock).mockImplementation(originalAddLineNumbers)
})

// Test for the case when file size is smaller than maxReadFileLine
it("should read entire file when line count is less than maxReadFileLine", async () => {
// Mock necessary functions
;(countFileLines as jest.Mock).mockResolvedValue(100)
;(extractTextFromFile as jest.Mock).mockResolvedValue("Small file content")

// Create mock implementation that would simulate the behavior
// Note: We're not testing the Cline class directly as it would be too complex
// We're testing the logic flow that would happen in the read_file implementation

const filePath = path.resolve("/test", "smallFile.txt")
const maxReadFileLine = 500

// Check line count
const lineCount = await countFileLines(filePath)
expect(lineCount).toBeLessThan(maxReadFileLine)

// Should use extractTextFromFile for small files
if (lineCount < maxReadFileLine) {
await extractTextFromFile(filePath)
}

expect(extractTextFromFile).toHaveBeenCalledWith(filePath)
expect(readLines).not.toHaveBeenCalled()
})

// Test for the case when file size is larger than maxReadFileLine
it("should truncate file when line count exceeds maxReadFileLine", async () => {
// Mock necessary functions
;(countFileLines as jest.Mock).mockResolvedValue(5000)
;(readLines as jest.Mock).mockResolvedValue("First 500 lines of large file")
;(addLineNumbers as jest.Mock).mockReturnValue("1 | First line\n2 | Second line\n...")

const filePath = path.resolve("/test", "largeFile.txt")
const maxReadFileLine = 500

// Check line count
const lineCount = await countFileLines(filePath)
expect(lineCount).toBeGreaterThan(maxReadFileLine)

// Should use readLines for large files
if (lineCount > maxReadFileLine) {
const content = await readLines(filePath, maxReadFileLine - 1, 0)
const numberedContent = addLineNumbers(content)

// Verify the truncation message is shown (simulated)
const truncationMsg = `\n\n[File truncated: showing ${maxReadFileLine} of ${lineCount} total lines]`
const fullResult = numberedContent + truncationMsg

expect(fullResult).toContain("File truncated")
}

expect(readLines).toHaveBeenCalledWith(filePath, maxReadFileLine - 1, 0)
expect(addLineNumbers).toHaveBeenCalled()
expect(extractTextFromFile).not.toHaveBeenCalled()
})

// Test for the case when the file is a source code file
it("should add source code file type info for large source code files", async () => {
// Mock necessary functions
;(countFileLines as jest.Mock).mockResolvedValue(5000)
;(readLines as jest.Mock).mockResolvedValue("First 500 lines of large JavaScript file")
;(addLineNumbers as jest.Mock).mockReturnValue('1 | const foo = "bar";\n2 | function test() {...')

const filePath = path.resolve("/test", "largeFile.js")
const maxReadFileLine = 500

// Check line count
const lineCount = await countFileLines(filePath)
expect(lineCount).toBeGreaterThan(maxReadFileLine)

// Check if the file is a source code file
const fileExt = path.extname(filePath).toLowerCase()
const isSourceCode = [
".js",
".ts",
".jsx",
".tsx",
".py",
".java",
".c",
".cpp",
".cs",
".go",
".rb",
".php",
".swift",
".rs",
].includes(fileExt)
expect(isSourceCode).toBeTruthy()

// Should use readLines for large files
if (lineCount > maxReadFileLine) {
const content = await readLines(filePath, maxReadFileLine - 1, 0)
const numberedContent = addLineNumbers(content)

// Verify the truncation message and source code message are shown (simulated)
let truncationMsg = `\n\n[File truncated: showing ${maxReadFileLine} of ${lineCount} total lines]`
if (isSourceCode) {
truncationMsg +=
"\n\nThis appears to be a source code file. Consider using list_code_definition_names to understand its structure."
}
const fullResult = numberedContent + truncationMsg

expect(fullResult).toContain("source code file")
expect(fullResult).toContain("list_code_definition_names")
}

expect(readLines).toHaveBeenCalledWith(filePath, maxReadFileLine - 1, 0)
expect(addLineNumbers).toHaveBeenCalled()
})
})
2 changes: 1 addition & 1 deletion src/core/assistant-message/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface ExecuteCommandToolUse extends ToolUse {

export interface ReadFileToolUse extends ToolUse {
name: "read_file"
params: Partial<Pick<Record<ToolParamName, string>, "path">>
params: Partial<Pick<Record<ToolParamName, string>, "path" | "start_line" | "end_line">>
}

export interface WriteToFileToolUse extends ToolUse {
Expand Down
Loading