mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge branch 'main' of https://github.com/RooVetGit/Roo-Cline into feature/pupeteer-updates
This commit is contained in:
commit
4d53bd2104
8 changed files with 784 additions and 8 deletions
92
.github/workflows/codeql.yml
vendored
Normal file
92
.github/workflows/codeql.yml
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '24 19 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
# Roo Cline Changelog
|
||||
|
||||
## [2.1.17]
|
||||
|
||||
- Switch to search/replace diffs in experimental diff editing mode
|
||||
|
||||
## [2.1.16]
|
||||
|
||||
- Allow copying prompts from the history screen
|
||||
|
||||
## [2.1.15]
|
||||
|
||||
- Incorporate dbasclpy's [PR](https://github.com/RooVetGit/Roo-Cline/pull/54) to add support for gemini-exp-1206
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ A fork of Cline, an autonomous coding agent, with some added experimental config
|
|||
- Auto-approval capabilities for commands, write, and browser operations
|
||||
- Support for .clinerules per-project custom instructions
|
||||
- Ability to run side-by-side with Cline
|
||||
- Code is unit-tested
|
||||
- Unit test coverage (written almost entirely by Roo Cline!)
|
||||
- Support for playing sound effects
|
||||
- Support for OpenRouter compression
|
||||
- Support for editing through diffs (very experimental)
|
||||
- Support for gemini-exp-1206
|
||||
- Support for copying prompts from the history screen
|
||||
|
||||
Here's an example of Roo-Cline autonomously creating a snake game with "Always approve write operations" and "Always approve browser actions" turned on:
|
||||
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "roo-cline",
|
||||
"version": "2.1.15",
|
||||
"version": "2.1.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "roo-cline",
|
||||
"version": "2.1.15",
|
||||
"version": "2.1.17",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
"@anthropic-ai/sdk": "^0.26.0",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"displayName": "Roo Cline",
|
||||
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
||||
"publisher": "RooVeterinaryInc",
|
||||
"version": "2.1.16",
|
||||
"version": "2.1.17",
|
||||
"icon": "assets/icons/rocket.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import type { DiffStrategy } from './types'
|
||||
import { UnifiedDiffStrategy } from './strategies/unified'
|
||||
|
||||
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
|
||||
/**
|
||||
* Get the appropriate diff strategy for the given model
|
||||
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
|
||||
* @returns The appropriate diff strategy for the model
|
||||
*/
|
||||
export function getDiffStrategy(model: string): DiffStrategy {
|
||||
// For now, return UnifiedDiffStrategy for all models
|
||||
// For now, return SearchReplaceDiffStrategy for all models
|
||||
// This architecture allows for future optimizations based on model capabilities
|
||||
return new UnifiedDiffStrategy()
|
||||
return new SearchReplaceDiffStrategy()
|
||||
}
|
||||
|
||||
export type { DiffStrategy }
|
||||
export { UnifiedDiffStrategy }
|
||||
export { UnifiedDiffStrategy, SearchReplaceDiffStrategy }
|
||||
|
|
|
|||
504
src/core/diff/strategies/__tests__/search-replace.test.ts
Normal file
504
src/core/diff/strategies/__tests__/search-replace.test.ts
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
import { SearchReplaceDiffStrategy } from '../search-replace'
|
||||
|
||||
describe('SearchReplaceDiffStrategy', () => {
|
||||
let strategy: SearchReplaceDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new SearchReplaceDiffStrategy()
|
||||
})
|
||||
|
||||
describe('applyDiff', () => {
|
||||
it('should replace matching content', () => {
|
||||
const originalContent = `function hello() {
|
||||
console.log("hello")
|
||||
}
|
||||
`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
function hello() {
|
||||
console.log("hello")
|
||||
}
|
||||
=======
|
||||
function hello() {
|
||||
console.log("hello world")
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe(`function hello() {
|
||||
console.log("hello world")
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should handle extra whitespace in search/replace blocks', () => {
|
||||
const originalContent = `function test() {
|
||||
return true;
|
||||
}
|
||||
`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
|
||||
function test() {
|
||||
return true;
|
||||
}
|
||||
|
||||
=======
|
||||
function test() {
|
||||
return false;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe(`function test() {
|
||||
return false;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should match content with different surrounding whitespace', () => {
|
||||
const originalContent = `
|
||||
function example() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
function example() {
|
||||
return 42;
|
||||
}
|
||||
=======
|
||||
function example() {
|
||||
return 43;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe(`
|
||||
function example() {
|
||||
return 43;
|
||||
}
|
||||
|
||||
`)
|
||||
})
|
||||
|
||||
it('should match content with different indentation in search block', () => {
|
||||
const originalContent = ` function test() {
|
||||
return true;
|
||||
}
|
||||
`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
function test() {
|
||||
return true;
|
||||
}
|
||||
=======
|
||||
function test() {
|
||||
return false;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe(` function test() {
|
||||
return false;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should handle tab-based indentation', () => {
|
||||
const originalContent = "function test() {\n\treturn true;\n}\n"
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
function test() {
|
||||
\treturn true;
|
||||
}
|
||||
=======
|
||||
function test() {
|
||||
\treturn false;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe("function test() {\n\treturn false;\n}\n")
|
||||
})
|
||||
|
||||
it('should preserve mixed tabs and spaces', () => {
|
||||
const originalContent = "\tclass Example {\n\t constructor() {\n\t\tthis.value = 0;\n\t }\n\t}"
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
\tclass Example {
|
||||
\t constructor() {
|
||||
\t\tthis.value = 0;
|
||||
\t }
|
||||
\t}
|
||||
=======
|
||||
\tclass Example {
|
||||
\t constructor() {
|
||||
\t\tthis.value = 1;
|
||||
\t }
|
||||
\t}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe("\tclass Example {\n\t constructor() {\n\t\tthis.value = 1;\n\t }\n\t}")
|
||||
})
|
||||
|
||||
it('should handle additional indentation with tabs', () => {
|
||||
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
function test() {
|
||||
\treturn true;
|
||||
}
|
||||
=======
|
||||
function test() {
|
||||
\t// Add comment
|
||||
\treturn false;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe("\tfunction test() {\n\t\t// Add comment\n\t\treturn false;\n\t}")
|
||||
})
|
||||
|
||||
it('should preserve exact indentation characters when adding lines', () => {
|
||||
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
\tfunction test() {
|
||||
\t\treturn true;
|
||||
\t}
|
||||
=======
|
||||
\tfunction test() {
|
||||
\t\t// First comment
|
||||
\t\t// Second comment
|
||||
\t\treturn true;
|
||||
\t}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe("\tfunction test() {\n\t\t// First comment\n\t\t// Second comment\n\t\treturn true;\n\t}")
|
||||
})
|
||||
|
||||
it('should return false if search content does not match', () => {
|
||||
const originalContent = `function hello() {
|
||||
console.log("hello")
|
||||
}
|
||||
`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
function hello() {
|
||||
console.log("wrong")
|
||||
}
|
||||
=======
|
||||
function hello() {
|
||||
console.log("hello world")
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false if diff format is invalid', () => {
|
||||
const originalContent = `function hello() {
|
||||
console.log("hello")
|
||||
}
|
||||
`
|
||||
const diffContent = `test.ts
|
||||
Invalid diff format`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle multiple lines with proper indentation', () => {
|
||||
const originalContent = `class Example {
|
||||
constructor() {
|
||||
this.value = 0
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
getValue() {
|
||||
return this.value
|
||||
}
|
||||
=======
|
||||
getValue() {
|
||||
// Add logging
|
||||
console.log("Getting value")
|
||||
return this.value
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe(`class Example {
|
||||
constructor() {
|
||||
this.value = 0
|
||||
}
|
||||
|
||||
getValue() {
|
||||
// Add logging
|
||||
console.log("Getting value")
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should preserve whitespace exactly in the output', () => {
|
||||
const originalContent = " indented\n more indented\n back\n"
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
indented
|
||||
more indented
|
||||
back
|
||||
=======
|
||||
modified
|
||||
still indented
|
||||
end
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result).toBe(" modified\n still indented\n end\n")
|
||||
})
|
||||
|
||||
it('should handle complex refactoring with multiple functions', () => {
|
||||
const originalContent = `export async function extractTextFromFile(filePath: string): Promise<string> {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch (error) {
|
||||
throw new Error(\`File not found: \${filePath}\`)
|
||||
}
|
||||
const fileExtension = path.extname(filePath).toLowerCase()
|
||||
switch (fileExtension) {
|
||||
case ".pdf":
|
||||
return extractTextFromPDF(filePath)
|
||||
case ".docx":
|
||||
return extractTextFromDOCX(filePath)
|
||||
case ".ipynb":
|
||||
return extractTextFromIPYNB(filePath)
|
||||
default:
|
||||
const isBinary = await isBinaryFile(filePath).catch(() => false)
|
||||
if (!isBinary) {
|
||||
return addLineNumbers(await fs.readFile(filePath, "utf8"))
|
||||
} else {
|
||||
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addLineNumbers(content: string): string {
|
||||
const lines = content.split('\\n')
|
||||
const maxLineNumberWidth = String(lines.length).length
|
||||
return lines
|
||||
.map((line, index) => {
|
||||
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
|
||||
return \`\${lineNumber} | \${line}\`
|
||||
}).join('\\n')
|
||||
}`
|
||||
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
export async function extractTextFromFile(filePath: string): Promise<string> {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch (error) {
|
||||
throw new Error(\`File not found: \${filePath}\`)
|
||||
}
|
||||
const fileExtension = path.extname(filePath).toLowerCase()
|
||||
switch (fileExtension) {
|
||||
case ".pdf":
|
||||
return extractTextFromPDF(filePath)
|
||||
case ".docx":
|
||||
return extractTextFromDOCX(filePath)
|
||||
case ".ipynb":
|
||||
return extractTextFromIPYNB(filePath)
|
||||
default:
|
||||
const isBinary = await isBinaryFile(filePath).catch(() => false)
|
||||
if (!isBinary) {
|
||||
return addLineNumbers(await fs.readFile(filePath, "utf8"))
|
||||
} else {
|
||||
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addLineNumbers(content: string): string {
|
||||
const lines = content.split('\\n')
|
||||
const maxLineNumberWidth = String(lines.length).length
|
||||
return lines
|
||||
.map((line, index) => {
|
||||
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
|
||||
return \`\${lineNumber} | \${line}\`
|
||||
}).join('\\n')
|
||||
}
|
||||
=======
|
||||
function extractLineRange(content: string, startLine?: number, endLine?: number): string {
|
||||
const lines = content.split('\\n')
|
||||
const start = startLine ? Math.max(1, startLine) : 1
|
||||
const end = endLine ? Math.min(lines.length, endLine) : lines.length
|
||||
|
||||
if (start > end || start > lines.length) {
|
||||
throw new Error(\`Invalid line range: start=\${start}, end=\${end}, total lines=\${lines.length}\`)
|
||||
}
|
||||
|
||||
return lines.slice(start - 1, end).join('\\n')
|
||||
}
|
||||
|
||||
export async function extractTextFromFile(filePath: string, startLine?: number, endLine?: number): Promise<string> {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch (error) {
|
||||
throw new Error(\`File not found: \${filePath}\`)
|
||||
}
|
||||
const fileExtension = path.extname(filePath).toLowerCase()
|
||||
let content: string
|
||||
|
||||
switch (fileExtension) {
|
||||
case ".pdf": {
|
||||
const dataBuffer = await fs.readFile(filePath)
|
||||
const data = await pdf(dataBuffer)
|
||||
content = extractLineRange(data.text, startLine, endLine)
|
||||
break
|
||||
}
|
||||
case ".docx": {
|
||||
const result = await mammoth.extractRawText({ path: filePath })
|
||||
content = extractLineRange(result.value, startLine, endLine)
|
||||
break
|
||||
}
|
||||
case ".ipynb": {
|
||||
const data = await fs.readFile(filePath, "utf8")
|
||||
const notebook = JSON.parse(data)
|
||||
let extractedText = ""
|
||||
|
||||
for (const cell of notebook.cells) {
|
||||
if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
|
||||
extractedText += cell.source.join("\\n") + "\\n"
|
||||
}
|
||||
}
|
||||
content = extractLineRange(extractedText, startLine, endLine)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const isBinary = await isBinaryFile(filePath).catch(() => false)
|
||||
if (!isBinary) {
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
content = extractLineRange(fileContent, startLine, endLine)
|
||||
} else {
|
||||
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addLineNumbers(content, startLine)
|
||||
}
|
||||
|
||||
export function addLineNumbers(content: string, startLine: number = 1): string {
|
||||
const lines = content.split('\\n')
|
||||
const maxLineNumberWidth = String(startLine + lines.length - 1).length
|
||||
return lines
|
||||
.map((line, index) => {
|
||||
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
|
||||
return \`\${lineNumber} | \${line}\`
|
||||
}).join('\\n')
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const expected = `function extractLineRange(content: string, startLine?: number, endLine?: number): string {
|
||||
const lines = content.split('\\n')
|
||||
const start = startLine ? Math.max(1, startLine) : 1
|
||||
const end = endLine ? Math.min(lines.length, endLine) : lines.length
|
||||
|
||||
if (start > end || start > lines.length) {
|
||||
throw new Error(\`Invalid line range: start=\${start}, end=\${end}, total lines=\${lines.length}\`)
|
||||
}
|
||||
|
||||
return lines.slice(start - 1, end).join('\\n')
|
||||
}
|
||||
|
||||
export async function extractTextFromFile(filePath: string, startLine?: number, endLine?: number): Promise<string> {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch (error) {
|
||||
throw new Error(\`File not found: \${filePath}\`)
|
||||
}
|
||||
const fileExtension = path.extname(filePath).toLowerCase()
|
||||
let content: string
|
||||
|
||||
switch (fileExtension) {
|
||||
case ".pdf": {
|
||||
const dataBuffer = await fs.readFile(filePath)
|
||||
const data = await pdf(dataBuffer)
|
||||
content = extractLineRange(data.text, startLine, endLine)
|
||||
break
|
||||
}
|
||||
case ".docx": {
|
||||
const result = await mammoth.extractRawText({ path: filePath })
|
||||
content = extractLineRange(result.value, startLine, endLine)
|
||||
break
|
||||
}
|
||||
case ".ipynb": {
|
||||
const data = await fs.readFile(filePath, "utf8")
|
||||
const notebook = JSON.parse(data)
|
||||
let extractedText = ""
|
||||
|
||||
for (const cell of notebook.cells) {
|
||||
if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
|
||||
extractedText += cell.source.join("\\n") + "\\n"
|
||||
}
|
||||
}
|
||||
content = extractLineRange(extractedText, startLine, endLine)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const isBinary = await isBinaryFile(filePath).catch(() => false)
|
||||
if (!isBinary) {
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
content = extractLineRange(fileContent, startLine, endLine)
|
||||
} else {
|
||||
throw new Error(\`Cannot read text for file type: \${fileExtension}\`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addLineNumbers(content, startLine)
|
||||
}
|
||||
|
||||
export function addLineNumbers(content: string, startLine: number = 1): string {
|
||||
const lines = content.split('\\n')
|
||||
const maxLineNumberWidth = String(startLine + lines.length - 1).length
|
||||
return lines
|
||||
.map((line, index) => {
|
||||
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
|
||||
return \`\${lineNumber} | \${line}\`
|
||||
}).join('\\n')
|
||||
}`
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getToolDescription', () => {
|
||||
it('should include the current working directory', () => {
|
||||
const cwd = '/test/dir'
|
||||
const description = strategy.getToolDescription(cwd)
|
||||
expect(description).toContain(`relative to the current working directory ${cwd}`)
|
||||
})
|
||||
|
||||
it('should include required format elements', () => {
|
||||
const description = strategy.getToolDescription('/test')
|
||||
expect(description).toContain('<<<<<<< SEARCH')
|
||||
expect(description).toContain('=======')
|
||||
expect(description).toContain('>>>>>>> REPLACE')
|
||||
expect(description).toContain('<apply_diff>')
|
||||
expect(description).toContain('</apply_diff>')
|
||||
})
|
||||
})
|
||||
})
|
||||
171
src/core/diff/strategies/search-replace.ts
Normal file
171
src/core/diff/strategies/search-replace.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { DiffStrategy } from "../types"
|
||||
|
||||
export class SearchReplaceDiffStrategy implements DiffStrategy {
|
||||
getToolDescription(cwd: string): string {
|
||||
return `## apply_diff
|
||||
Description: Request to replace existing code using search and replace blocks.
|
||||
This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with.
|
||||
Only use this tool when you need to replace/fix existing code.
|
||||
The tool will maintain proper indentation and formatting while making changes.
|
||||
Only a single operation is allowed per tool use.
|
||||
The SEARCH section must exactly match existing content including whitespace and indentation.
|
||||
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
|
||||
- diff: (required) The search/replace blocks defining the changes.
|
||||
|
||||
Format:
|
||||
1. First line must be the file path
|
||||
2. Followed by search/replace blocks:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
[exact content to find including whitespace]
|
||||
=======
|
||||
[new content to replace with]
|
||||
>>>>>>> REPLACE
|
||||
\`\`\`
|
||||
|
||||
Example:
|
||||
|
||||
Original file:
|
||||
\`\`\`
|
||||
def calculate_total(items):
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item
|
||||
return total
|
||||
\`\`\`
|
||||
|
||||
Search/Replace content:
|
||||
\`\`\`
|
||||
main.py
|
||||
<<<<<<< SEARCH
|
||||
def calculate_total(items):
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item
|
||||
return total
|
||||
=======
|
||||
def calculate_total(items):
|
||||
"""Calculate total with 10% markup"""
|
||||
return sum(item * 1.1 for item in items)
|
||||
>>>>>>> REPLACE
|
||||
\`\`\`
|
||||
|
||||
Usage:
|
||||
<apply_diff>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
Your search/replace content here
|
||||
</diff>
|
||||
</apply_diff>`
|
||||
}
|
||||
|
||||
applyDiff(originalContent: string, diffContent: string): string | false {
|
||||
// Extract the search and replace blocks
|
||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [_, searchContent, replaceContent] = match;
|
||||
|
||||
// Split content into lines
|
||||
const searchLines = searchContent.trim().split('\n');
|
||||
const replaceLines = replaceContent.trim().split('\n');
|
||||
const originalLines = originalContent.split('\n');
|
||||
|
||||
// Find the search content in the original
|
||||
let matchIndex = -1;
|
||||
|
||||
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
||||
let found = true;
|
||||
|
||||
for (let j = 0; j < searchLines.length; j++) {
|
||||
const originalLine = originalLines[i + j];
|
||||
const searchLine = searchLines[j];
|
||||
|
||||
// Compare lines after removing leading/trailing whitespace
|
||||
if (originalLine.trim() !== searchLine.trim()) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
matchIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the matched lines from the original content
|
||||
const matchedLines = originalLines.slice(matchIndex, matchIndex + searchLines.length);
|
||||
|
||||
// Get the exact indentation (preserving tabs/spaces) of each line
|
||||
const originalIndents = matchedLines.map(line => {
|
||||
const match = line.match(/^[\t ]*/);
|
||||
return match ? match[0] : '';
|
||||
});
|
||||
|
||||
// Get the exact indentation of each line in the search block
|
||||
const searchIndents = searchLines.map(line => {
|
||||
const match = line.match(/^[\t ]*/);
|
||||
return match ? match[0] : '';
|
||||
});
|
||||
|
||||
// Apply the replacement while preserving exact indentation
|
||||
const indentedReplace = replaceLines.map((line, i) => {
|
||||
// Get the corresponding original and search indentations
|
||||
const originalIndent = originalIndents[Math.min(i, originalIndents.length - 1)];
|
||||
const searchIndent = searchIndents[Math.min(i, searchIndents.length - 1)];
|
||||
|
||||
// Get the current line's indentation
|
||||
const currentIndentMatch = line.match(/^[\t ]*/);
|
||||
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : '';
|
||||
|
||||
// If this line has the same indentation level as the search block,
|
||||
// use the original indentation. Otherwise, calculate the difference
|
||||
// and preserve the exact type of whitespace characters
|
||||
if (currentIndent.length === searchIndent.length) {
|
||||
return originalIndent + line.trim();
|
||||
} else {
|
||||
// Get the corresponding search line's indentation
|
||||
const searchLineIndex = Math.min(i, searchLines.length - 1);
|
||||
const searchLineIndent = searchIndents[searchLineIndex];
|
||||
|
||||
// Get the corresponding original line's indentation
|
||||
const originalLineIndex = Math.min(i, originalIndents.length - 1);
|
||||
const originalLineIndent = originalIndents[originalLineIndex];
|
||||
|
||||
// If this line has the same indentation as its corresponding search line,
|
||||
// use the original indentation
|
||||
if (currentIndent === searchLineIndent) {
|
||||
return originalLineIndent + line.trim();
|
||||
}
|
||||
|
||||
// Otherwise, preserve the original indentation structure
|
||||
const indentChar = originalLineIndent.charAt(0) || '\t';
|
||||
const indentLevel = Math.floor(originalLineIndent.length / indentChar.length);
|
||||
|
||||
// Calculate the relative indentation from the search line
|
||||
const searchLevel = Math.floor(searchLineIndent.length / indentChar.length);
|
||||
const currentLevel = Math.floor(currentIndent.length / indentChar.length);
|
||||
const relativeLevel = currentLevel - searchLevel;
|
||||
|
||||
// Apply the relative indentation to the original level
|
||||
const targetLevel = Math.max(0, indentLevel + relativeLevel);
|
||||
return indentChar.repeat(targetLevel) + line.trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Construct the final content
|
||||
const beforeMatch = originalLines.slice(0, matchIndex);
|
||||
const afterMatch = originalLines.slice(matchIndex + searchLines.length);
|
||||
|
||||
return [...beforeMatch, ...indentedReplace, ...afterMatch].join('\n');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue