Skip to content

Commit e369486

Browse files
committed
Extract code for read_file from Cline
1 parent 62a7bc7 commit e369486

File tree

2 files changed

+169
-145
lines changed

2 files changed

+169
-145
lines changed

src/core/Cline.ts

Lines changed: 3 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from "../integrations/misc/extract-text"
3131
import { countFileLines } from "../integrations/misc/line-counter"
3232
import { fetchInstructionsTool } from "./tools/fetchInstructionsTool"
33+
import { readFileTool } from "./tools/readFileTool"
3334
import { ExitCodeDetails } from "../integrations/terminal/TerminalProcess"
3435
import { Terminal } from "../integrations/terminal/Terminal"
3536
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
@@ -2256,151 +2257,8 @@ export class Cline extends EventEmitter<ClineEvents> {
22562257
}
22572258

22582259
case "read_file": {
2259-
const relPath: string | undefined = block.params.path
2260-
const startLineStr: string | undefined = block.params.start_line
2261-
const endLineStr: string | undefined = block.params.end_line
2262-
2263-
// Get the full path and determine if it's outside the workspace
2264-
const fullPath = relPath ? path.resolve(this.cwd, removeClosingTag("path", relPath)) : ""
2265-
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
2266-
2267-
const sharedMessageProps: ClineSayTool = {
2268-
tool: "readFile",
2269-
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
2270-
isOutsideWorkspace,
2271-
}
2272-
try {
2273-
if (block.partial) {
2274-
const partialMessage = JSON.stringify({
2275-
...sharedMessageProps,
2276-
content: undefined,
2277-
} satisfies ClineSayTool)
2278-
await this.ask("tool", partialMessage, block.partial).catch(() => {})
2279-
break
2280-
} else {
2281-
if (!relPath) {
2282-
this.consecutiveMistakeCount++
2283-
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
2284-
break
2285-
}
2286-
2287-
// Check if we're doing a line range read
2288-
let isRangeRead = false
2289-
let startLine: number | undefined = undefined
2290-
let endLine: number | undefined = undefined
2291-
2292-
// Check if we have either range parameter
2293-
if (startLineStr || endLineStr) {
2294-
isRangeRead = true
2295-
}
2296-
2297-
// Parse start_line if provided
2298-
if (startLineStr) {
2299-
startLine = parseInt(startLineStr)
2300-
if (isNaN(startLine)) {
2301-
// Invalid start_line
2302-
this.consecutiveMistakeCount++
2303-
await this.say("error", `Failed to parse start_line: ${startLineStr}`)
2304-
pushToolResult(formatResponse.toolError("Invalid start_line value"))
2305-
break
2306-
}
2307-
startLine -= 1 // Convert to 0-based index
2308-
}
2309-
2310-
// Parse end_line if provided
2311-
if (endLineStr) {
2312-
endLine = parseInt(endLineStr)
2313-
2314-
if (isNaN(endLine)) {
2315-
// Invalid end_line
2316-
this.consecutiveMistakeCount++
2317-
await this.say("error", `Failed to parse end_line: ${endLineStr}`)
2318-
pushToolResult(formatResponse.toolError("Invalid end_line value"))
2319-
break
2320-
}
2321-
2322-
// Convert to 0-based index
2323-
endLine -= 1
2324-
}
2325-
2326-
const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
2327-
if (!accessAllowed) {
2328-
await this.say("rooignore_error", relPath)
2329-
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
2330-
2331-
break
2332-
}
2333-
2334-
this.consecutiveMistakeCount = 0
2335-
const absolutePath = path.resolve(this.cwd, relPath)
2336-
const completeMessage = JSON.stringify({
2337-
...sharedMessageProps,
2338-
content: absolutePath,
2339-
} satisfies ClineSayTool)
2340-
2341-
const didApprove = await askApproval("tool", completeMessage)
2342-
if (!didApprove) {
2343-
break
2344-
}
2345-
2346-
// Get the maxReadFileLine setting
2347-
const { maxReadFileLine = 500 } = (await this.providerRef.deref()?.getState()) ?? {}
2348-
2349-
// Count total lines in the file
2350-
let totalLines = 0
2351-
try {
2352-
totalLines = await countFileLines(absolutePath)
2353-
} catch (error) {
2354-
console.error(`Error counting lines in file ${absolutePath}:`, error)
2355-
}
2356-
2357-
// now execute the tool like normal
2358-
let content: string
2359-
let isFileTruncated = false
2360-
let sourceCodeDef = ""
2361-
2362-
const isBinary = await isBinaryFile(absolutePath).catch(() => false)
2363-
2364-
if (isRangeRead) {
2365-
if (startLine === undefined) {
2366-
content = addLineNumbers(await readLines(absolutePath, endLine, startLine))
2367-
} else {
2368-
content = addLineNumbers(
2369-
await readLines(absolutePath, endLine, startLine),
2370-
startLine + 1,
2371-
)
2372-
}
2373-
} else if (!isBinary && maxReadFileLine >= 0 && totalLines > maxReadFileLine) {
2374-
// If file is too large, only read the first maxReadFileLine lines
2375-
isFileTruncated = true
2376-
2377-
const res = await Promise.all([
2378-
maxReadFileLine > 0 ? readLines(absolutePath, maxReadFileLine - 1, 0) : "",
2379-
parseSourceCodeDefinitionsForFile(absolutePath, this.rooIgnoreController),
2380-
])
2381-
2382-
content = res[0].length > 0 ? addLineNumbers(res[0]) : ""
2383-
const result = res[1]
2384-
if (result) {
2385-
sourceCodeDef = `\n\n${result}`
2386-
}
2387-
} else {
2388-
// Read entire file
2389-
content = await extractTextFromFile(absolutePath)
2390-
}
2391-
2392-
// Add truncation notice if applicable
2393-
if (isFileTruncated) {
2394-
content += `\n\n[Showing only ${maxReadFileLine} of ${totalLines} total lines. Use start_line and end_line if you need to read more]${sourceCodeDef}`
2395-
}
2396-
2397-
pushToolResult(content)
2398-
break
2399-
}
2400-
} catch (error) {
2401-
await handleError("reading file", error)
2402-
break
2403-
}
2260+
readFileTool(this, block, askApproval, handleError, pushToolResult)
2261+
break
24042262
}
24052263

24062264
case "fetch_instructions": {

src/core/tools/readFileTool.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import path from "path"
2+
import { Cline } from "../Cline"
3+
import { ClineSayTool } from "../../shared/ExtensionMessage"
4+
import { ToolUse } from "../assistant-message"
5+
import { formatResponse } from "../prompts/responses"
6+
import { AskApproval, HandleError, PushToolResult } from "./types"
7+
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
8+
import { getReadablePath } from "../../utils/path"
9+
import { countFileLines } from "../../integrations/misc/line-counter"
10+
import { readLines } from "../../integrations/misc/read-lines"
11+
import { extractTextFromFile, addLineNumbers } from "../../integrations/misc/extract-text"
12+
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
13+
import { isBinaryFile } from "isbinaryfile"
14+
15+
export async function readFileTool(
16+
cline: Cline,
17+
block: ToolUse,
18+
askApproval: AskApproval,
19+
handleError: HandleError,
20+
pushToolResult: PushToolResult,
21+
) {
22+
switch (true) {
23+
default:
24+
const relPath: string | undefined = block.params.path
25+
const startLineStr: string | undefined = block.params.start_line
26+
const endLineStr: string | undefined = block.params.end_line
27+
28+
// Get the full path and determine if it's outside the workspace
29+
const fullPath = relPath ? path.resolve(cline.cwd, relPath) : ""
30+
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
31+
32+
const sharedMessageProps: ClineSayTool = {
33+
tool: "readFile",
34+
path: getReadablePath(cline.cwd, relPath),
35+
isOutsideWorkspace,
36+
}
37+
try {
38+
if (block.partial) {
39+
const partialMessage = JSON.stringify({
40+
...sharedMessageProps,
41+
content: undefined,
42+
} satisfies ClineSayTool)
43+
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
44+
break
45+
} else {
46+
if (!relPath) {
47+
cline.consecutiveMistakeCount++
48+
pushToolResult(await cline.sayAndCreateMissingParamError("read_file", "path"))
49+
break
50+
}
51+
52+
// Check if we're doing a line range read
53+
let isRangeRead = false
54+
let startLine: number | undefined = undefined
55+
let endLine: number | undefined = undefined
56+
57+
// Check if we have either range parameter
58+
if (startLineStr || endLineStr) {
59+
isRangeRead = true
60+
}
61+
62+
// Parse start_line if provided
63+
if (startLineStr) {
64+
startLine = parseInt(startLineStr)
65+
if (isNaN(startLine)) {
66+
// Invalid start_line
67+
cline.consecutiveMistakeCount++
68+
await cline.say("error", `Failed to parse start_line: `)
69+
pushToolResult(formatResponse.toolError("Invalid start_line value"))
70+
break
71+
}
72+
startLine -= 1 // Convert to 0-based index
73+
}
74+
75+
// Parse end_line if provided
76+
if (endLineStr) {
77+
endLine = parseInt(endLineStr)
78+
79+
if (isNaN(endLine)) {
80+
// Invalid end_line
81+
cline.consecutiveMistakeCount++
82+
await cline.say("error", `Failed to parse end_line: `)
83+
pushToolResult(formatResponse.toolError("Invalid end_line value"))
84+
break
85+
}
86+
87+
// Convert to 0-based index
88+
endLine -= 1
89+
}
90+
91+
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
92+
if (!accessAllowed) {
93+
await cline.say("rooignore_error", relPath)
94+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
95+
break
96+
}
97+
98+
cline.consecutiveMistakeCount = 0
99+
const absolutePath = path.resolve(cline.cwd, relPath)
100+
const completeMessage = JSON.stringify({
101+
...sharedMessageProps,
102+
content: absolutePath,
103+
} satisfies ClineSayTool)
104+
105+
const didApprove = await askApproval("tool", completeMessage)
106+
if (!didApprove) {
107+
break
108+
}
109+
110+
// Get the maxReadFileLine setting
111+
const { maxReadFileLine = 500 } = (await cline.providerRef.deref()?.getState()) ?? {}
112+
113+
// Count total lines in the file
114+
let totalLines = 0
115+
try {
116+
totalLines = await countFileLines(absolutePath)
117+
} catch (error) {
118+
console.error(`Error counting lines in file :`, error)
119+
}
120+
121+
// now execute the tool like normal
122+
let content: string
123+
let isFileTruncated = false
124+
let sourceCodeDef = ""
125+
126+
const isBinary = await isBinaryFile(absolutePath).catch(() => false)
127+
128+
if (isRangeRead) {
129+
if (startLine === undefined) {
130+
content = addLineNumbers(await readLines(absolutePath, endLine, startLine))
131+
} else {
132+
content = addLineNumbers(await readLines(absolutePath, endLine, startLine), startLine + 1)
133+
}
134+
} else if (!isBinary && maxReadFileLine >= 0 && totalLines > maxReadFileLine) {
135+
// If file is too large, only read the first maxReadFileLine lines
136+
isFileTruncated = true
137+
138+
const res = await Promise.all([
139+
maxReadFileLine > 0 ? readLines(absolutePath, maxReadFileLine - 1, 0) : "",
140+
parseSourceCodeDefinitionsForFile(absolutePath, cline.rooIgnoreController),
141+
])
142+
143+
content = res[0].length > 0 ? addLineNumbers(res[0]) : ""
144+
const result = res[1]
145+
if (result) {
146+
sourceCodeDef = `\n\n`
147+
}
148+
} else {
149+
// Read entire file
150+
content = await extractTextFromFile(absolutePath)
151+
}
152+
153+
// Add truncation notice if applicable
154+
if (isFileTruncated) {
155+
content += `\n\n[Showing only ${maxReadFileLine} of ${totalLines} total lines. Use start_line and end_line if you need to read more]`
156+
}
157+
158+
pushToolResult(content)
159+
break
160+
}
161+
} catch (error) {
162+
await handleError("reading file", error)
163+
break
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)