mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge pull request #364 from daniel-lxs/new_unified
New unified edit strategy
This commit is contained in:
107
package-lock.json
generated
107
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||||
"@types/clone-deep": "^4.0.4",
|
"@types/clone-deep": "^4.0.4",
|
||||||
"@types/pdf-parse": "^1.1.4",
|
"@types/pdf-parse": "^1.1.4",
|
||||||
|
"@types/tmp": "^0.2.6",
|
||||||
"@types/turndown": "^5.0.5",
|
"@types/turndown": "^5.0.5",
|
||||||
"@types/vscode": "^1.95.0",
|
"@types/vscode": "^1.95.0",
|
||||||
"@vscode/codicons": "^0.0.36",
|
"@vscode/codicons": "^0.0.36",
|
||||||
@@ -27,7 +28,9 @@
|
|||||||
"default-shell": "^2.2.0",
|
"default-shell": "^2.2.0",
|
||||||
"delay": "^6.0.0",
|
"delay": "^6.0.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
|
"diff-match-patch": "^1.0.5",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
"isbinaryfile": "^5.0.2",
|
"isbinaryfile": "^5.0.2",
|
||||||
"mammoth": "^1.8.0",
|
"mammoth": "^1.8.0",
|
||||||
@@ -39,8 +42,11 @@
|
|||||||
"puppeteer-chromium-resolver": "^23.0.0",
|
"puppeteer-chromium-resolver": "^23.0.0",
|
||||||
"puppeteer-core": "^23.4.0",
|
"puppeteer-core": "^23.4.0",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
|
"simple-git": "^3.27.0",
|
||||||
"sound-play": "^1.1.0",
|
"sound-play": "^1.1.0",
|
||||||
|
"string-similarity": "^4.0.4",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
|
"tmp": "^0.2.3",
|
||||||
"tree-sitter-wasms": "^0.1.11",
|
"tree-sitter-wasms": "^0.1.11",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"web-tree-sitter": "^0.22.6",
|
"web-tree-sitter": "^0.22.6",
|
||||||
@@ -50,9 +56,11 @@
|
|||||||
"@changesets/cli": "^2.27.10",
|
"@changesets/cli": "^2.27.10",
|
||||||
"@changesets/types": "^6.0.0",
|
"@changesets/types": "^6.0.0",
|
||||||
"@types/diff": "^5.2.1",
|
"@types/diff": "^5.2.1",
|
||||||
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/node": "20.x",
|
"@types/node": "20.x",
|
||||||
|
"@types/string-similarity": "^4.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||||
"@typescript-eslint/parser": "^7.11.0",
|
"@typescript-eslint/parser": "^7.11.0",
|
||||||
"@vscode/test-cli": "^0.0.9",
|
"@vscode/test-cli": "^0.0.9",
|
||||||
@@ -4108,6 +4116,21 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kwsites/file-exists": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@kwsites/promise-deferred": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@manypkg/find-root": {
|
"node_modules/@manypkg/find-root": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz",
|
||||||
@@ -6069,6 +6092,13 @@
|
|||||||
"integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==",
|
"integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/diff-match-patch": {
|
||||||
|
"version": "1.0.36",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
|
||||||
|
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/graceful-fs": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||||
@@ -6146,6 +6176,19 @@
|
|||||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/string-similarity": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/string-similarity/-/string-similarity-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LkJQ/jsXtCVMK+sKYAmX/8zEq+/46f1PTQw7YtmQwb74jemS1SlNLmARM2Zml9DgdDTWKAtc5L13WorpHPDjDA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/tmp": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/turndown": {
|
"node_modules/@types/turndown": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz",
|
||||||
@@ -7913,6 +7956,12 @@
|
|||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/diff-match-patch": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/diff-sequences": {
|
"node_modules/diff-sequences": {
|
||||||
"version": "29.6.3",
|
"version": "29.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
||||||
@@ -8716,6 +8765,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/external-editor/node_modules/tmp": {
|
||||||
|
"version": "0.0.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||||
|
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"os-tmpdir": "~1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/extract-zip": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
@@ -8807,6 +8869,15 @@
|
|||||||
"fxparser": "src/cli/cli.js"
|
"fxparser": "src/cli/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fastest-levenshtein": {
|
||||||
|
"version": "1.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
|
||||||
|
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||||
@@ -12720,6 +12791,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||||
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
|
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -13926,6 +13998,21 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-git": {
|
||||||
|
"version": "3.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz",
|
||||||
|
"integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kwsites/file-exists": "^1.1.1",
|
||||||
|
"@kwsites/promise-deferred": "^1.1.1",
|
||||||
|
"debug": "^4.3.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/steveukx/git-js?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sisteransi": {
|
"node_modules/sisteransi": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
@@ -14202,6 +14289,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-similarity": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
@@ -14544,15 +14638,12 @@
|
|||||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
||||||
},
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.0.33",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||||
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
|
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"os-tmpdir": "~1.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6.0"
|
"node": ">=14.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tmpl": {
|
"node_modules/tmpl": {
|
||||||
|
|||||||
@@ -202,9 +202,11 @@
|
|||||||
"@changesets/cli": "^2.27.10",
|
"@changesets/cli": "^2.27.10",
|
||||||
"@changesets/types": "^6.0.0",
|
"@changesets/types": "^6.0.0",
|
||||||
"@types/diff": "^5.2.1",
|
"@types/diff": "^5.2.1",
|
||||||
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/node": "20.x",
|
"@types/node": "20.x",
|
||||||
|
"@types/string-similarity": "^4.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||||
"@typescript-eslint/parser": "^7.11.0",
|
"@typescript-eslint/parser": "^7.11.0",
|
||||||
"@vscode/test-cli": "^0.0.9",
|
"@vscode/test-cli": "^0.0.9",
|
||||||
@@ -230,6 +232,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||||
"@types/clone-deep": "^4.0.4",
|
"@types/clone-deep": "^4.0.4",
|
||||||
"@types/pdf-parse": "^1.1.4",
|
"@types/pdf-parse": "^1.1.4",
|
||||||
|
"@types/tmp": "^0.2.6",
|
||||||
"@types/turndown": "^5.0.5",
|
"@types/turndown": "^5.0.5",
|
||||||
"@types/vscode": "^1.95.0",
|
"@types/vscode": "^1.95.0",
|
||||||
"@vscode/codicons": "^0.0.36",
|
"@vscode/codicons": "^0.0.36",
|
||||||
@@ -240,7 +243,9 @@
|
|||||||
"default-shell": "^2.2.0",
|
"default-shell": "^2.2.0",
|
||||||
"delay": "^6.0.0",
|
"delay": "^6.0.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
|
"diff-match-patch": "^1.0.5",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
"isbinaryfile": "^5.0.2",
|
"isbinaryfile": "^5.0.2",
|
||||||
"mammoth": "^1.8.0",
|
"mammoth": "^1.8.0",
|
||||||
@@ -252,8 +257,11 @@
|
|||||||
"puppeteer-chromium-resolver": "^23.0.0",
|
"puppeteer-chromium-resolver": "^23.0.0",
|
||||||
"puppeteer-core": "^23.4.0",
|
"puppeteer-core": "^23.4.0",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
|
"simple-git": "^3.27.0",
|
||||||
"sound-play": "^1.1.0",
|
"sound-play": "^1.1.0",
|
||||||
|
"string-similarity": "^4.0.4",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
|
"tmp": "^0.2.3",
|
||||||
"tree-sitter-wasms": "^0.1.11",
|
"tree-sitter-wasms": "^0.1.11",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"web-tree-sitter": "^0.22.6",
|
"web-tree-sitter": "^0.22.6",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { detectCodeOmission } from "../integrations/editor/detect-omission"
|
|||||||
import { BrowserSession } from "../services/browser/BrowserSession"
|
import { BrowserSession } from "../services/browser/BrowserSession"
|
||||||
import { OpenRouterHandler } from "../api/providers/openrouter"
|
import { OpenRouterHandler } from "../api/providers/openrouter"
|
||||||
import { McpHub } from "../services/mcp/McpHub"
|
import { McpHub } from "../services/mcp/McpHub"
|
||||||
|
import crypto from "crypto"
|
||||||
|
|
||||||
const cwd =
|
const cwd =
|
||||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||||
@@ -71,6 +72,7 @@ export class Cline {
|
|||||||
customInstructions?: string
|
customInstructions?: string
|
||||||
diffStrategy?: DiffStrategy
|
diffStrategy?: DiffStrategy
|
||||||
diffEnabled: boolean = false
|
diffEnabled: boolean = false
|
||||||
|
fuzzyMatchThreshold: number = 1.0
|
||||||
|
|
||||||
apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
|
apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
|
||||||
clineMessages: ClineMessage[] = []
|
clineMessages: ClineMessage[] = []
|
||||||
@@ -105,28 +107,46 @@ export class Cline {
|
|||||||
fuzzyMatchThreshold?: number,
|
fuzzyMatchThreshold?: number,
|
||||||
task?: string | undefined,
|
task?: string | undefined,
|
||||||
images?: string[] | undefined,
|
images?: string[] | undefined,
|
||||||
historyItem?: HistoryItem | undefined
|
historyItem?: HistoryItem | undefined,
|
||||||
|
experimentalDiffStrategy: boolean = false,
|
||||||
) {
|
) {
|
||||||
this.providerRef = new WeakRef(provider)
|
if (!task && !images && !historyItem) {
|
||||||
|
throw new Error('Either historyItem or task/images must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.taskId = crypto.randomUUID()
|
||||||
this.api = buildApiHandler(apiConfiguration)
|
this.api = buildApiHandler(apiConfiguration)
|
||||||
this.terminalManager = new TerminalManager()
|
this.terminalManager = new TerminalManager()
|
||||||
this.urlContentFetcher = new UrlContentFetcher(provider.context)
|
this.urlContentFetcher = new UrlContentFetcher(provider.context)
|
||||||
this.browserSession = new BrowserSession(provider.context)
|
this.browserSession = new BrowserSession(provider.context)
|
||||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
|
||||||
this.customInstructions = customInstructions
|
this.customInstructions = customInstructions
|
||||||
this.diffEnabled = enableDiff ?? false
|
this.diffEnabled = enableDiff ?? false
|
||||||
if (this.diffEnabled && this.api.getModel().id) {
|
this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
|
||||||
this.diffStrategy = getDiffStrategy(this.api.getModel().id, fuzzyMatchThreshold ?? 1.0)
|
this.providerRef = new WeakRef(provider)
|
||||||
}
|
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||||
|
|
||||||
if (historyItem) {
|
if (historyItem) {
|
||||||
this.taskId = historyItem.id
|
this.taskId = historyItem.id
|
||||||
this.resumeTaskFromHistory()
|
|
||||||
} else if (task || images) {
|
|
||||||
this.taskId = Date.now().toString()
|
|
||||||
this.startTask(task, images)
|
|
||||||
} else {
|
|
||||||
throw new Error("Either historyItem or task/images must be provided")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize diffStrategy based on current state
|
||||||
|
this.updateDiffStrategy(experimentalDiffStrategy)
|
||||||
|
|
||||||
|
if (task || images) {
|
||||||
|
this.startTask(task, images)
|
||||||
|
} else if (historyItem) {
|
||||||
|
this.resumeTaskFromHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to update diffStrategy
|
||||||
|
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
|
||||||
|
// If not provided, get from current state
|
||||||
|
if (experimentalDiffStrategy === undefined) {
|
||||||
|
const { experimentalDiffStrategy: stateExperimentalDiffStrategy } = await this.providerRef.deref()?.getState() ?? {}
|
||||||
|
experimentalDiffStrategy = stateExperimentalDiffStrategy ?? false
|
||||||
|
}
|
||||||
|
this.diffStrategy = getDiffStrategy(this.api.getModel().id, this.fuzzyMatchThreshold, experimentalDiffStrategy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storing task to disk for history
|
// Storing task to disk for history
|
||||||
@@ -1326,7 +1346,7 @@ export class Cline {
|
|||||||
const originalContent = await fs.readFile(absolutePath, "utf-8")
|
const originalContent = await fs.readFile(absolutePath, "utf-8")
|
||||||
|
|
||||||
// Apply the diff to the original content
|
// Apply the diff to the original content
|
||||||
const diffResult = this.diffStrategy?.applyDiff(
|
const diffResult = await this.diffStrategy?.applyDiff(
|
||||||
originalContent,
|
originalContent,
|
||||||
diffContent,
|
diffContent,
|
||||||
parseInt(block.params.start_line ?? ''),
|
parseInt(block.params.start_line ?? ''),
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ describe('Cline', () => {
|
|||||||
|
|
||||||
expect(cline.diffEnabled).toBe(true);
|
expect(cline.diffEnabled).toBe(true);
|
||||||
expect(cline.diffStrategy).toBeDefined();
|
expect(cline.diffStrategy).toBeDefined();
|
||||||
expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 0.9);
|
expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 0.9, false);
|
||||||
|
|
||||||
getDiffStrategySpy.mockRestore();
|
getDiffStrategySpy.mockRestore();
|
||||||
});
|
});
|
||||||
@@ -341,7 +341,7 @@ describe('Cline', () => {
|
|||||||
|
|
||||||
expect(cline.diffEnabled).toBe(true);
|
expect(cline.diffEnabled).toBe(true);
|
||||||
expect(cline.diffStrategy).toBeDefined();
|
expect(cline.diffStrategy).toBeDefined();
|
||||||
expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 1.0);
|
expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 1.0, false);
|
||||||
|
|
||||||
getDiffStrategySpy.mockRestore();
|
getDiffStrategySpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import type { DiffStrategy } from './types'
|
import type { DiffStrategy } from './types'
|
||||||
import { UnifiedDiffStrategy } from './strategies/unified'
|
import { UnifiedDiffStrategy } from './strategies/unified'
|
||||||
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
|
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
|
||||||
|
import { NewUnifiedDiffStrategy } from './strategies/new-unified'
|
||||||
/**
|
/**
|
||||||
* Get the appropriate diff strategy for the given model
|
* 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')
|
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
|
||||||
* @returns The appropriate diff strategy for the model
|
* @returns The appropriate diff strategy for the model
|
||||||
*/
|
*/
|
||||||
export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number): DiffStrategy {
|
export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number, experimentalDiffStrategy: boolean = false): DiffStrategy {
|
||||||
// For now, return SearchReplaceDiffStrategy for all models
|
if (experimentalDiffStrategy) {
|
||||||
// This architecture allows for future optimizations based on model capabilities
|
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
|
||||||
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold ?? 1.0)
|
}
|
||||||
|
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { DiffStrategy }
|
export type { DiffStrategy }
|
||||||
|
|||||||
739
src/core/diff/strategies/__tests__/new-unified.test.ts
Normal file
739
src/core/diff/strategies/__tests__/new-unified.test.ts
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
import { NewUnifiedDiffStrategy } from '../new-unified';
|
||||||
|
|
||||||
|
describe('main', () => {
|
||||||
|
|
||||||
|
let strategy: NewUnifiedDiffStrategy
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
strategy = new NewUnifiedDiffStrategy(0.97)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should use default confidence threshold when not provided', () => {
|
||||||
|
const defaultStrategy = new NewUnifiedDiffStrategy()
|
||||||
|
expect(defaultStrategy['confidenceThreshold']).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use provided confidence threshold', () => {
|
||||||
|
const customStrategy = new NewUnifiedDiffStrategy(0.85)
|
||||||
|
expect(customStrategy['confidenceThreshold']).toBe(0.85)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should enforce minimum confidence threshold', () => {
|
||||||
|
const lowStrategy = new NewUnifiedDiffStrategy(0.7) // Below minimum of 0.8
|
||||||
|
expect(lowStrategy['confidenceThreshold']).toBe(0.8)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getToolDescription', () => {
|
||||||
|
it('should return tool description with correct cwd', () => {
|
||||||
|
const cwd = '/test/path'
|
||||||
|
const description = strategy.getToolDescription(cwd)
|
||||||
|
|
||||||
|
expect(description).toContain('apply_diff')
|
||||||
|
expect(description).toContain(cwd)
|
||||||
|
expect(description).toContain('Parameters:')
|
||||||
|
expect(description).toContain('Format Requirements:')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply simple diff correctly', async () => {
|
||||||
|
const original = `line1
|
||||||
|
line2
|
||||||
|
line3`;
|
||||||
|
|
||||||
|
const diff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
line1
|
||||||
|
+new line
|
||||||
|
line2
|
||||||
|
-line3
|
||||||
|
+modified line3`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if(result.success) {
|
||||||
|
expect(result.content).toBe(`line1
|
||||||
|
new line
|
||||||
|
line2
|
||||||
|
modified line3`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple hunks', async () => {
|
||||||
|
const original = `line1
|
||||||
|
line2
|
||||||
|
line3
|
||||||
|
line4
|
||||||
|
line5`;
|
||||||
|
|
||||||
|
const diff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
line1
|
||||||
|
+new line
|
||||||
|
line2
|
||||||
|
-line3
|
||||||
|
+modified line3
|
||||||
|
@@ ... @@
|
||||||
|
line4
|
||||||
|
-line5
|
||||||
|
+modified line5
|
||||||
|
+new line at end`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`line1
|
||||||
|
new line
|
||||||
|
line2
|
||||||
|
modified line3
|
||||||
|
line4
|
||||||
|
modified line5
|
||||||
|
new line at end`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex large', async () => {
|
||||||
|
const original = `line1
|
||||||
|
line2
|
||||||
|
line3
|
||||||
|
line4
|
||||||
|
line5
|
||||||
|
line6
|
||||||
|
line7
|
||||||
|
line8
|
||||||
|
line9
|
||||||
|
line10`;
|
||||||
|
|
||||||
|
const diff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
line1
|
||||||
|
+header line
|
||||||
|
+another header
|
||||||
|
line2
|
||||||
|
-line3
|
||||||
|
-line4
|
||||||
|
+modified line3
|
||||||
|
+modified line4
|
||||||
|
+extra line
|
||||||
|
@@ ... @@
|
||||||
|
line6
|
||||||
|
+middle section
|
||||||
|
line7
|
||||||
|
-line8
|
||||||
|
+changed line8
|
||||||
|
+bonus line
|
||||||
|
@@ ... @@
|
||||||
|
line9
|
||||||
|
-line10
|
||||||
|
+final line
|
||||||
|
+very last line`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`line1
|
||||||
|
header line
|
||||||
|
another header
|
||||||
|
line2
|
||||||
|
modified line3
|
||||||
|
modified line4
|
||||||
|
extra line
|
||||||
|
line5
|
||||||
|
line6
|
||||||
|
middle section
|
||||||
|
line7
|
||||||
|
changed line8
|
||||||
|
bonus line
|
||||||
|
line9
|
||||||
|
final line
|
||||||
|
very last line`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle indentation changes', async () => {
|
||||||
|
const original = `first line
|
||||||
|
indented line
|
||||||
|
double indented line
|
||||||
|
back to single indent
|
||||||
|
no indent
|
||||||
|
indented again
|
||||||
|
double indent again
|
||||||
|
triple indent
|
||||||
|
back to single
|
||||||
|
last line`;
|
||||||
|
|
||||||
|
const diff = `--- original
|
||||||
|
+++ modified
|
||||||
|
@@ ... @@
|
||||||
|
first line
|
||||||
|
indented line
|
||||||
|
+ tab indented line
|
||||||
|
+ new indented line
|
||||||
|
double indented line
|
||||||
|
back to single indent
|
||||||
|
no indent
|
||||||
|
indented again
|
||||||
|
double indent again
|
||||||
|
- triple indent
|
||||||
|
+ hi there mate
|
||||||
|
back to single
|
||||||
|
last line`;
|
||||||
|
|
||||||
|
const expected = `first line
|
||||||
|
indented line
|
||||||
|
tab indented line
|
||||||
|
new indented line
|
||||||
|
double indented line
|
||||||
|
back to single indent
|
||||||
|
no indent
|
||||||
|
indented again
|
||||||
|
double indent again
|
||||||
|
hi there mate
|
||||||
|
back to single
|
||||||
|
last line`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle high level edits', async () => {
|
||||||
|
|
||||||
|
const original = `def factorial(n):
|
||||||
|
if n == 0:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return n * factorial(n-1)`
|
||||||
|
const diff = `@@ ... @@
|
||||||
|
-def factorial(n):
|
||||||
|
- if n == 0:
|
||||||
|
- return 1
|
||||||
|
- else:
|
||||||
|
- return n * factorial(n-1)
|
||||||
|
+def factorial(number):
|
||||||
|
+ if number == 0:
|
||||||
|
+ return 1
|
||||||
|
+ else:
|
||||||
|
+ return number * factorial(number-1)`
|
||||||
|
|
||||||
|
const expected = `def factorial(number):
|
||||||
|
if number == 0:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return number * factorial(number-1)`
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it should handle very complex edits', async () => {
|
||||||
|
const original = `//Initialize the array that will hold the primes
|
||||||
|
var primeArray = [];
|
||||||
|
/*Write a function that checks for primeness and
|
||||||
|
pushes those values to t*he array*/
|
||||||
|
function PrimeCheck(candidate){
|
||||||
|
isPrime = true;
|
||||||
|
for(var i = 2; i < candidate && isPrime; i++){
|
||||||
|
if(candidate%i === 0){
|
||||||
|
isPrime = false;
|
||||||
|
} else {
|
||||||
|
isPrime = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(isPrime){
|
||||||
|
primeArray.push(candidate);
|
||||||
|
}
|
||||||
|
return primeArray;
|
||||||
|
}
|
||||||
|
/*Write the code that runs the above until the
|
||||||
|
l ength of the array equa*ls the number of primes
|
||||||
|
desired*/
|
||||||
|
|
||||||
|
var numPrimes = prompt("How many primes?");
|
||||||
|
|
||||||
|
//Display the finished array of primes
|
||||||
|
|
||||||
|
//for loop starting at 2 as that is the lowest prime number keep going until the array is as long as we requested
|
||||||
|
for (var i = 2; primeArray.length < numPrimes; i++) {
|
||||||
|
PrimeCheck(i); //
|
||||||
|
}
|
||||||
|
console.log(primeArray);
|
||||||
|
`
|
||||||
|
|
||||||
|
const diff = `--- test_diff.js
|
||||||
|
+++ test_diff.js
|
||||||
|
@@ ... @@
|
||||||
|
-//Initialize the array that will hold the primes
|
||||||
|
var primeArray = [];
|
||||||
|
-/*Write a function that checks for primeness and
|
||||||
|
- pushes those values to t*he array*/
|
||||||
|
function PrimeCheck(candidate){
|
||||||
|
isPrime = true;
|
||||||
|
for(var i = 2; i < candidate && isPrime; i++){
|
||||||
|
@@ ... @@
|
||||||
|
return primeArray;
|
||||||
|
}
|
||||||
|
-/*Write the code that runs the above until the
|
||||||
|
- l ength of the array equa*ls the number of primes
|
||||||
|
- desired*/
|
||||||
|
|
||||||
|
var numPrimes = prompt("How many primes?");
|
||||||
|
|
||||||
|
-//Display the finished array of primes
|
||||||
|
-
|
||||||
|
-//for loop starting at 2 as that is the lowest prime number keep going until the array is as long as we requested
|
||||||
|
for (var i = 2; primeArray.length < numPrimes; i++) {
|
||||||
|
- PrimeCheck(i); //
|
||||||
|
+ PrimeCheck(i);
|
||||||
|
}
|
||||||
|
console.log(primeArray);`
|
||||||
|
|
||||||
|
const expected = `var primeArray = [];
|
||||||
|
function PrimeCheck(candidate){
|
||||||
|
isPrime = true;
|
||||||
|
for(var i = 2; i < candidate && isPrime; i++){
|
||||||
|
if(candidate%i === 0){
|
||||||
|
isPrime = false;
|
||||||
|
} else {
|
||||||
|
isPrime = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(isPrime){
|
||||||
|
primeArray.push(candidate);
|
||||||
|
}
|
||||||
|
return primeArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
var numPrimes = prompt("How many primes?");
|
||||||
|
|
||||||
|
for (var i = 2; primeArray.length < numPrimes; i++) {
|
||||||
|
PrimeCheck(i);
|
||||||
|
}
|
||||||
|
console.log(primeArray);
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling and edge cases', () => {
|
||||||
|
it('should reject completely invalid diff format', async () => {
|
||||||
|
const original = 'line1\nline2\nline3';
|
||||||
|
const invalidDiff = 'this is not a diff at all';
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, invalidDiff);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject diff with invalid hunk format', async () => {
|
||||||
|
const original = 'line1\nline2\nline3';
|
||||||
|
const invalidHunkDiff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
invalid hunk header
|
||||||
|
line1
|
||||||
|
-line2
|
||||||
|
+new line`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, invalidHunkDiff);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail when diff tries to modify non-existent content', async () => {
|
||||||
|
const original = 'line1\nline2\nline3';
|
||||||
|
const nonMatchingDiff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
line1
|
||||||
|
-nonexistent line
|
||||||
|
+new line
|
||||||
|
line3`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, nonMatchingDiff);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle overlapping hunks', async () => {
|
||||||
|
const original = `line1
|
||||||
|
line2
|
||||||
|
line3
|
||||||
|
line4
|
||||||
|
line5`;
|
||||||
|
const overlappingDiff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
line1
|
||||||
|
line2
|
||||||
|
-line3
|
||||||
|
+modified3
|
||||||
|
line4
|
||||||
|
@@ ... @@
|
||||||
|
line2
|
||||||
|
-line3
|
||||||
|
-line4
|
||||||
|
+modified3and4
|
||||||
|
line5`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, overlappingDiff);
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty lines modifications', async () => {
|
||||||
|
const original = `line1
|
||||||
|
|
||||||
|
line3
|
||||||
|
|
||||||
|
line5`;
|
||||||
|
const emptyLinesDiff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
line1
|
||||||
|
|
||||||
|
-line3
|
||||||
|
+line3modified
|
||||||
|
|
||||||
|
line5`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, emptyLinesDiff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`line1
|
||||||
|
|
||||||
|
line3modified
|
||||||
|
|
||||||
|
line5`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed line endings in diff', async () => {
|
||||||
|
const original = 'line1\r\nline2\nline3\r\n';
|
||||||
|
const mixedEndingsDiff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
line1\r
|
||||||
|
-line2
|
||||||
|
+modified2\r
|
||||||
|
line3`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, mixedEndingsDiff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe('line1\r\nmodified2\r\nline3\r\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle partial line modifications', async () => {
|
||||||
|
const original = 'const value = oldValue + 123;';
|
||||||
|
const partialDiff = `--- a/file.txt
|
||||||
|
+++ b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
-const value = oldValue + 123;
|
||||||
|
+const value = newValue + 123;`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, partialDiff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe('const value = newValue + 123;');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle slightly malformed but recoverable diff', async () => {
|
||||||
|
const original = 'line1\nline2\nline3';
|
||||||
|
// Missing space after --- and +++
|
||||||
|
const slightlyBadDiff = `---a/file.txt
|
||||||
|
+++b/file.txt
|
||||||
|
@@ ... @@
|
||||||
|
line1
|
||||||
|
-line2
|
||||||
|
+new line
|
||||||
|
line3`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, slightlyBadDiff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe('line1\nnew line\nline3');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('similar code sections', () => {
|
||||||
|
it('should correctly modify the right section when similar code exists', async () => {
|
||||||
|
const original = `function add(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subtract(a, b) {
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiply(a, b) {
|
||||||
|
return a + b; // Bug here
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const diff = `--- a/math.js
|
||||||
|
+++ b/math.js
|
||||||
|
@@ ... @@
|
||||||
|
function multiply(a, b) {
|
||||||
|
- return a + b; // Bug here
|
||||||
|
+ return a * b;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function add(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subtract(a, b) {
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiply(a, b) {
|
||||||
|
return a * b;
|
||||||
|
}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple similar sections with correct context', async () => {
|
||||||
|
const original = `if (condition) {
|
||||||
|
doSomething();
|
||||||
|
doSomething();
|
||||||
|
doSomething();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherCondition) {
|
||||||
|
doSomething();
|
||||||
|
doSomething();
|
||||||
|
doSomething();
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const diff = `--- a/file.js
|
||||||
|
+++ b/file.js
|
||||||
|
@@ ... @@
|
||||||
|
if (otherCondition) {
|
||||||
|
doSomething();
|
||||||
|
- doSomething();
|
||||||
|
+ doSomethingElse();
|
||||||
|
doSomething();
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`if (condition) {
|
||||||
|
doSomething();
|
||||||
|
doSomething();
|
||||||
|
doSomething();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherCondition) {
|
||||||
|
doSomething();
|
||||||
|
doSomethingElse();
|
||||||
|
doSomething();
|
||||||
|
}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hunk splitting', () => {
|
||||||
|
it('should handle large diffs with multiple non-contiguous changes', async () => {
|
||||||
|
const original = `import { readFile } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { Logger } from './logger';
|
||||||
|
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
|
async function processFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
const data = await readFile(filePath, 'utf8');
|
||||||
|
logger.info('File read successfully');
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to read file:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateInput(input: string): boolean {
|
||||||
|
if (!input) {
|
||||||
|
logger.warn('Empty input received');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return input.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeOutput(data: string) {
|
||||||
|
logger.info('Processing output');
|
||||||
|
// TODO: Implement output writing
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfig(configPath: string) {
|
||||||
|
logger.debug('Reading config from:', configPath);
|
||||||
|
// Basic config parsing
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
maxRetries: 3
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
processFile,
|
||||||
|
validateInput,
|
||||||
|
writeOutput,
|
||||||
|
parseConfig
|
||||||
|
};`;
|
||||||
|
|
||||||
|
const diff = `--- a/file.ts
|
||||||
|
+++ b/file.ts
|
||||||
|
@@ ... @@
|
||||||
|
-import { readFile } from 'fs';
|
||||||
|
+import { readFile, writeFile } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
-import { Logger } from './logger';
|
||||||
|
+import { Logger } from './utils/logger';
|
||||||
|
+import { Config } from './types';
|
||||||
|
|
||||||
|
-const logger = new Logger();
|
||||||
|
+const logger = new Logger('FileProcessor');
|
||||||
|
|
||||||
|
async function processFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
const data = await readFile(filePath, 'utf8');
|
||||||
|
- logger.info('File read successfully');
|
||||||
|
+ logger.info(\`File \${filePath} read successfully\`);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
- logger.error('Failed to read file:', error);
|
||||||
|
+ logger.error(\`Failed to read file \${filePath}:\`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateInput(input: string): boolean {
|
||||||
|
if (!input) {
|
||||||
|
- logger.warn('Empty input received');
|
||||||
|
+ logger.warn('Validation failed: Empty input received');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
- return input.length > 0;
|
||||||
|
+ return input.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
-async function writeOutput(data: string) {
|
||||||
|
- logger.info('Processing output');
|
||||||
|
- // TODO: Implement output writing
|
||||||
|
- return Promise.resolve();
|
||||||
|
+async function writeOutput(data: string, outputPath: string) {
|
||||||
|
+ try {
|
||||||
|
+ await writeFile(outputPath, data, 'utf8');
|
||||||
|
+ logger.info(\`Output written to \${outputPath}\`);
|
||||||
|
+ } catch (error) {
|
||||||
|
+ logger.error(\`Failed to write output to \${outputPath}:\`, error);
|
||||||
|
+ throw error;
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
-function parseConfig(configPath: string) {
|
||||||
|
- logger.debug('Reading config from:', configPath);
|
||||||
|
- // Basic config parsing
|
||||||
|
- return {
|
||||||
|
- enabled: true,
|
||||||
|
- maxRetries: 3
|
||||||
|
- };
|
||||||
|
+async function parseConfig(configPath: string): Promise<Config> {
|
||||||
|
+ try {
|
||||||
|
+ const configData = await readFile(configPath, 'utf8');
|
||||||
|
+ logger.debug(\`Reading config from \${configPath}\`);
|
||||||
|
+ return JSON.parse(configData);
|
||||||
|
+ } catch (error) {
|
||||||
|
+ logger.error(\`Failed to parse config from \${configPath}:\`, error);
|
||||||
|
+ throw error;
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
processFile,
|
||||||
|
validateInput,
|
||||||
|
writeOutput,
|
||||||
|
- parseConfig
|
||||||
|
+ parseConfig,
|
||||||
|
+ type Config
|
||||||
|
};`;
|
||||||
|
|
||||||
|
const expected = `import { readFile, writeFile } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { Logger } from './utils/logger';
|
||||||
|
import { Config } from './types';
|
||||||
|
|
||||||
|
const logger = new Logger('FileProcessor');
|
||||||
|
|
||||||
|
async function processFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
const data = await readFile(filePath, 'utf8');
|
||||||
|
logger.info(\`File \${filePath} read successfully\`);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(\`Failed to read file \${filePath}:\`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateInput(input: string): boolean {
|
||||||
|
if (!input) {
|
||||||
|
logger.warn('Validation failed: Empty input received');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return input.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeOutput(data: string, outputPath: string) {
|
||||||
|
try {
|
||||||
|
await writeFile(outputPath, data, 'utf8');
|
||||||
|
logger.info(\`Output written to \${outputPath}\`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(\`Failed to write output to \${outputPath}:\`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseConfig(configPath: string): Promise<Config> {
|
||||||
|
try {
|
||||||
|
const configData = await readFile(configPath, 'utf8');
|
||||||
|
logger.debug(\`Reading config from \${configPath}\`);
|
||||||
|
return JSON.parse(configData);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(\`Failed to parse config from \${configPath}:\`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
processFile,
|
||||||
|
validateInput,
|
||||||
|
writeOutput,
|
||||||
|
parseConfig,
|
||||||
|
type Config
|
||||||
|
};`;
|
||||||
|
|
||||||
|
const result = await strategy.applyDiff(original, diff);
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@ describe('SearchReplaceDiffStrategy', () => {
|
|||||||
strategy = new SearchReplaceDiffStrategy(1.0, 5) // Default 1.0 threshold for exact matching, 5 line buffer for tests
|
strategy = new SearchReplaceDiffStrategy(1.0, 5) // Default 1.0 threshold for exact matching, 5 line buffer for tests
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should replace matching content', () => {
|
it('should replace matching content', async () => {
|
||||||
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
|
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -21,14 +21,14 @@ function hello() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('function hello() {\n console.log("hello world")\n}\n')
|
expect(result.content).toBe('function hello() {\n console.log("hello world")\n}\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match content with different surrounding whitespace', () => {
|
it('should match content with different surrounding whitespace', async () => {
|
||||||
const originalContent = '\nfunction example() {\n return 42;\n}\n\n'
|
const originalContent = '\nfunction example() {\n return 42;\n}\n\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -41,14 +41,14 @@ function example() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('\nfunction example() {\n return 43;\n}\n\n')
|
expect(result.content).toBe('\nfunction example() {\n return 43;\n}\n\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match content with different indentation in search block', () => {
|
it('should match content with different indentation in search block', async () => {
|
||||||
const originalContent = ' function test() {\n return true;\n }\n'
|
const originalContent = ' function test() {\n return true;\n }\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -61,14 +61,14 @@ function test() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(' function test() {\n return false;\n }\n')
|
expect(result.content).toBe(' function test() {\n return false;\n }\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle tab-based indentation', () => {
|
it('should handle tab-based indentation', async () => {
|
||||||
const originalContent = "function test() {\n\treturn true;\n}\n"
|
const originalContent = "function test() {\n\treturn true;\n}\n"
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -81,14 +81,14 @@ function test() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe("function test() {\n\treturn false;\n}\n")
|
expect(result.content).toBe("function test() {\n\treturn false;\n}\n")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve mixed tabs and spaces', () => {
|
it('should preserve mixed tabs and spaces', async () => {
|
||||||
const originalContent = "\tclass Example {\n\t constructor() {\n\t\tthis.value = 0;\n\t }\n\t}"
|
const originalContent = "\tclass Example {\n\t constructor() {\n\t\tthis.value = 0;\n\t }\n\t}"
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -105,14 +105,14 @@ function test() {
|
|||||||
\t}
|
\t}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe("\tclass Example {\n\t constructor() {\n\t\tthis.value = 1;\n\t }\n\t}")
|
expect(result.content).toBe("\tclass Example {\n\t constructor() {\n\t\tthis.value = 1;\n\t }\n\t}")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle additional indentation with tabs', () => {
|
it('should handle additional indentation with tabs', async () => {
|
||||||
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
|
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -126,14 +126,14 @@ function test() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe("\tfunction test() {\n\t\t// Add comment\n\t\treturn false;\n\t}")
|
expect(result.content).toBe("\tfunction test() {\n\t\t// Add comment\n\t\treturn false;\n\t}")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve exact indentation characters when adding lines', () => {
|
it('should preserve exact indentation characters when adding lines', async () => {
|
||||||
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
|
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -148,14 +148,14 @@ function test() {
|
|||||||
\t}
|
\t}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe("\tfunction test() {\n\t\t// First comment\n\t\t// Second comment\n\t\treturn true;\n\t}")
|
expect(result.content).toBe("\tfunction test() {\n\t\t// First comment\n\t\t// Second comment\n\t\treturn true;\n\t}")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle Windows-style CRLF line endings', () => {
|
it('should handle Windows-style CRLF line endings', async () => {
|
||||||
const originalContent = "function test() {\r\n return true;\r\n}\r\n"
|
const originalContent = "function test() {\r\n return true;\r\n}\r\n"
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -168,14 +168,14 @@ function test() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe("function test() {\r\n return false;\r\n}\r\n")
|
expect(result.content).toBe("function test() {\r\n return false;\r\n}\r\n")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return false if search content does not match', () => {
|
it('should return false if search content does not match', async () => {
|
||||||
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
|
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -188,19 +188,19 @@ function hello() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return false if diff format is invalid', () => {
|
it('should return false if diff format is invalid', async () => {
|
||||||
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
|
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
|
||||||
const diffContent = `test.ts\nInvalid diff format`
|
const diffContent = `test.ts\nInvalid diff format`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle multiple lines with proper indentation', () => {
|
it('should handle multiple lines with proper indentation', async () => {
|
||||||
const originalContent = 'class Example {\n constructor() {\n this.value = 0\n }\n\n getValue() {\n return this.value\n }\n}\n'
|
const originalContent = 'class Example {\n constructor() {\n this.value = 0\n }\n\n getValue() {\n return this.value\n }\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -215,14 +215,14 @@ function hello() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('class Example {\n constructor() {\n this.value = 0\n }\n\n getValue() {\n // Add logging\n console.log("Getting value")\n return this.value\n }\n}\n')
|
expect(result.content).toBe('class Example {\n constructor() {\n this.value = 0\n }\n\n getValue() {\n // Add logging\n console.log("Getting value")\n return this.value\n }\n}\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve whitespace exactly in the output', () => {
|
it('should preserve whitespace exactly in the output', async () => {
|
||||||
const originalContent = " indented\n more indented\n back\n"
|
const originalContent = " indented\n more indented\n back\n"
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -235,14 +235,14 @@ function hello() {
|
|||||||
end
|
end
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(" modified\n still indented\n end\n")
|
expect(result.content).toBe(" modified\n still indented\n end\n")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve indentation when adding new lines after existing content', () => {
|
it('should preserve indentation when adding new lines after existing content', async () => {
|
||||||
const originalContent = ' onScroll={() => updateHighlights()}'
|
const originalContent = ' onScroll={() => updateHighlights()}'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -255,14 +255,14 @@ function hello() {
|
|||||||
}}
|
}}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(' onScroll={() => updateHighlights()}\n onDragOver={(e) => {\n e.preventDefault()\n e.stopPropagation()\n }}')
|
expect(result.content).toBe(' onScroll={() => updateHighlights()}\n onDragOver={(e) => {\n e.preventDefault()\n e.stopPropagation()\n }}')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle varying indentation levels correctly', () => {
|
it('should handle varying indentation levels correctly', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
class Example {
|
class Example {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -296,7 +296,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`.trim();
|
>>>>>>> REPLACE`.trim();
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent);
|
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`
|
expect(result.content).toBe(`
|
||||||
@@ -313,7 +313,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle mixed indentation styles in the same file', () => {
|
it('should handle mixed indentation styles in the same file', async () => {
|
||||||
const originalContent = `class Example {
|
const originalContent = `class Example {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.value = 0;
|
this.value = 0;
|
||||||
@@ -340,7 +340,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`;
|
>>>>>>> REPLACE`;
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent);
|
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`class Example {
|
expect(result.content).toBe(`class Example {
|
||||||
@@ -355,7 +355,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle Python-style significant whitespace', () => {
|
it('should handle Python-style significant whitespace', async () => {
|
||||||
const originalContent = `def example():
|
const originalContent = `def example():
|
||||||
if condition:
|
if condition:
|
||||||
do_something()
|
do_something()
|
||||||
@@ -376,7 +376,7 @@ class Example {
|
|||||||
process(item)
|
process(item)
|
||||||
>>>>>>> REPLACE`;
|
>>>>>>> REPLACE`;
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent);
|
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`def example():
|
expect(result.content).toBe(`def example():
|
||||||
@@ -389,7 +389,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve empty lines with indentation', () => {
|
it('should preserve empty lines with indentation', async () => {
|
||||||
const originalContent = `function test() {
|
const originalContent = `function test() {
|
||||||
const x = 1;
|
const x = 1;
|
||||||
|
|
||||||
@@ -409,7 +409,7 @@ class Example {
|
|||||||
if (x) {
|
if (x) {
|
||||||
>>>>>>> REPLACE`;
|
>>>>>>> REPLACE`;
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent);
|
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function test() {
|
expect(result.content).toBe(`function test() {
|
||||||
@@ -423,7 +423,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle indentation when replacing entire blocks', () => {
|
it('should handle indentation when replacing entire blocks', async () => {
|
||||||
const originalContent = `class Test {
|
const originalContent = `class Test {
|
||||||
method() {
|
method() {
|
||||||
if (true) {
|
if (true) {
|
||||||
@@ -450,7 +450,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`;
|
>>>>>>> REPLACE`;
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent);
|
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`class Test {
|
expect(result.content).toBe(`class Test {
|
||||||
@@ -467,7 +467,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle negative indentation relative to search content', () => {
|
it('should handle negative indentation relative to search content', async () => {
|
||||||
const originalContent = `class Example {
|
const originalContent = `class Example {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (true) {
|
if (true) {
|
||||||
@@ -484,8 +484,8 @@ class Example {
|
|||||||
this.init();
|
this.init();
|
||||||
this.setup();
|
this.setup();
|
||||||
>>>>>>> REPLACE`;
|
>>>>>>> REPLACE`;
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent);
|
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`class Example {
|
expect(result.content).toBe(`class Example {
|
||||||
@@ -499,7 +499,7 @@ class Example {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle extreme negative indentation (no indent)', () => {
|
it('should handle extreme negative indentation (no indent)', async () => {
|
||||||
const originalContent = `class Example {
|
const originalContent = `class Example {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (true) {
|
if (true) {
|
||||||
@@ -514,7 +514,7 @@ class Example {
|
|||||||
this.init();
|
this.init();
|
||||||
>>>>>>> REPLACE`;
|
>>>>>>> REPLACE`;
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent);
|
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`class Example {
|
expect(result.content).toBe(`class Example {
|
||||||
@@ -527,7 +527,7 @@ this.init();
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle mixed indentation changes in replace block', () => {
|
it('should handle mixed indentation changes in replace block', async () => {
|
||||||
const originalContent = `class Example {
|
const originalContent = `class Example {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (true) {
|
if (true) {
|
||||||
@@ -548,7 +548,7 @@ this.init();
|
|||||||
this.validate();
|
this.validate();
|
||||||
>>>>>>> REPLACE`;
|
>>>>>>> REPLACE`;
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent);
|
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`class Example {
|
expect(result.content).toBe(`class Example {
|
||||||
@@ -563,7 +563,7 @@ this.init();
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find matches from middle out', () => {
|
it('should find matches from middle out', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return "target";
|
return "target";
|
||||||
@@ -595,7 +595,7 @@ function five() {
|
|||||||
// Search around the middle (function three)
|
// Search around the middle (function three)
|
||||||
// Even though all functions contain the target text,
|
// Even though all functions contain the target text,
|
||||||
// it should match the one closest to line 9 first
|
// it should match the one closest to line 9 first
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 9, 9)
|
const result = await strategy.applyDiff(originalContent, diffContent, 9, 9)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function one() {
|
expect(result.content).toBe(`function one() {
|
||||||
@@ -629,7 +629,7 @@ function five() {
|
|||||||
strategy = new SearchReplaceDiffStrategy()
|
strategy = new SearchReplaceDiffStrategy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should strip line numbers from both search and replace sections', () => {
|
it('should strip line numbers from both search and replace sections', async () => {
|
||||||
const originalContent = 'function test() {\n return true;\n}\n'
|
const originalContent = 'function test() {\n return true;\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -642,14 +642,14 @@ function five() {
|
|||||||
3 | }
|
3 | }
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should strip line numbers with leading spaces', () => {
|
it('should strip line numbers with leading spaces', async () => {
|
||||||
const originalContent = 'function test() {\n return true;\n}\n'
|
const originalContent = 'function test() {\n return true;\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -662,14 +662,14 @@ function five() {
|
|||||||
3 | }
|
3 | }
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not strip when not all lines have numbers in either section', () => {
|
it('should not strip when not all lines have numbers in either section', async () => {
|
||||||
const originalContent = 'function test() {\n return true;\n}\n'
|
const originalContent = 'function test() {\n return true;\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -682,11 +682,11 @@ function five() {
|
|||||||
3 | }
|
3 | }
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve content that naturally starts with pipe', () => {
|
it('should preserve content that naturally starts with pipe', async () => {
|
||||||
const originalContent = '|header|another|\n|---|---|\n|data|more|\n'
|
const originalContent = '|header|another|\n|---|---|\n|data|more|\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -699,14 +699,14 @@ function five() {
|
|||||||
3 | |data|updated|
|
3 | |data|updated|
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('|header|another|\n|---|---|\n|data|updated|\n')
|
expect(result.content).toBe('|header|another|\n|---|---|\n|data|updated|\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve indentation when stripping line numbers', () => {
|
it('should preserve indentation when stripping line numbers', async () => {
|
||||||
const originalContent = ' function test() {\n return true;\n }\n'
|
const originalContent = ' function test() {\n return true;\n }\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -719,14 +719,14 @@ function five() {
|
|||||||
3 | }
|
3 | }
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(' function test() {\n return false;\n }\n')
|
expect(result.content).toBe(' function test() {\n return false;\n }\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle different line numbers between sections', () => {
|
it('should handle different line numbers between sections', async () => {
|
||||||
const originalContent = 'function test() {\n return true;\n}\n'
|
const originalContent = 'function test() {\n return true;\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -739,14 +739,14 @@ function five() {
|
|||||||
22 | }
|
22 | }
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not strip content that starts with pipe but no line number', () => {
|
it('should not strip content that starts with pipe but no line number', async () => {
|
||||||
const originalContent = '| Pipe\n|---|\n| Data\n'
|
const originalContent = '| Pipe\n|---|\n| Data\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -759,14 +759,14 @@ function five() {
|
|||||||
| Updated
|
| Updated
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('| Pipe\n|---|\n| Updated\n')
|
expect(result.content).toBe('| Pipe\n|---|\n| Updated\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle mix of line-numbered and pipe-only content', () => {
|
it('should handle mix of line-numbered and pipe-only content', async () => {
|
||||||
const originalContent = '| Pipe\n|---|\n| Data\n'
|
const originalContent = '| Pipe\n|---|\n| Data\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -779,7 +779,7 @@ function five() {
|
|||||||
3 | | NewData
|
3 | | NewData
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('1 | | Pipe\n2 | |---|\n3 | | NewData\n')
|
expect(result.content).toBe('1 | | Pipe\n2 | |---|\n3 | | NewData\n')
|
||||||
@@ -796,7 +796,7 @@ function five() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('deletion', () => {
|
describe('deletion', () => {
|
||||||
it('should delete code when replace block is empty', () => {
|
it('should delete code when replace block is empty', async () => {
|
||||||
const originalContent = `function test() {
|
const originalContent = `function test() {
|
||||||
console.log("hello");
|
console.log("hello");
|
||||||
// Comment to remove
|
// Comment to remove
|
||||||
@@ -808,7 +808,7 @@ function five() {
|
|||||||
=======
|
=======
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function test() {
|
expect(result.content).toBe(`function test() {
|
||||||
@@ -818,7 +818,7 @@ function five() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should delete multiple lines when replace block is empty', () => {
|
it('should delete multiple lines when replace block is empty', async () => {
|
||||||
const originalContent = `class Example {
|
const originalContent = `class Example {
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize
|
// Initialize
|
||||||
@@ -838,7 +838,7 @@ function five() {
|
|||||||
=======
|
=======
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`class Example {
|
expect(result.content).toBe(`class Example {
|
||||||
@@ -848,7 +848,7 @@ function five() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve indentation when deleting nested code', () => {
|
it('should preserve indentation when deleting nested code', async () => {
|
||||||
const originalContent = `function outer() {
|
const originalContent = `function outer() {
|
||||||
if (true) {
|
if (true) {
|
||||||
// Remove this
|
// Remove this
|
||||||
@@ -865,7 +865,7 @@ function five() {
|
|||||||
=======
|
=======
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function outer() {
|
expect(result.content).toBe(`function outer() {
|
||||||
@@ -878,7 +878,7 @@ function five() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('insertion', () => {
|
describe('insertion', () => {
|
||||||
it('should insert code at specified line when search block is empty', () => {
|
it('should insert code at specified line when search block is empty', async () => {
|
||||||
const originalContent = `function test() {
|
const originalContent = `function test() {
|
||||||
const x = 1;
|
const x = 1;
|
||||||
return x;
|
return x;
|
||||||
@@ -889,7 +889,7 @@ function five() {
|
|||||||
console.log("Adding log");
|
console.log("Adding log");
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 2, 2)
|
const result = await strategy.applyDiff(originalContent, diffContent, 2, 2)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function test() {
|
expect(result.content).toBe(`function test() {
|
||||||
@@ -900,7 +900,7 @@ function five() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should preserve indentation when inserting at nested location', () => {
|
it('should preserve indentation when inserting at nested location', async () => {
|
||||||
const originalContent = `function test() {
|
const originalContent = `function test() {
|
||||||
if (true) {
|
if (true) {
|
||||||
const x = 1;
|
const x = 1;
|
||||||
@@ -913,7 +913,7 @@ function five() {
|
|||||||
console.log("After");
|
console.log("After");
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 3, 3)
|
const result = await strategy.applyDiff(originalContent, diffContent, 3, 3)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function test() {
|
expect(result.content).toBe(`function test() {
|
||||||
@@ -926,7 +926,7 @@ function five() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle insertion at start of file', () => {
|
it('should handle insertion at start of file', async () => {
|
||||||
const originalContent = `function test() {
|
const originalContent = `function test() {
|
||||||
return true;
|
return true;
|
||||||
}`
|
}`
|
||||||
@@ -938,7 +938,7 @@ function five() {
|
|||||||
|
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 1, 1)
|
const result = await strategy.applyDiff(originalContent, diffContent, 1, 1)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`// Copyright 2024
|
expect(result.content).toBe(`// Copyright 2024
|
||||||
@@ -950,7 +950,7 @@ function test() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle insertion at end of file', () => {
|
it('should handle insertion at end of file', async () => {
|
||||||
const originalContent = `function test() {
|
const originalContent = `function test() {
|
||||||
return true;
|
return true;
|
||||||
}`
|
}`
|
||||||
@@ -961,7 +961,7 @@ function test() {
|
|||||||
// End of file
|
// End of file
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 4, 4)
|
const result = await strategy.applyDiff(originalContent, diffContent, 4, 4)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function test() {
|
expect(result.content).toBe(`function test() {
|
||||||
@@ -972,7 +972,7 @@ function test() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should error if no start_line is provided for insertion', () => {
|
it('should error if no start_line is provided for insertion', async () => {
|
||||||
const originalContent = `function test() {
|
const originalContent = `function test() {
|
||||||
return true;
|
return true;
|
||||||
}`
|
}`
|
||||||
@@ -982,7 +982,7 @@ function test() {
|
|||||||
console.log("test");
|
console.log("test");
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -994,7 +994,7 @@ console.log("test");
|
|||||||
strategy = new SearchReplaceDiffStrategy(0.9, 5) // 90% similarity threshold, 5 line buffer for tests
|
strategy = new SearchReplaceDiffStrategy(0.9, 5) // 90% similarity threshold, 5 line buffer for tests
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match content with small differences (>90% similar)', () => {
|
it('should match content with small differences (>90% similar)', async () => {
|
||||||
const originalContent = 'function getData() {\n const results = fetchData();\n return results.filter(Boolean);\n}\n'
|
const originalContent = 'function getData() {\n const results = fetchData();\n return results.filter(Boolean);\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -1011,14 +1011,14 @@ function getData() {
|
|||||||
|
|
||||||
strategy = new SearchReplaceDiffStrategy(0.9, 5) // Use 5 line buffer for tests
|
strategy = new SearchReplaceDiffStrategy(0.9, 5) // Use 5 line buffer for tests
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('function getData() {\n const data = fetchData();\n return data.filter(Boolean);\n}\n')
|
expect(result.content).toBe('function getData() {\n const data = fetchData();\n return data.filter(Boolean);\n}\n')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not match when content is too different (<90% similar)', () => {
|
it('should not match when content is too different (<90% similar)', async () => {
|
||||||
const originalContent = 'function processUsers(data) {\n return data.map(user => user.name);\n}\n'
|
const originalContent = 'function processUsers(data) {\n return data.map(user => user.name);\n}\n'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -1031,11 +1031,11 @@ function processData(data) {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match content with extra whitespace', () => {
|
it('should match content with extra whitespace', async () => {
|
||||||
const originalContent = 'function sum(a, b) {\n return a + b;\n}'
|
const originalContent = 'function sum(a, b) {\n return a + b;\n}'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -1048,14 +1048,14 @@ function sum(a, b) {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('function sum(a, b) {\n return a + b + 1;\n}')
|
expect(result.content).toBe('function sum(a, b) {\n return a + b + 1;\n}')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not exact match empty lines', () => {
|
it('should not exact match empty lines', async () => {
|
||||||
const originalContent = 'function sum(a, b) {\n\n return a + b;\n}'
|
const originalContent = 'function sum(a, b) {\n\n return a + b;\n}'
|
||||||
const diffContent = `test.ts
|
const diffContent = `test.ts
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
@@ -1065,7 +1065,7 @@ import { a } from "a";
|
|||||||
function sum(a, b) {
|
function sum(a, b) {
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe('import { a } from "a";\nfunction sum(a, b) {\n\n return a + b;\n}')
|
expect(result.content).toBe('import { a } from "a";\nfunction sum(a, b) {\n\n return a + b;\n}')
|
||||||
@@ -1080,7 +1080,7 @@ function sum(a, b) {
|
|||||||
strategy = new SearchReplaceDiffStrategy(0.9, 5)
|
strategy = new SearchReplaceDiffStrategy(0.9, 5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should find and replace within specified line range', () => {
|
it('should find and replace within specified line range', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1105,7 +1105,7 @@ function two() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 5, 7)
|
const result = await strategy.applyDiff(originalContent, diffContent, 5, 7)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function one() {
|
expect(result.content).toBe(`function one() {
|
||||||
@@ -1122,7 +1122,7 @@ function three() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should find and replace within buffer zone (5 lines before/after)', () => {
|
it('should find and replace within buffer zone (5 lines before/after)', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1149,7 +1149,7 @@ function three() {
|
|||||||
|
|
||||||
// Even though we specify lines 5-7, it should still find the match at lines 9-11
|
// Even though we specify lines 5-7, it should still find the match at lines 9-11
|
||||||
// because it's within the 5-line buffer zone
|
// because it's within the 5-line buffer zone
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 5, 7)
|
const result = await strategy.applyDiff(originalContent, diffContent, 5, 7)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function one() {
|
expect(result.content).toBe(`function one() {
|
||||||
@@ -1166,7 +1166,7 @@ function three() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not find matches outside search range and buffer zone', () => {
|
it('should not find matches outside search range and buffer zone', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1201,11 +1201,11 @@ function five() {
|
|||||||
|
|
||||||
// Searching around function two() (lines 5-7)
|
// Searching around function two() (lines 5-7)
|
||||||
// function five() is more than 5 lines away, so it shouldn't match
|
// function five() is more than 5 lines away, so it shouldn't match
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 5, 7)
|
const result = await strategy.applyDiff(originalContent, diffContent, 5, 7)
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle search range at start of file', () => {
|
it('should handle search range at start of file', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1226,7 +1226,7 @@ function one() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 1, 3)
|
const result = await strategy.applyDiff(originalContent, diffContent, 1, 3)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function one() {
|
expect(result.content).toBe(`function one() {
|
||||||
@@ -1239,7 +1239,7 @@ function two() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle search range at end of file', () => {
|
it('should handle search range at end of file', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1260,7 +1260,7 @@ function two() {
|
|||||||
}
|
}
|
||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 5, 7)
|
const result = await strategy.applyDiff(originalContent, diffContent, 5, 7)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function one() {
|
expect(result.content).toBe(`function one() {
|
||||||
@@ -1273,7 +1273,7 @@ function two() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match specific instance of duplicate code using line numbers', () => {
|
it('should match specific instance of duplicate code using line numbers', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function processData(data) {
|
function processData(data) {
|
||||||
return data.map(x => x * 2);
|
return data.map(x => x * 2);
|
||||||
@@ -1306,7 +1306,7 @@ function processData(data) {
|
|||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
// Target the second instance of processData
|
// Target the second instance of processData
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 10, 12)
|
const result = await strategy.applyDiff(originalContent, diffContent, 10, 12)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function processData(data) {
|
expect(result.content).toBe(`function processData(data) {
|
||||||
@@ -1330,7 +1330,7 @@ function moreStuff() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should search from start line to end of file when only start_line is provided', () => {
|
it('should search from start line to end of file when only start_line is provided', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1356,7 +1356,7 @@ function three() {
|
|||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
// Only provide start_line, should search from there to end of file
|
// Only provide start_line, should search from there to end of file
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 8)
|
const result = await strategy.applyDiff(originalContent, diffContent, 8)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function one() {
|
expect(result.content).toBe(`function one() {
|
||||||
@@ -1373,7 +1373,7 @@ function three() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should search from start of file to end line when only end_line is provided', () => {
|
it('should search from start of file to end line when only end_line is provided', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1399,7 +1399,7 @@ function one() {
|
|||||||
>>>>>>> REPLACE`
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
// Only provide end_line, should search from start of file to there
|
// Only provide end_line, should search from start of file to there
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, undefined, 4)
|
const result = await strategy.applyDiff(originalContent, diffContent, undefined, 4)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function one() {
|
expect(result.content).toBe(`function one() {
|
||||||
@@ -1416,7 +1416,7 @@ function three() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should prioritize exact line match over expanded search', () => {
|
it('should prioritize exact line match over expanded search', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1446,7 +1446,7 @@ function process() {
|
|||||||
|
|
||||||
// Should match the second instance exactly at lines 10-12
|
// Should match the second instance exactly at lines 10-12
|
||||||
// even though the first instance at 6-8 is within the expanded search range
|
// even though the first instance at 6-8 is within the expanded search range
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 10, 12)
|
const result = await strategy.applyDiff(originalContent, diffContent, 10, 12)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`
|
expect(result.content).toBe(`
|
||||||
@@ -1468,7 +1468,7 @@ function two() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should fall back to expanded search only if exact match fails', () => {
|
it('should fall back to expanded search only if exact match fails', async () => {
|
||||||
const originalContent = `
|
const originalContent = `
|
||||||
function one() {
|
function one() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1494,7 +1494,7 @@ function process() {
|
|||||||
|
|
||||||
// Specify wrong line numbers (3-5), but content exists at 6-8
|
// Specify wrong line numbers (3-5), but content exists at 6-8
|
||||||
// Should still find and replace it since it's within the expanded range
|
// Should still find and replace it since it's within the expanded range
|
||||||
const result = strategy.applyDiff(originalContent, diffContent, 3, 5)
|
const result = await strategy.applyDiff(originalContent, diffContent, 3, 5)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(`function one() {
|
expect(result.content).toBe(`function one() {
|
||||||
@@ -1519,14 +1519,14 @@ function two() {
|
|||||||
strategy = new SearchReplaceDiffStrategy()
|
strategy = new SearchReplaceDiffStrategy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should include the current working directory', () => {
|
it('should include the current working directory', async () => {
|
||||||
const cwd = '/test/dir'
|
const cwd = '/test/dir'
|
||||||
const description = strategy.getToolDescription(cwd)
|
const description = await strategy.getToolDescription(cwd)
|
||||||
expect(description).toContain(`relative to the current working directory ${cwd}`)
|
expect(description).toContain(`relative to the current working directory ${cwd}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should include required format elements', () => {
|
it('should include required format elements', async () => {
|
||||||
const description = strategy.getToolDescription('/test')
|
const description = await strategy.getToolDescription('/test')
|
||||||
expect(description).toContain('<<<<<<< SEARCH')
|
expect(description).toContain('<<<<<<< SEARCH')
|
||||||
expect(description).toContain('=======')
|
expect(description).toContain('=======')
|
||||||
expect(description).toContain('>>>>>>> REPLACE')
|
expect(description).toContain('>>>>>>> REPLACE')
|
||||||
@@ -1534,8 +1534,8 @@ function two() {
|
|||||||
expect(description).toContain('</apply_diff>')
|
expect(description).toContain('</apply_diff>')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should document start_line and end_line parameters', () => {
|
it('should document start_line and end_line parameters', async () => {
|
||||||
const description = strategy.getToolDescription('/test')
|
const description = await strategy.getToolDescription('/test')
|
||||||
expect(description).toContain('start_line: (required) The line number where the search block starts.')
|
expect(description).toContain('start_line: (required) The line number where the search block starts.')
|
||||||
expect(description).toContain('end_line: (required) The line number where the search block ends.')
|
expect(description).toContain('end_line: (required) The line number where the search block ends.')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('UnifiedDiffStrategy', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('applyDiff', () => {
|
describe('applyDiff', () => {
|
||||||
it('should successfully apply a function modification diff', () => {
|
it('should successfully apply a function modification diff', async () => {
|
||||||
const originalContent = `import { Logger } from '../logger';
|
const originalContent = `import { Logger } from '../logger';
|
||||||
|
|
||||||
function calculateTotal(items: number[]): number {
|
function calculateTotal(items: number[]): number {
|
||||||
@@ -58,14 +58,14 @@ function calculateTotal(items: number[]): number {
|
|||||||
|
|
||||||
export { calculateTotal };`
|
export { calculateTotal };`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(expected)
|
expect(result.content).toBe(expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should successfully apply a diff adding a new method', () => {
|
it('should successfully apply a diff adding a new method', async () => {
|
||||||
const originalContent = `class Calculator {
|
const originalContent = `class Calculator {
|
||||||
add(a: number, b: number): number {
|
add(a: number, b: number): number {
|
||||||
return a + b;
|
return a + b;
|
||||||
@@ -95,14 +95,14 @@ export { calculateTotal };`
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(expected)
|
expect(result.content).toBe(expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should successfully apply a diff modifying imports', () => {
|
it('should successfully apply a diff modifying imports', async () => {
|
||||||
const originalContent = `import { useState } from 'react';
|
const originalContent = `import { useState } from 'react';
|
||||||
import { Button } from './components';
|
import { Button } from './components';
|
||||||
|
|
||||||
@@ -132,15 +132,15 @@ function App() {
|
|||||||
useEffect(() => { document.title = \`Count: \${count}\` }, [count]);
|
useEffect(() => { document.title = \`Count: \${count}\` }, [count]);
|
||||||
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
|
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(expected)
|
expect(result.content).toBe(expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should successfully apply a diff with multiple hunks', () => {
|
it('should successfully apply a diff with multiple hunks', async () => {
|
||||||
const originalContent = `import { readFile, writeFile } from 'fs';
|
const originalContent = `import { readFile, writeFile } from 'fs';
|
||||||
|
|
||||||
function processFile(path: string) {
|
function processFile(path: string) {
|
||||||
@@ -198,14 +198,14 @@ async function processFile(path: string) {
|
|||||||
|
|
||||||
export { processFile };`
|
export { processFile };`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(expected)
|
expect(result.content).toBe(expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle empty original content', () => {
|
it('should handle empty original content', async () => {
|
||||||
const originalContent = ''
|
const originalContent = ''
|
||||||
const diffContent = `--- empty.ts
|
const diffContent = `--- empty.ts
|
||||||
+++ empty.ts
|
+++ empty.ts
|
||||||
@@ -218,7 +218,7 @@ export { processFile };`
|
|||||||
return \`Hello, \${name}!\`;
|
return \`Hello, \${name}!\`;
|
||||||
}\n`
|
}\n`
|
||||||
|
|
||||||
const result = strategy.applyDiff(originalContent, diffContent)
|
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.content).toBe(expected)
|
expect(result.content).toBe(expected)
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import { applyContextMatching, applyDMP, applyGitFallback } from "../edit-strategies"
|
||||||
|
import { Hunk } from "../types"
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
name: "should return original content if no match is found",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "line1" },
|
||||||
|
{ type: "add", content: "line2" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: ["line1", "line3"],
|
||||||
|
matchPosition: -1,
|
||||||
|
expected: {
|
||||||
|
confidence: 0,
|
||||||
|
result: ["line1", "line3"],
|
||||||
|
},
|
||||||
|
expectedResult: "line1\nline3",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a simple add change",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "line1" },
|
||||||
|
{ type: "add", content: "line2" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: ["line1", "line3"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: ["line1", "line2", "line3"],
|
||||||
|
},
|
||||||
|
expectedResult: "line1\nline2\nline3",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a simple remove change",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "line1" },
|
||||||
|
{ type: "remove", content: "line2" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: ["line1", "line2", "line3"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: ["line1", "line3"],
|
||||||
|
},
|
||||||
|
expectedResult: "line1\nline3",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a simple context change",
|
||||||
|
hunk: {
|
||||||
|
changes: [{ type: "context", content: "line1" }],
|
||||||
|
} as Hunk,
|
||||||
|
content: ["line1", "line2", "line3"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: ["line1", "line2", "line3"],
|
||||||
|
},
|
||||||
|
expectedResult: "line1\nline2\nline3",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a multi-line add change",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "line1" },
|
||||||
|
{ type: "add", content: "line2\nline3" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: ["line1", "line4"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: ["line1", "line2\nline3", "line4"],
|
||||||
|
},
|
||||||
|
expectedResult: "line1\nline2\nline3\nline4",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a multi-line remove change",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "line1" },
|
||||||
|
{ type: "remove", content: "line2\nline3" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: ["line1", "line2", "line3", "line4"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: ["line1", "line4"],
|
||||||
|
},
|
||||||
|
expectedResult: "line1\nline4",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a multi-line context change",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "line1" },
|
||||||
|
{ type: "context", content: "line2\nline3" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: ["line1", "line2", "line3", "line4"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: ["line1", "line2\nline3", "line4"],
|
||||||
|
},
|
||||||
|
expectedResult: "line1\nline2\nline3\nline4",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a change with indentation",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: " line1" },
|
||||||
|
{ type: "add", content: " line2" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: [" line1", " line3"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: [" line1", " line2", " line3"],
|
||||||
|
},
|
||||||
|
expectedResult: " line1\n line2\n line3",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a change with mixed indentation",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "\tline1" },
|
||||||
|
{ type: "add", content: " line2" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: ["\tline1", " line3"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: ["\tline1", " line2", " line3"],
|
||||||
|
},
|
||||||
|
expectedResult: "\tline1\n line2\n line3",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a change with mixed indentation and multi-line",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: " line1" },
|
||||||
|
{ type: "add", content: "\tline2\n line3" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: [" line1", " line4"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: [" line1", "\tline2\n line3", " line4"],
|
||||||
|
},
|
||||||
|
expectedResult: " line1\n\tline2\n line3\n line4",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a complex change with mixed indentation and multi-line",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: " line1" },
|
||||||
|
{ type: "remove", content: " line2" },
|
||||||
|
{ type: "add", content: "\tline3\n line4" },
|
||||||
|
{ type: "context", content: " line5" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: [" line1", " line2", " line5", " line6"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: [" line1", "\tline3\n line4", " line5", " line6"],
|
||||||
|
},
|
||||||
|
expectedResult: " line1\n\tline3\n line4\n line5\n line6",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a complex change with mixed indentation and multi-line and context",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: " line1" },
|
||||||
|
{ type: "remove", content: " line2" },
|
||||||
|
{ type: "add", content: "\tline3\n line4" },
|
||||||
|
{ type: "context", content: " line5" },
|
||||||
|
{ type: "context", content: " line6" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: [" line1", " line2", " line5", " line6", " line7"],
|
||||||
|
matchPosition: 0,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: [" line1", "\tline3\n line4", " line5", " line6", " line7"],
|
||||||
|
},
|
||||||
|
expectedResult: " line1\n\tline3\n line4\n line5\n line6\n line7",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should apply a complex change with mixed indentation and multi-line and context and a different match position",
|
||||||
|
hunk: {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: " line1" },
|
||||||
|
{ type: "remove", content: " line2" },
|
||||||
|
{ type: "add", content: "\tline3\n line4" },
|
||||||
|
{ type: "context", content: " line5" },
|
||||||
|
{ type: "context", content: " line6" },
|
||||||
|
],
|
||||||
|
} as Hunk,
|
||||||
|
content: [" line0", " line1", " line2", " line5", " line6", " line7"],
|
||||||
|
matchPosition: 1,
|
||||||
|
expected: {
|
||||||
|
confidence: 1,
|
||||||
|
result: [" line0", " line1", "\tline3\n line4", " line5", " line6", " line7"],
|
||||||
|
},
|
||||||
|
expectedResult: " line0\n line1\n\tline3\n line4\n line5\n line6\n line7",
|
||||||
|
strategies: ["context", "dmp"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("applyContextMatching", () => {
|
||||||
|
testCases.forEach(({ name, hunk, content, matchPosition, expected, strategies, expectedResult }) => {
|
||||||
|
if (!strategies?.includes("context")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it(name, () => {
|
||||||
|
const result = applyContextMatching(hunk, content, matchPosition)
|
||||||
|
expect(result.result.join("\n")).toEqual(expectedResult)
|
||||||
|
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||||
|
expect(result.strategy).toBe("context")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("applyDMP", () => {
|
||||||
|
testCases.forEach(({ name, hunk, content, matchPosition, expected, strategies, expectedResult }) => {
|
||||||
|
if (!strategies?.includes("dmp")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it(name, () => {
|
||||||
|
const result = applyDMP(hunk, content, matchPosition)
|
||||||
|
expect(result.result.join("\n")).toEqual(expectedResult)
|
||||||
|
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||||
|
expect(result.strategy).toBe("dmp")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("applyGitFallback", () => {
|
||||||
|
it("should successfully apply changes using git operations", async () => {
|
||||||
|
const hunk = {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "line1", indent: "" },
|
||||||
|
{ type: "remove", content: "line2", indent: "" },
|
||||||
|
{ type: "add", content: "new line2", indent: "" },
|
||||||
|
{ type: "context", content: "line3", indent: "" }
|
||||||
|
]
|
||||||
|
} as Hunk
|
||||||
|
|
||||||
|
const content = ["line1", "line2", "line3"]
|
||||||
|
const result = await applyGitFallback(hunk, content)
|
||||||
|
|
||||||
|
expect(result.result.join("\n")).toEqual("line1\nnew line2\nline3")
|
||||||
|
expect(result.confidence).toBe(1)
|
||||||
|
expect(result.strategy).toBe("git-fallback")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return original content with 0 confidence when changes cannot be applied", async () => {
|
||||||
|
const hunk = {
|
||||||
|
changes: [
|
||||||
|
{ type: "context", content: "nonexistent", indent: "" },
|
||||||
|
{ type: "add", content: "new line", indent: "" }
|
||||||
|
]
|
||||||
|
} as Hunk
|
||||||
|
|
||||||
|
const content = ["line1", "line2", "line3"]
|
||||||
|
const result = await applyGitFallback(hunk, content)
|
||||||
|
|
||||||
|
expect(result.result).toEqual(content)
|
||||||
|
expect(result.confidence).toBe(0)
|
||||||
|
expect(result.strategy).toBe("git-fallback")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import { findAnchorMatch, findExactMatch, findSimilarityMatch, findLevenshteinMatch } from "../search-strategies"
|
||||||
|
|
||||||
|
type SearchStrategy = (
|
||||||
|
searchStr: string,
|
||||||
|
content: string[],
|
||||||
|
startIndex?: number
|
||||||
|
) => {
|
||||||
|
index: number
|
||||||
|
confidence: number
|
||||||
|
strategy: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
name: "should return no match if the search string is not found",
|
||||||
|
searchStr: "not found",
|
||||||
|
content: ["line1", "line2", "line3"],
|
||||||
|
expected: { index: -1, confidence: 0 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match if the search string is found",
|
||||||
|
searchStr: "line2",
|
||||||
|
content: ["line1", "line2", "line3"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with correct index when startIndex is provided",
|
||||||
|
searchStr: "line3",
|
||||||
|
content: ["line1", "line2", "line3", "line4", "line3"],
|
||||||
|
startIndex: 3,
|
||||||
|
expected: { index: 4, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match even if there are more lines in content",
|
||||||
|
searchStr: "line2",
|
||||||
|
content: ["line1", "line2", "line3", "line4", "line5"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match even if the search string is at the beginning of the content",
|
||||||
|
searchStr: "line1",
|
||||||
|
content: ["line1", "line2", "line3"],
|
||||||
|
expected: { index: 0, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match even if the search string is at the end of the content",
|
||||||
|
searchStr: "line3",
|
||||||
|
content: ["line1", "line2", "line3"],
|
||||||
|
expected: { index: 2, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match for a multi-line search string",
|
||||||
|
searchStr: "line2\nline3",
|
||||||
|
content: ["line1", "line2", "line3", "line4"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return no match if a multi-line search string is not found",
|
||||||
|
searchStr: "line2\nline4",
|
||||||
|
content: ["line1", "line2", "line3", "line4"],
|
||||||
|
expected: { index: -1, confidence: 0 },
|
||||||
|
strategies: ["exact", "similarity"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with indentation",
|
||||||
|
searchStr: " line2",
|
||||||
|
content: ["line1", " line2", "line3"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with more complex indentation",
|
||||||
|
searchStr: " line3",
|
||||||
|
content: [" line1", " line2", " line3", " line4"],
|
||||||
|
expected: { index: 2, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with mixed indentation",
|
||||||
|
searchStr: "\tline2",
|
||||||
|
content: [" line1", "\tline2", " line3"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with mixed indentation and multi-line",
|
||||||
|
searchStr: " line2\n\tline3",
|
||||||
|
content: ["line1", " line2", "\tline3", " line4"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return no match if mixed indentation and multi-line is not found",
|
||||||
|
searchStr: " line2\n line4",
|
||||||
|
content: ["line1", " line2", "\tline3", " line4"],
|
||||||
|
expected: { index: -1, confidence: 0 },
|
||||||
|
strategies: ["exact", "similarity"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with leading and trailing spaces",
|
||||||
|
searchStr: " line2 ",
|
||||||
|
content: ["line1", " line2 ", "line3"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with leading and trailing tabs",
|
||||||
|
searchStr: "\tline2\t",
|
||||||
|
content: ["line1", "\tline2\t", "line3"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with mixed leading and trailing spaces and tabs",
|
||||||
|
searchStr: " \tline2\t ",
|
||||||
|
content: ["line1", " \tline2\t ", "line3"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with mixed leading and trailing spaces and tabs and multi-line",
|
||||||
|
searchStr: " \tline2\t \n line3 ",
|
||||||
|
content: ["line1", " \tline2\t ", " line3 ", "line4"],
|
||||||
|
expected: { index: 1, confidence: 1 },
|
||||||
|
strategies: ["exact", "similarity", "levenshtein"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return no match if mixed leading and trailing spaces and tabs and multi-line is not found",
|
||||||
|
searchStr: " \tline2\t \n line4 ",
|
||||||
|
content: ["line1", " \tline2\t ", " line3 ", "line4"],
|
||||||
|
expected: { index: -1, confidence: 0 },
|
||||||
|
strategies: ["exact", "similarity"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("findExactMatch", () => {
|
||||||
|
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||||
|
if (!strategies?.includes("exact")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it(name, () => {
|
||||||
|
const result = findExactMatch(searchStr, content, startIndex)
|
||||||
|
expect(result.index).toBe(expected.index)
|
||||||
|
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||||
|
expect(result.strategy).toMatch(/exact(-overlapping)?/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("findAnchorMatch", () => {
|
||||||
|
const anchorTestCases = [
|
||||||
|
{
|
||||||
|
name: "should return no match if no anchors are found",
|
||||||
|
searchStr: " \n \n ",
|
||||||
|
content: ["line1", "line2", "line3"],
|
||||||
|
expected: { index: -1, confidence: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return no match if anchor positions cannot be validated",
|
||||||
|
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||||
|
content: [
|
||||||
|
"different line 1",
|
||||||
|
"different line 2",
|
||||||
|
"different line 3",
|
||||||
|
"another unique line",
|
||||||
|
"context line 1",
|
||||||
|
"context line 2",
|
||||||
|
],
|
||||||
|
expected: { index: -1, confidence: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match if anchor positions can be validated",
|
||||||
|
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||||
|
content: ["line1", "line2", "unique line", "context line 1", "context line 2", "line 6"],
|
||||||
|
expected: { index: 2, confidence: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match with correct index when startIndex is provided",
|
||||||
|
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||||
|
content: ["line1", "line2", "line3", "unique line", "context line 1", "context line 2", "line 7"],
|
||||||
|
startIndex: 3,
|
||||||
|
expected: { index: 3, confidence: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match even if there are more lines in content",
|
||||||
|
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||||
|
content: [
|
||||||
|
"line1",
|
||||||
|
"line2",
|
||||||
|
"unique line",
|
||||||
|
"context line 1",
|
||||||
|
"context line 2",
|
||||||
|
"line 6",
|
||||||
|
"extra line 1",
|
||||||
|
"extra line 2",
|
||||||
|
],
|
||||||
|
expected: { index: 2, confidence: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match even if the anchor is at the beginning of the content",
|
||||||
|
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||||
|
content: ["unique line", "context line 1", "context line 2", "line 6"],
|
||||||
|
expected: { index: 0, confidence: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return a match even if the anchor is at the end of the content",
|
||||||
|
searchStr: "unique line\ncontext line 1\ncontext line 2",
|
||||||
|
content: ["line1", "line2", "unique line", "context line 1", "context line 2"],
|
||||||
|
expected: { index: 2, confidence: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return no match if no valid anchor is found",
|
||||||
|
searchStr: "non-unique line\ncontext line 1\ncontext line 2",
|
||||||
|
content: ["line1", "line2", "non-unique line", "context line 1", "context line 2", "non-unique line"],
|
||||||
|
expected: { index: -1, confidence: 0 },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
anchorTestCases.forEach(({ name, searchStr, content, startIndex, expected }) => {
|
||||||
|
it(name, () => {
|
||||||
|
const result = findAnchorMatch(searchStr, content, startIndex)
|
||||||
|
expect(result.index).toBe(expected.index)
|
||||||
|
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||||
|
expect(result.strategy).toBe("anchor")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("findSimilarityMatch", () => {
|
||||||
|
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||||
|
if (!strategies?.includes("similarity")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it(name, () => {
|
||||||
|
const result = findSimilarityMatch(searchStr, content, startIndex)
|
||||||
|
expect(result.index).toBe(expected.index)
|
||||||
|
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||||
|
expect(result.strategy).toBe("similarity")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("findLevenshteinMatch", () => {
|
||||||
|
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
|
||||||
|
if (!strategies?.includes("levenshtein")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it(name, () => {
|
||||||
|
const result = findLevenshteinMatch(searchStr, content, startIndex)
|
||||||
|
expect(result.index).toBe(expected.index)
|
||||||
|
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
|
||||||
|
expect(result.strategy).toBe("levenshtein")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
305
src/core/diff/strategies/new-unified/edit-strategies.ts
Normal file
305
src/core/diff/strategies/new-unified/edit-strategies.ts
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import { diff_match_patch } from "diff-match-patch"
|
||||||
|
import { EditResult, Hunk } from "./types"
|
||||||
|
import { getDMPSimilarity, validateEditResult } from "./search-strategies"
|
||||||
|
import * as path from "path"
|
||||||
|
import simpleGit, { SimpleGit } from "simple-git"
|
||||||
|
import * as tmp from "tmp"
|
||||||
|
import * as fs from "fs"
|
||||||
|
|
||||||
|
// Helper function to infer indentation - simplified version
|
||||||
|
function inferIndentation(line: string, contextLines: string[], previousIndent: string = ""): string {
|
||||||
|
// If the line has explicit indentation in the change, use it exactly
|
||||||
|
const lineMatch = line.match(/^(\s+)/)
|
||||||
|
if (lineMatch) {
|
||||||
|
return lineMatch[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have context lines, use the indentation from the first context line
|
||||||
|
const contextLine = contextLines[0]
|
||||||
|
if (contextLine) {
|
||||||
|
const contextMatch = contextLine.match(/^(\s+)/)
|
||||||
|
if (contextMatch) {
|
||||||
|
return contextMatch[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to previous indent
|
||||||
|
return previousIndent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context matching edit strategy
|
||||||
|
export function applyContextMatching(
|
||||||
|
hunk: Hunk,
|
||||||
|
content: string[],
|
||||||
|
matchPosition: number,
|
||||||
|
): EditResult {
|
||||||
|
if (matchPosition === -1) {
|
||||||
|
return { confidence: 0, result: content, strategy: "context" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const newResult = [...content.slice(0, matchPosition)]
|
||||||
|
let sourceIndex = matchPosition
|
||||||
|
|
||||||
|
for (const change of hunk.changes) {
|
||||||
|
if (change.type === "context") {
|
||||||
|
// Use the original line from content if available
|
||||||
|
if (sourceIndex < content.length) {
|
||||||
|
newResult.push(content[sourceIndex])
|
||||||
|
} else {
|
||||||
|
const line = change.indent ? change.indent + change.content : change.content
|
||||||
|
newResult.push(line)
|
||||||
|
}
|
||||||
|
sourceIndex++
|
||||||
|
} else if (change.type === "add") {
|
||||||
|
// Use exactly the indentation from the change
|
||||||
|
const baseIndent = change.indent || ""
|
||||||
|
|
||||||
|
// Handle multi-line additions
|
||||||
|
const lines = change.content.split("\n").map((line) => {
|
||||||
|
// If the line already has indentation, preserve it relative to the base indent
|
||||||
|
const lineIndentMatch = line.match(/^(\s*)(.*)/)
|
||||||
|
if (lineIndentMatch) {
|
||||||
|
const [, lineIndent, content] = lineIndentMatch
|
||||||
|
// Only add base indent if the line doesn't already have it
|
||||||
|
return lineIndent ? line : baseIndent + content
|
||||||
|
}
|
||||||
|
return baseIndent + line
|
||||||
|
})
|
||||||
|
|
||||||
|
newResult.push(...lines)
|
||||||
|
} else if (change.type === "remove") {
|
||||||
|
// Handle multi-line removes by incrementing sourceIndex for each line
|
||||||
|
const removedLines = change.content.split("\n").length
|
||||||
|
sourceIndex += removedLines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append remaining content
|
||||||
|
newResult.push(...content.slice(sourceIndex))
|
||||||
|
|
||||||
|
// Calculate confidence based on the actual changes
|
||||||
|
const afterText = newResult.slice(matchPosition, newResult.length - (content.length - sourceIndex)).join("\n")
|
||||||
|
|
||||||
|
const confidence = validateEditResult(hunk, afterText)
|
||||||
|
|
||||||
|
return {
|
||||||
|
confidence,
|
||||||
|
result: newResult,
|
||||||
|
strategy: "context"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DMP edit strategy
|
||||||
|
export function applyDMP(
|
||||||
|
hunk: Hunk,
|
||||||
|
content: string[],
|
||||||
|
matchPosition: number,
|
||||||
|
): EditResult {
|
||||||
|
if (matchPosition === -1) {
|
||||||
|
return { confidence: 0, result: content, strategy: "dmp" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const dmp = new diff_match_patch()
|
||||||
|
|
||||||
|
// Calculate total lines in before block accounting for multi-line content
|
||||||
|
const beforeLineCount = hunk.changes
|
||||||
|
.filter((change) => change.type === "context" || change.type === "remove")
|
||||||
|
.reduce((count, change) => count + change.content.split("\n").length, 0)
|
||||||
|
|
||||||
|
// Build BEFORE block (context + removals)
|
||||||
|
const beforeLines = hunk.changes
|
||||||
|
.filter((change) => change.type === "context" || change.type === "remove")
|
||||||
|
.map((change) => {
|
||||||
|
if (change.originalLine) {
|
||||||
|
return change.originalLine
|
||||||
|
}
|
||||||
|
return change.indent ? change.indent + change.content : change.content
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build AFTER block (context + additions)
|
||||||
|
const afterLines = hunk.changes
|
||||||
|
.filter((change) => change.type === "context" || change.type === "add")
|
||||||
|
.map((change) => {
|
||||||
|
if (change.originalLine) {
|
||||||
|
return change.originalLine
|
||||||
|
}
|
||||||
|
return change.indent ? change.indent + change.content : change.content
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert to text with proper line endings
|
||||||
|
const beforeText = beforeLines.join("\n")
|
||||||
|
const afterText = afterLines.join("\n")
|
||||||
|
|
||||||
|
// Create and apply patch
|
||||||
|
const patch = dmp.patch_make(beforeText, afterText)
|
||||||
|
const targetText = content.slice(matchPosition, matchPosition + beforeLineCount).join("\n")
|
||||||
|
const [patchedText] = dmp.patch_apply(patch, targetText)
|
||||||
|
|
||||||
|
// Split result and preserve line endings
|
||||||
|
const patchedLines = patchedText.split("\n")
|
||||||
|
|
||||||
|
// Construct final result
|
||||||
|
const newResult = [
|
||||||
|
...content.slice(0, matchPosition),
|
||||||
|
...patchedLines,
|
||||||
|
...content.slice(matchPosition + beforeLineCount),
|
||||||
|
]
|
||||||
|
|
||||||
|
const confidence = validateEditResult(hunk, patchedText)
|
||||||
|
|
||||||
|
return {
|
||||||
|
confidence,
|
||||||
|
result: newResult,
|
||||||
|
strategy: "dmp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git fallback strategy that works with full content
|
||||||
|
export async function applyGitFallback(hunk: Hunk, content: string[]): Promise<EditResult> {
|
||||||
|
let tmpDir: tmp.DirResult | undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
tmpDir = tmp.dirSync({ unsafeCleanup: true })
|
||||||
|
const git: SimpleGit = simpleGit(tmpDir.name)
|
||||||
|
|
||||||
|
await git.init()
|
||||||
|
await git.addConfig("user.name", "Temp")
|
||||||
|
await git.addConfig("user.email", "temp@example.com")
|
||||||
|
|
||||||
|
const filePath = path.join(tmpDir.name, "file.txt")
|
||||||
|
|
||||||
|
const searchLines = hunk.changes
|
||||||
|
.filter((change) => change.type === "context" || change.type === "remove")
|
||||||
|
.map((change) => change.originalLine || change.indent + change.content)
|
||||||
|
|
||||||
|
const replaceLines = hunk.changes
|
||||||
|
.filter((change) => change.type === "context" || change.type === "add")
|
||||||
|
.map((change) => change.originalLine || change.indent + change.content)
|
||||||
|
|
||||||
|
const searchText = searchLines.join("\n")
|
||||||
|
const replaceText = replaceLines.join("\n")
|
||||||
|
const originalText = content.join("\n")
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(filePath, originalText)
|
||||||
|
await git.add("file.txt")
|
||||||
|
const originalCommit = await git.commit("original")
|
||||||
|
console.log("Strategy 1 - Original commit:", originalCommit.commit)
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, searchText)
|
||||||
|
await git.add("file.txt")
|
||||||
|
const searchCommit1 = await git.commit("search")
|
||||||
|
console.log("Strategy 1 - Search commit:", searchCommit1.commit)
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, replaceText)
|
||||||
|
await git.add("file.txt")
|
||||||
|
const replaceCommit = await git.commit("replace")
|
||||||
|
console.log("Strategy 1 - Replace commit:", replaceCommit.commit)
|
||||||
|
|
||||||
|
console.log("Strategy 1 - Attempting checkout of:", originalCommit.commit)
|
||||||
|
await git.raw(["checkout", originalCommit.commit])
|
||||||
|
try {
|
||||||
|
console.log("Strategy 1 - Attempting cherry-pick of:", replaceCommit.commit)
|
||||||
|
await git.raw(["cherry-pick", "--minimal", replaceCommit.commit])
|
||||||
|
|
||||||
|
const newText = fs.readFileSync(filePath, "utf-8")
|
||||||
|
const newLines = newText.split("\n")
|
||||||
|
return {
|
||||||
|
confidence: 1,
|
||||||
|
result: newLines,
|
||||||
|
strategy: "git-fallback",
|
||||||
|
}
|
||||||
|
} catch (cherryPickError) {
|
||||||
|
console.error("Strategy 1 failed with merge conflict")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Strategy 1 failed:", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await git.init()
|
||||||
|
await git.addConfig("user.name", "Temp")
|
||||||
|
await git.addConfig("user.email", "temp@example.com")
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, searchText)
|
||||||
|
await git.add("file.txt")
|
||||||
|
const searchCommit = await git.commit("search")
|
||||||
|
const searchHash = searchCommit.commit.replace(/^HEAD /, "")
|
||||||
|
console.log("Strategy 2 - Search commit:", searchHash)
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, replaceText)
|
||||||
|
await git.add("file.txt")
|
||||||
|
const replaceCommit = await git.commit("replace")
|
||||||
|
const replaceHash = replaceCommit.commit.replace(/^HEAD /, "")
|
||||||
|
console.log("Strategy 2 - Replace commit:", replaceHash)
|
||||||
|
|
||||||
|
console.log("Strategy 2 - Attempting checkout of:", searchHash)
|
||||||
|
await git.raw(["checkout", searchHash])
|
||||||
|
fs.writeFileSync(filePath, originalText)
|
||||||
|
await git.add("file.txt")
|
||||||
|
const originalCommit2 = await git.commit("original")
|
||||||
|
console.log("Strategy 2 - Original commit:", originalCommit2.commit)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Strategy 2 - Attempting cherry-pick of:", replaceHash)
|
||||||
|
await git.raw(["cherry-pick", "--minimal", replaceHash])
|
||||||
|
|
||||||
|
const newText = fs.readFileSync(filePath, "utf-8")
|
||||||
|
const newLines = newText.split("\n")
|
||||||
|
return {
|
||||||
|
confidence: 1,
|
||||||
|
result: newLines,
|
||||||
|
strategy: "git-fallback",
|
||||||
|
}
|
||||||
|
} catch (cherryPickError) {
|
||||||
|
console.error("Strategy 2 failed with merge conflict")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Strategy 2 failed:", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Git fallback failed")
|
||||||
|
return { confidence: 0, result: content, strategy: "git-fallback" }
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Git fallback strategy failed:", error)
|
||||||
|
return { confidence: 0, result: content, strategy: "git-fallback" }
|
||||||
|
} finally {
|
||||||
|
if (tmpDir) {
|
||||||
|
tmpDir.removeCallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main edit function that tries strategies sequentially
|
||||||
|
export async function applyEdit(
|
||||||
|
hunk: Hunk,
|
||||||
|
content: string[],
|
||||||
|
matchPosition: number,
|
||||||
|
confidence: number,
|
||||||
|
confidenceThreshold: number = 0.97
|
||||||
|
): Promise<EditResult> {
|
||||||
|
// Don't attempt regular edits if confidence is too low
|
||||||
|
if (confidence < confidenceThreshold) {
|
||||||
|
console.log(
|
||||||
|
`Search confidence (${confidence}) below minimum threshold (${confidenceThreshold}), trying git fallback...`
|
||||||
|
)
|
||||||
|
return applyGitFallback(hunk, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try each strategy in sequence until one succeeds
|
||||||
|
const strategies = [
|
||||||
|
{ name: "dmp", apply: () => applyDMP(hunk, content, matchPosition) },
|
||||||
|
{ name: "context", apply: () => applyContextMatching(hunk, content, matchPosition) },
|
||||||
|
{ name: "git-fallback", apply: () => applyGitFallback(hunk, content) },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Try strategies sequentially until one succeeds
|
||||||
|
for (const strategy of strategies) {
|
||||||
|
const result = await strategy.apply()
|
||||||
|
if (result.confidence >= confidenceThreshold) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { confidence: 0, result: content, strategy: "none" }
|
||||||
|
}
|
||||||
359
src/core/diff/strategies/new-unified/index.ts
Normal file
359
src/core/diff/strategies/new-unified/index.ts
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import { Diff, Hunk, Change } from "./types"
|
||||||
|
import { findBestMatch, prepareSearchString } from "./search-strategies"
|
||||||
|
import { applyEdit } from "./edit-strategies"
|
||||||
|
import { DiffResult, DiffStrategy } from "../../types"
|
||||||
|
|
||||||
|
export class NewUnifiedDiffStrategy implements DiffStrategy {
|
||||||
|
private readonly confidenceThreshold: number
|
||||||
|
|
||||||
|
constructor(confidenceThreshold: number = 1) {
|
||||||
|
this.confidenceThreshold = Math.max(confidenceThreshold, 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseUnifiedDiff(diff: string): Diff {
|
||||||
|
const MAX_CONTEXT_LINES = 6 // Number of context lines to keep before/after changes
|
||||||
|
const lines = diff.split("\n")
|
||||||
|
const hunks: Hunk[] = []
|
||||||
|
let currentHunk: Hunk | null = null
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
while (i < lines.length && !lines[i].startsWith("@@")) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
|
||||||
|
if (line.startsWith("@@")) {
|
||||||
|
if (
|
||||||
|
currentHunk &&
|
||||||
|
currentHunk.changes.length > 0 &&
|
||||||
|
currentHunk.changes.some((change) => change.type === "add" || change.type === "remove")
|
||||||
|
) {
|
||||||
|
const changes = currentHunk.changes
|
||||||
|
let startIdx = 0
|
||||||
|
let endIdx = changes.length - 1
|
||||||
|
|
||||||
|
for (let j = 0; j < changes.length; j++) {
|
||||||
|
if (changes[j].type !== "context") {
|
||||||
|
startIdx = Math.max(0, j - MAX_CONTEXT_LINES)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = changes.length - 1; j >= 0; j--) {
|
||||||
|
if (changes[j].type !== "context") {
|
||||||
|
endIdx = Math.min(changes.length - 1, j + MAX_CONTEXT_LINES)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHunk.changes = changes.slice(startIdx, endIdx + 1)
|
||||||
|
hunks.push(currentHunk)
|
||||||
|
}
|
||||||
|
currentHunk = { changes: [] }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentHunk) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = line.slice(1)
|
||||||
|
const indentMatch = content.match(/^(\s*)/)
|
||||||
|
const indent = indentMatch ? indentMatch[0] : ""
|
||||||
|
const trimmedContent = content.slice(indent.length)
|
||||||
|
|
||||||
|
if (line.startsWith(" ")) {
|
||||||
|
currentHunk.changes.push({
|
||||||
|
type: "context",
|
||||||
|
content: trimmedContent,
|
||||||
|
indent,
|
||||||
|
originalLine: content,
|
||||||
|
})
|
||||||
|
} else if (line.startsWith("+")) {
|
||||||
|
currentHunk.changes.push({
|
||||||
|
type: "add",
|
||||||
|
content: trimmedContent,
|
||||||
|
indent,
|
||||||
|
originalLine: content,
|
||||||
|
})
|
||||||
|
} else if (line.startsWith("-")) {
|
||||||
|
currentHunk.changes.push({
|
||||||
|
type: "remove",
|
||||||
|
content: trimmedContent,
|
||||||
|
indent,
|
||||||
|
originalLine: content,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const finalContent = trimmedContent ? " " + trimmedContent : " "
|
||||||
|
currentHunk.changes.push({
|
||||||
|
type: "context",
|
||||||
|
content: finalContent,
|
||||||
|
indent,
|
||||||
|
originalLine: content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentHunk &&
|
||||||
|
currentHunk.changes.length > 0 &&
|
||||||
|
currentHunk.changes.some((change) => change.type === "add" || change.type === "remove")
|
||||||
|
) {
|
||||||
|
hunks.push(currentHunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hunks }
|
||||||
|
}
|
||||||
|
|
||||||
|
getToolDescription(cwd: string): string {
|
||||||
|
return `# apply_diff Tool Rules:
|
||||||
|
|
||||||
|
Generate a unified diff similar to what "diff -U0" would produce.
|
||||||
|
|
||||||
|
The first two lines must include the file paths, starting with "---" for the original file and "+++" for the updated file. Do not include timestamps with the file paths.
|
||||||
|
|
||||||
|
Each hunk of changes must start with a line containing only "@@ ... @@". Do not include line numbers or ranges in the "@@ ... @@" lines. These are not necessary for the user's patch tool.
|
||||||
|
|
||||||
|
Your output must be a correct, clean patch that applies successfully against the current file contents. Mark all lines that need to be removed or changed with "-". Mark all new or modified lines with "+". Ensure you include all necessary changes; missing or unmarked lines will result in a broken patch.
|
||||||
|
|
||||||
|
Indentation matters! Make sure to preserve the exact indentation of both removed and added lines.
|
||||||
|
|
||||||
|
Start a new hunk for each section of the file that requires changes. However, include only the hunks that contain actual changes. If a hunk consists entirely of unchanged lines, skip it.
|
||||||
|
|
||||||
|
Group related changes together in the same hunk whenever possible. Output hunks in whatever logical order makes the most sense.
|
||||||
|
|
||||||
|
When editing a function, method, loop, or similar code block, replace the *entire* block in one hunk. Use "-" lines to delete the existing block and "+" lines to add the updated block. This ensures accuracy in your diffs.
|
||||||
|
|
||||||
|
If you need to move code within a file, create two hunks: one to delete the code from its original location and another to insert it at the new location.
|
||||||
|
|
||||||
|
To create a new file, show a diff from "--- /dev/null" to "+++ path/to/new/file.ext".
|
||||||
|
|
||||||
|
Format Requirements:
|
||||||
|
|
||||||
|
\`\`\`diff
|
||||||
|
--- mathweb/flask/app.py
|
||||||
|
+++ mathweb/flask/app.py
|
||||||
|
@@ ... @@
|
||||||
|
-class MathWeb:
|
||||||
|
+import sympy
|
||||||
|
|
||||||
|
+
|
||||||
|
+class MathWeb:
|
||||||
|
@@ ... @@
|
||||||
|
-def is_prime(x):
|
||||||
|
- if x < 2:
|
||||||
|
- return False
|
||||||
|
- for i in range(2, int(math.sqrt(x)) + 1):
|
||||||
|
- if x % i == 0:
|
||||||
|
- return False
|
||||||
|
- return True
|
||||||
|
@@ ... @@
|
||||||
|
-@app.route('/prime/<int:n>')
|
||||||
|
-def nth_prime(n):
|
||||||
|
- count = 0
|
||||||
|
- num = 1
|
||||||
|
- while count < n:
|
||||||
|
- num += 1
|
||||||
|
- if is_prime(num):
|
||||||
|
- count += 1
|
||||||
|
- return str(num)
|
||||||
|
+@app.route('/prime/<int:n>')
|
||||||
|
+def nth_prime(n):
|
||||||
|
+ count = 0
|
||||||
|
+ num = 1
|
||||||
|
+ while count < n:
|
||||||
|
+ num += 1
|
||||||
|
+ if sympy.isprime(num):
|
||||||
|
+ count += 1
|
||||||
|
+ return str(num)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Be precise, consistent, and follow these rules carefully to generate correct diffs!
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- path: (required) The path of the file to apply the diff to (relative to the current working directory ${cwd})
|
||||||
|
- diff: (required) The diff content in unified format to apply to the file.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
<apply_diff>
|
||||||
|
<path>File path here</path>
|
||||||
|
<diff>
|
||||||
|
Your diff here
|
||||||
|
</diff>
|
||||||
|
</apply_diff>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to split a hunk into smaller hunks based on contiguous changes
|
||||||
|
private splitHunk(hunk: Hunk): Hunk[] {
|
||||||
|
const result: Hunk[] = []
|
||||||
|
let currentHunk: Hunk | null = null
|
||||||
|
let contextBefore: Change[] = []
|
||||||
|
let contextAfter: Change[] = []
|
||||||
|
const MAX_CONTEXT_LINES = 3 // Keep 3 lines of context before/after changes
|
||||||
|
|
||||||
|
for (let i = 0; i < hunk.changes.length; i++) {
|
||||||
|
const change = hunk.changes[i]
|
||||||
|
|
||||||
|
if (change.type === "context") {
|
||||||
|
if (!currentHunk) {
|
||||||
|
contextBefore.push(change)
|
||||||
|
if (contextBefore.length > MAX_CONTEXT_LINES) {
|
||||||
|
contextBefore.shift()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contextAfter.push(change)
|
||||||
|
if (contextAfter.length > MAX_CONTEXT_LINES) {
|
||||||
|
// We've collected enough context after changes, create a new hunk
|
||||||
|
currentHunk.changes.push(...contextAfter)
|
||||||
|
result.push(currentHunk)
|
||||||
|
currentHunk = null
|
||||||
|
// Keep the last few context lines for the next hunk
|
||||||
|
contextBefore = contextAfter
|
||||||
|
contextAfter = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!currentHunk) {
|
||||||
|
currentHunk = { changes: [...contextBefore] }
|
||||||
|
contextAfter = []
|
||||||
|
} else if (contextAfter.length > 0) {
|
||||||
|
// Add accumulated context to current hunk
|
||||||
|
currentHunk.changes.push(...contextAfter)
|
||||||
|
contextAfter = []
|
||||||
|
}
|
||||||
|
currentHunk.changes.push(change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining changes
|
||||||
|
if (currentHunk) {
|
||||||
|
if (contextAfter.length > 0) {
|
||||||
|
currentHunk.changes.push(...contextAfter)
|
||||||
|
}
|
||||||
|
result.push(currentHunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyDiff(
|
||||||
|
originalContent: string,
|
||||||
|
diffContent: string,
|
||||||
|
startLine?: number,
|
||||||
|
endLine?: number
|
||||||
|
): Promise<DiffResult> {
|
||||||
|
const parsedDiff = this.parseUnifiedDiff(diffContent)
|
||||||
|
const originalLines = originalContent.split("\n")
|
||||||
|
let result = [...originalLines]
|
||||||
|
|
||||||
|
if (!parsedDiff.hunks.length) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "No hunks found in diff. Please ensure your diff includes actual changes and follows the unified diff format.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const hunk of parsedDiff.hunks) {
|
||||||
|
const contextStr = prepareSearchString(hunk.changes)
|
||||||
|
const {
|
||||||
|
index: matchPosition,
|
||||||
|
confidence,
|
||||||
|
strategy,
|
||||||
|
} = findBestMatch(contextStr, result, 0, this.confidenceThreshold)
|
||||||
|
|
||||||
|
if (confidence < this.confidenceThreshold) {
|
||||||
|
console.log("Full hunk application failed, trying sub-hunks strategy")
|
||||||
|
// Try splitting the hunk into smaller hunks
|
||||||
|
const subHunks = this.splitHunk(hunk)
|
||||||
|
let subHunkSuccess = true
|
||||||
|
let subHunkResult = [...result]
|
||||||
|
|
||||||
|
for (const subHunk of subHunks) {
|
||||||
|
const subContextStr = prepareSearchString(subHunk.changes)
|
||||||
|
const subSearchResult = findBestMatch(subContextStr, subHunkResult, 0, this.confidenceThreshold)
|
||||||
|
|
||||||
|
if (subSearchResult.confidence >= this.confidenceThreshold) {
|
||||||
|
const subEditResult = await applyEdit(
|
||||||
|
subHunk,
|
||||||
|
subHunkResult,
|
||||||
|
subSearchResult.index,
|
||||||
|
subSearchResult.confidence,
|
||||||
|
this.confidenceThreshold
|
||||||
|
)
|
||||||
|
if (subEditResult.confidence >= this.confidenceThreshold) {
|
||||||
|
subHunkResult = subEditResult.result
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subHunkSuccess = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subHunkSuccess) {
|
||||||
|
result = subHunkResult
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sub-hunks also failed, return the original error
|
||||||
|
const contextLines = hunk.changes.filter((c) => c.type === "context").length
|
||||||
|
const totalLines = hunk.changes.length
|
||||||
|
const contextRatio = contextLines / totalLines
|
||||||
|
|
||||||
|
let errorMsg = `Failed to find a matching location in the file (${Math.floor(
|
||||||
|
confidence * 100
|
||||||
|
)}% confidence, needs ${Math.floor(this.confidenceThreshold * 100)}%)\n\n`
|
||||||
|
errorMsg += "Debug Info:\n"
|
||||||
|
errorMsg += `- Search Strategy Used: ${strategy}\n`
|
||||||
|
errorMsg += `- Context Lines: ${contextLines} out of ${totalLines} total lines (${Math.floor(
|
||||||
|
contextRatio * 100
|
||||||
|
)}%)\n`
|
||||||
|
errorMsg += `- Attempted to split into ${subHunks.length} sub-hunks but still failed\n`
|
||||||
|
|
||||||
|
if (contextRatio < 0.2) {
|
||||||
|
errorMsg += "\nPossible Issues:\n"
|
||||||
|
errorMsg += "- Not enough context lines to uniquely identify the location\n"
|
||||||
|
errorMsg += "- Add a few more lines of unchanged code around your changes\n"
|
||||||
|
} else if (contextRatio > 0.5) {
|
||||||
|
errorMsg += "\nPossible Issues:\n"
|
||||||
|
errorMsg += "- Too many context lines may reduce search accuracy\n"
|
||||||
|
errorMsg += "- Try to keep only 2-3 lines of context before and after changes\n"
|
||||||
|
} else {
|
||||||
|
errorMsg += "\nPossible Issues:\n"
|
||||||
|
errorMsg += "- The diff may be targeting a different version of the file\n"
|
||||||
|
errorMsg +=
|
||||||
|
"- There may be too many changes in a single hunk, try splitting the changes into multiple hunks\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startLine && endLine) {
|
||||||
|
errorMsg += `\nSearch Range: lines ${startLine}-${endLine}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: errorMsg }
|
||||||
|
}
|
||||||
|
|
||||||
|
const editResult = await applyEdit(hunk, result, matchPosition, confidence, this.confidenceThreshold)
|
||||||
|
if (editResult.confidence >= this.confidenceThreshold) {
|
||||||
|
result = editResult.result
|
||||||
|
} else {
|
||||||
|
// Edit failure - likely due to content mismatch
|
||||||
|
let errorMsg = `Failed to apply the edit using ${editResult.strategy} strategy (${Math.floor(
|
||||||
|
editResult.confidence * 100
|
||||||
|
)}% confidence)\n\n`
|
||||||
|
errorMsg += "Debug Info:\n"
|
||||||
|
errorMsg += "- The location was found but the content didn't match exactly\n"
|
||||||
|
errorMsg += "- This usually means the file has been modified since the diff was created\n"
|
||||||
|
errorMsg += "- Or the diff may be targeting a different version of the file\n"
|
||||||
|
errorMsg += "\nPossible Solutions:\n"
|
||||||
|
errorMsg += "1. Refresh your view of the file and create a new diff\n"
|
||||||
|
errorMsg += "2. Double-check that the removed lines (-) match the current file content\n"
|
||||||
|
errorMsg += "3. Ensure your diff targets the correct version of the file"
|
||||||
|
|
||||||
|
return { success: false, error: errorMsg }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, content: result.join("\n") }
|
||||||
|
}
|
||||||
|
}
|
||||||
408
src/core/diff/strategies/new-unified/search-strategies.ts
Normal file
408
src/core/diff/strategies/new-unified/search-strategies.ts
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
import { compareTwoStrings } from "string-similarity"
|
||||||
|
import { closest } from "fastest-levenshtein"
|
||||||
|
import { diff_match_patch } from "diff-match-patch"
|
||||||
|
import { Change, Hunk } from "./types"
|
||||||
|
|
||||||
|
export type SearchResult = {
|
||||||
|
index: number
|
||||||
|
confidence: number
|
||||||
|
strategy: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LARGE_FILE_THRESHOLD = 1000 // lines
|
||||||
|
const UNIQUE_CONTENT_BOOST = 0.05
|
||||||
|
const DEFAULT_OVERLAP_SIZE = 3 // lines of overlap between windows
|
||||||
|
const MAX_WINDOW_SIZE = 500 // maximum lines in a window
|
||||||
|
|
||||||
|
// Helper function to calculate adaptive confidence threshold based on file size
|
||||||
|
function getAdaptiveThreshold(contentLength: number, baseThreshold: number): number {
|
||||||
|
if (contentLength <= LARGE_FILE_THRESHOLD) {
|
||||||
|
return baseThreshold
|
||||||
|
}
|
||||||
|
return Math.max(baseThreshold - 0.07, 0.8) // Reduce threshold for large files but keep minimum at 80%
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to evaluate content uniqueness
|
||||||
|
function evaluateContentUniqueness(searchStr: string, content: string[]): number {
|
||||||
|
const searchLines = searchStr.split("\n")
|
||||||
|
const uniqueLines = new Set(searchLines)
|
||||||
|
const contentStr = content.join("\n")
|
||||||
|
|
||||||
|
// Calculate how many search lines are relatively unique in the content
|
||||||
|
let uniqueCount = 0
|
||||||
|
for (const line of uniqueLines) {
|
||||||
|
const regex = new RegExp(line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
|
||||||
|
const matches = contentStr.match(regex)
|
||||||
|
if (matches && matches.length <= 2) {
|
||||||
|
// Line appears at most twice
|
||||||
|
uniqueCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueCount / uniqueLines.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to prepare search string from context
|
||||||
|
export function prepareSearchString(changes: Change[]): string {
|
||||||
|
const lines = changes.filter((c) => c.type === "context" || c.type === "remove").map((c) => c.originalLine)
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to evaluate similarity between two texts
|
||||||
|
export function evaluateSimilarity(original: string, modified: string): number {
|
||||||
|
return compareTwoStrings(original, modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to validate using diff-match-patch
|
||||||
|
export function getDMPSimilarity(original: string, modified: string): number {
|
||||||
|
const dmp = new diff_match_patch()
|
||||||
|
const diffs = dmp.diff_main(original, modified)
|
||||||
|
dmp.diff_cleanupSemantic(diffs)
|
||||||
|
const patches = dmp.patch_make(original, diffs)
|
||||||
|
const [expectedText] = dmp.patch_apply(patches, original)
|
||||||
|
|
||||||
|
const similarity = evaluateSimilarity(expectedText, modified)
|
||||||
|
return similarity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to validate edit results using hunk information
|
||||||
|
export function validateEditResult(hunk: Hunk, result: string): number {
|
||||||
|
// Build the expected text from the hunk
|
||||||
|
const expectedText = hunk.changes
|
||||||
|
.filter(change => change.type === "context" || change.type === "add")
|
||||||
|
.map(change => change.indent ? change.indent + change.content : change.content)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
// Calculate similarity between the result and expected text
|
||||||
|
const similarity = getDMPSimilarity(expectedText, result);
|
||||||
|
|
||||||
|
// If the result is unchanged from original, return low confidence
|
||||||
|
const originalText = hunk.changes
|
||||||
|
.filter(change => change.type === "context" || change.type === "remove")
|
||||||
|
.map(change => change.indent ? change.indent + change.content : change.content)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const originalSimilarity = getDMPSimilarity(originalText, result);
|
||||||
|
if (originalSimilarity > 0.97 && similarity !== 1) {
|
||||||
|
return 0.8 * similarity; // Some confidence since we found the right location
|
||||||
|
}
|
||||||
|
|
||||||
|
// For partial matches, scale the confidence but keep it high if we're close
|
||||||
|
return similarity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to validate context lines against original content
|
||||||
|
function validateContextLines(searchStr: string, content: string, confidenceThreshold: number): number {
|
||||||
|
// Extract just the context lines from the search string
|
||||||
|
const contextLines = searchStr.split("\n").filter((line) => !line.startsWith("-")) // Exclude removed lines
|
||||||
|
|
||||||
|
// Compare context lines with content
|
||||||
|
const similarity = evaluateSimilarity(contextLines.join("\n"), content)
|
||||||
|
|
||||||
|
// Get adaptive threshold based on content size
|
||||||
|
const threshold = getAdaptiveThreshold(content.split("\n").length, confidenceThreshold)
|
||||||
|
|
||||||
|
// Calculate uniqueness boost
|
||||||
|
const uniquenessScore = evaluateContentUniqueness(searchStr, content.split("\n"))
|
||||||
|
const uniquenessBoost = uniquenessScore * UNIQUE_CONTENT_BOOST
|
||||||
|
|
||||||
|
// Adjust confidence based on threshold and uniqueness
|
||||||
|
return similarity < threshold ? similarity * 0.3 + uniquenessBoost : similarity + uniquenessBoost
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create overlapping windows
|
||||||
|
function createOverlappingWindows(
|
||||||
|
content: string[],
|
||||||
|
searchSize: number,
|
||||||
|
overlapSize: number = DEFAULT_OVERLAP_SIZE
|
||||||
|
): { window: string[]; startIndex: number }[] {
|
||||||
|
const windows: { window: string[]; startIndex: number }[] = []
|
||||||
|
|
||||||
|
// Ensure minimum window size is at least searchSize
|
||||||
|
const effectiveWindowSize = Math.max(searchSize, Math.min(searchSize * 2, MAX_WINDOW_SIZE))
|
||||||
|
|
||||||
|
// Ensure overlap size doesn't exceed window size
|
||||||
|
const effectiveOverlapSize = Math.min(overlapSize, effectiveWindowSize - 1)
|
||||||
|
|
||||||
|
// Calculate step size, ensure it's at least 1
|
||||||
|
const stepSize = Math.max(1, effectiveWindowSize - effectiveOverlapSize)
|
||||||
|
|
||||||
|
for (let i = 0; i < content.length; i += stepSize) {
|
||||||
|
const windowContent = content.slice(i, i + effectiveWindowSize)
|
||||||
|
if (windowContent.length >= searchSize) {
|
||||||
|
windows.push({ window: windowContent, startIndex: i })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return windows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to combine overlapping matches
|
||||||
|
function combineOverlappingMatches(
|
||||||
|
matches: (SearchResult & { windowIndex: number })[],
|
||||||
|
overlapSize: number = DEFAULT_OVERLAP_SIZE
|
||||||
|
): SearchResult[] {
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort matches by confidence
|
||||||
|
matches.sort((a, b) => b.confidence - a.confidence)
|
||||||
|
|
||||||
|
const combinedMatches: SearchResult[] = []
|
||||||
|
const usedIndices = new Set<number>()
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
if (usedIndices.has(match.windowIndex)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find overlapping matches
|
||||||
|
const overlapping = matches.filter(
|
||||||
|
(m) =>
|
||||||
|
Math.abs(m.windowIndex - match.windowIndex) === 1 &&
|
||||||
|
Math.abs(m.index - match.index) <= overlapSize &&
|
||||||
|
!usedIndices.has(m.windowIndex)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (overlapping.length > 0) {
|
||||||
|
// Boost confidence if we find same match in overlapping windows
|
||||||
|
const avgConfidence =
|
||||||
|
(match.confidence + overlapping.reduce((sum, m) => sum + m.confidence, 0)) / (overlapping.length + 1)
|
||||||
|
const boost = Math.min(0.05 * overlapping.length, 0.1) // Max 10% boost
|
||||||
|
|
||||||
|
combinedMatches.push({
|
||||||
|
index: match.index,
|
||||||
|
confidence: Math.min(1, avgConfidence + boost),
|
||||||
|
strategy: `${match.strategy}-overlapping`,
|
||||||
|
})
|
||||||
|
|
||||||
|
usedIndices.add(match.windowIndex)
|
||||||
|
overlapping.forEach((m) => usedIndices.add(m.windowIndex))
|
||||||
|
} else {
|
||||||
|
combinedMatches.push({
|
||||||
|
index: match.index,
|
||||||
|
confidence: match.confidence,
|
||||||
|
strategy: match.strategy,
|
||||||
|
})
|
||||||
|
usedIndices.add(match.windowIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findExactMatch(
|
||||||
|
searchStr: string,
|
||||||
|
content: string[],
|
||||||
|
startIndex: number = 0,
|
||||||
|
confidenceThreshold: number = 0.97
|
||||||
|
): SearchResult {
|
||||||
|
const searchLines = searchStr.split("\n")
|
||||||
|
const windows = createOverlappingWindows(content.slice(startIndex), searchLines.length)
|
||||||
|
const matches: (SearchResult & { windowIndex: number })[] = []
|
||||||
|
|
||||||
|
windows.forEach((windowData, windowIndex) => {
|
||||||
|
const windowStr = windowData.window.join("\n")
|
||||||
|
const exactMatch = windowStr.indexOf(searchStr)
|
||||||
|
|
||||||
|
if (exactMatch !== -1) {
|
||||||
|
const matchedContent = windowData.window
|
||||||
|
.slice(
|
||||||
|
windowStr.slice(0, exactMatch).split("\n").length - 1,
|
||||||
|
windowStr.slice(0, exactMatch).split("\n").length - 1 + searchLines.length
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
const similarity = getDMPSimilarity(searchStr, matchedContent)
|
||||||
|
const contextSimilarity = validateContextLines(searchStr, matchedContent, confidenceThreshold)
|
||||||
|
const confidence = Math.min(similarity, contextSimilarity)
|
||||||
|
|
||||||
|
matches.push({
|
||||||
|
index: startIndex + windowData.startIndex + windowStr.slice(0, exactMatch).split("\n").length - 1,
|
||||||
|
confidence,
|
||||||
|
strategy: "exact",
|
||||||
|
windowIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const combinedMatches = combineOverlappingMatches(matches)
|
||||||
|
return combinedMatches.length > 0 ? combinedMatches[0] : { index: -1, confidence: 0, strategy: "exact" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// String similarity strategy
|
||||||
|
export function findSimilarityMatch(
|
||||||
|
searchStr: string,
|
||||||
|
content: string[],
|
||||||
|
startIndex: number = 0,
|
||||||
|
confidenceThreshold: number = 0.97
|
||||||
|
): SearchResult {
|
||||||
|
const searchLines = searchStr.split("\n")
|
||||||
|
let bestScore = 0
|
||||||
|
let bestIndex = -1
|
||||||
|
|
||||||
|
for (let i = startIndex; i < content.length - searchLines.length + 1; i++) {
|
||||||
|
const windowStr = content.slice(i, i + searchLines.length).join("\n")
|
||||||
|
const score = compareTwoStrings(searchStr, windowStr)
|
||||||
|
if (score > bestScore && score >= confidenceThreshold) {
|
||||||
|
const similarity = getDMPSimilarity(searchStr, windowStr)
|
||||||
|
const contextSimilarity = validateContextLines(searchStr, windowStr, confidenceThreshold)
|
||||||
|
const adjustedScore = Math.min(similarity, contextSimilarity) * score
|
||||||
|
|
||||||
|
if (adjustedScore > bestScore) {
|
||||||
|
bestScore = adjustedScore
|
||||||
|
bestIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: bestIndex,
|
||||||
|
confidence: bestIndex !== -1 ? bestScore : 0,
|
||||||
|
strategy: "similarity",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levenshtein strategy
|
||||||
|
export function findLevenshteinMatch(
|
||||||
|
searchStr: string,
|
||||||
|
content: string[],
|
||||||
|
startIndex: number = 0,
|
||||||
|
confidenceThreshold: number = 0.97
|
||||||
|
): SearchResult {
|
||||||
|
const searchLines = searchStr.split("\n")
|
||||||
|
const candidates = []
|
||||||
|
|
||||||
|
for (let i = startIndex; i < content.length - searchLines.length + 1; i++) {
|
||||||
|
candidates.push(content.slice(i, i + searchLines.length).join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
const closestMatch = closest(searchStr, candidates)
|
||||||
|
const index = startIndex + candidates.indexOf(closestMatch)
|
||||||
|
const similarity = getDMPSimilarity(searchStr, closestMatch)
|
||||||
|
const contextSimilarity = validateContextLines(searchStr, closestMatch, confidenceThreshold)
|
||||||
|
const confidence = Math.min(similarity, contextSimilarity)
|
||||||
|
return {
|
||||||
|
index: confidence === 0 ? -1 : index,
|
||||||
|
confidence: index !== -1 ? confidence : 0,
|
||||||
|
strategy: "levenshtein",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { index: -1, confidence: 0, strategy: "levenshtein" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to identify anchor lines
|
||||||
|
function identifyAnchors(searchStr: string): { first: string | null; last: string | null } {
|
||||||
|
const searchLines = searchStr.split("\n")
|
||||||
|
let first: string | null = null
|
||||||
|
let last: string | null = null
|
||||||
|
|
||||||
|
// Find the first non-empty line
|
||||||
|
for (const line of searchLines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
first = line
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the last non-empty line
|
||||||
|
for (let i = searchLines.length - 1; i >= 0; i--) {
|
||||||
|
if (searchLines[i].trim()) {
|
||||||
|
last = searchLines[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { first, last }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anchor-based search strategy
|
||||||
|
export function findAnchorMatch(
|
||||||
|
searchStr: string,
|
||||||
|
content: string[],
|
||||||
|
startIndex: number = 0,
|
||||||
|
confidenceThreshold: number = 0.97
|
||||||
|
): SearchResult {
|
||||||
|
const searchLines = searchStr.split("\n")
|
||||||
|
const { first, last } = identifyAnchors(searchStr)
|
||||||
|
|
||||||
|
if (!first || !last) {
|
||||||
|
return { index: -1, confidence: 0, strategy: "anchor" }
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstIndex = -1
|
||||||
|
let lastIndex = -1
|
||||||
|
|
||||||
|
// Check if the first anchor is unique
|
||||||
|
let firstOccurrences = 0
|
||||||
|
for (const contentLine of content) {
|
||||||
|
if (contentLine === first) {
|
||||||
|
firstOccurrences++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstOccurrences !== 1) {
|
||||||
|
return { index: -1, confidence: 0, strategy: "anchor" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first anchor
|
||||||
|
for (let i = startIndex; i < content.length; i++) {
|
||||||
|
if (content[i] === first) {
|
||||||
|
firstIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the last anchor
|
||||||
|
for (let i = content.length - 1; i >= startIndex; i--) {
|
||||||
|
if (content[i] === last) {
|
||||||
|
lastIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstIndex === -1 || lastIndex === -1 || lastIndex <= firstIndex) {
|
||||||
|
return { index: -1, confidence: 0, strategy: "anchor" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the context
|
||||||
|
const expectedContext = searchLines.slice(searchLines.indexOf(first) + 1, searchLines.indexOf(last)).join("\n")
|
||||||
|
const actualContext = content.slice(firstIndex + 1, lastIndex).join("\n")
|
||||||
|
const contextSimilarity = evaluateSimilarity(expectedContext, actualContext)
|
||||||
|
|
||||||
|
if (contextSimilarity < getAdaptiveThreshold(content.length, confidenceThreshold)) {
|
||||||
|
return { index: -1, confidence: 0, strategy: "anchor" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const confidence = 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: firstIndex,
|
||||||
|
confidence: confidence,
|
||||||
|
strategy: "anchor",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main search function that tries all strategies
|
||||||
|
export function findBestMatch(
|
||||||
|
searchStr: string,
|
||||||
|
content: string[],
|
||||||
|
startIndex: number = 0,
|
||||||
|
confidenceThreshold: number = 0.97
|
||||||
|
): SearchResult {
|
||||||
|
const strategies = [findExactMatch, findAnchorMatch, findSimilarityMatch, findLevenshteinMatch]
|
||||||
|
|
||||||
|
let bestResult: SearchResult = { index: -1, confidence: 0, strategy: "none" }
|
||||||
|
|
||||||
|
for (const strategy of strategies) {
|
||||||
|
const result = strategy(searchStr, content, startIndex, confidenceThreshold)
|
||||||
|
if (result.confidence > bestResult.confidence) {
|
||||||
|
bestResult = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestResult
|
||||||
|
}
|
||||||
20
src/core/diff/strategies/new-unified/types.ts
Normal file
20
src/core/diff/strategies/new-unified/types.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export type Change = {
|
||||||
|
type: 'context' | 'add' | 'remove';
|
||||||
|
content: string;
|
||||||
|
indent: string;
|
||||||
|
originalLine?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Hunk = {
|
||||||
|
changes: Change[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Diff = {
|
||||||
|
hunks: Hunk[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditResult = {
|
||||||
|
confidence: number;
|
||||||
|
result: string[];
|
||||||
|
strategy: string;
|
||||||
|
};
|
||||||
@@ -127,7 +127,7 @@ Your search/replace content here
|
|||||||
</apply_diff>`
|
</apply_diff>`
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult {
|
async applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise<DiffResult> {
|
||||||
// Extract the search and replace blocks
|
// Extract the search and replace blocks
|
||||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
|
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ Your diff here
|
|||||||
</apply_diff>`
|
</apply_diff>`
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDiff(originalContent: string, diffContent: string): DiffResult {
|
async applyDiff(originalContent: string, diffContent: string): Promise<DiffResult> {
|
||||||
try {
|
try {
|
||||||
const result = applyPatch(originalContent, diffContent)
|
const result = applyPatch(originalContent, diffContent)
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
|
|||||||
@@ -28,5 +28,5 @@ export interface DiffStrategy {
|
|||||||
* @param endLine Optional line number where the search block ends. If not provided, searches the entire file.
|
* @param endLine Optional line number where the search block ends. If not provided, searches the entire file.
|
||||||
* @returns A DiffResult object containing either the successful result or error details
|
* @returns A DiffResult object containing either the successful result or error details
|
||||||
*/
|
*/
|
||||||
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult
|
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise<DiffResult>
|
||||||
}
|
}
|
||||||
@@ -99,6 +99,7 @@ type GlobalStateKey =
|
|||||||
| "modeApiConfigs"
|
| "modeApiConfigs"
|
||||||
| "customPrompts"
|
| "customPrompts"
|
||||||
| "enhancementApiConfigId"
|
| "enhancementApiConfigId"
|
||||||
|
| "experimentalDiffStrategy"
|
||||||
| "autoApprovalEnabled"
|
| "autoApprovalEnabled"
|
||||||
|
|
||||||
export const GlobalFileNames = {
|
export const GlobalFileNames = {
|
||||||
@@ -254,6 +255,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
fuzzyMatchThreshold,
|
fuzzyMatchThreshold,
|
||||||
mode,
|
mode,
|
||||||
customInstructions: globalInstructions,
|
customInstructions: globalInstructions,
|
||||||
|
experimentalDiffStrategy
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||||
@@ -268,7 +270,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
diffEnabled,
|
diffEnabled,
|
||||||
fuzzyMatchThreshold,
|
fuzzyMatchThreshold,
|
||||||
task,
|
task,
|
||||||
images
|
images,
|
||||||
|
undefined,
|
||||||
|
experimentalDiffStrategy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +285,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
fuzzyMatchThreshold,
|
fuzzyMatchThreshold,
|
||||||
mode,
|
mode,
|
||||||
customInstructions: globalInstructions,
|
customInstructions: globalInstructions,
|
||||||
|
experimentalDiffStrategy
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||||
@@ -296,7 +301,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
fuzzyMatchThreshold,
|
fuzzyMatchThreshold,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
historyItem
|
historyItem,
|
||||||
|
experimentalDiffStrategy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1070,6 +1076,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
vscode.window.showErrorMessage("Failed to get list api configuration")
|
vscode.window.showErrorMessage("Failed to get list api configuration")
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case "experimentalDiffStrategy":
|
||||||
|
await this.updateGlobalState("experimentalDiffStrategy", message.bool ?? false)
|
||||||
|
// Update diffStrategy in current Cline instance if it exists
|
||||||
|
if (this.cline) {
|
||||||
|
await this.cline.updateDiffStrategy(message.bool ?? false)
|
||||||
|
}
|
||||||
|
await this.postStateToWebview()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
@@ -1541,7 +1554,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
uiMessagesFilePath: string
|
uiMessagesFilePath: string
|
||||||
apiConversationHistory: Anthropic.MessageParam[]
|
apiConversationHistory: Anthropic.MessageParam[]
|
||||||
}> {
|
}> {
|
||||||
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
|
const history = (await this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
|
||||||
const historyItem = history.find((item) => item.id === id)
|
const historyItem = history.find((item) => item.id === id)
|
||||||
if (historyItem) {
|
if (historyItem) {
|
||||||
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
|
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
|
||||||
@@ -1606,7 +1619,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
|
|
||||||
async deleteTaskFromState(id: string) {
|
async deleteTaskFromState(id: string) {
|
||||||
// Remove the task from history
|
// Remove the task from history
|
||||||
const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
|
const taskHistory = (await this.getGlobalState("taskHistory") as HistoryItem[]) || []
|
||||||
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
|
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
|
||||||
await this.updateGlobalState("taskHistory", updatedTaskHistory)
|
await this.updateGlobalState("taskHistory", updatedTaskHistory)
|
||||||
|
|
||||||
@@ -1647,6 +1660,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
mode,
|
mode,
|
||||||
customPrompts,
|
customPrompts,
|
||||||
enhancementApiConfigId,
|
enhancementApiConfigId,
|
||||||
|
experimentalDiffStrategy,
|
||||||
autoApprovalEnabled,
|
autoApprovalEnabled,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
@@ -1687,6 +1701,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
mode: mode ?? codeMode,
|
mode: mode ?? codeMode,
|
||||||
customPrompts: customPrompts ?? {},
|
customPrompts: customPrompts ?? {},
|
||||||
enhancementApiConfigId,
|
enhancementApiConfigId,
|
||||||
|
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1803,6 +1818,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
modeApiConfigs,
|
modeApiConfigs,
|
||||||
customPrompts,
|
customPrompts,
|
||||||
enhancementApiConfigId,
|
enhancementApiConfigId,
|
||||||
|
experimentalDiffStrategy,
|
||||||
autoApprovalEnabled,
|
autoApprovalEnabled,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||||
@@ -1864,6 +1880,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
|
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
|
||||||
this.getGlobalState("customPrompts") as Promise<CustomPrompts | undefined>,
|
this.getGlobalState("customPrompts") as Promise<CustomPrompts | undefined>,
|
||||||
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
||||||
|
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
|
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1969,13 +1986,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
modeApiConfigs: modeApiConfigs ?? {} as Record<Mode, string>,
|
modeApiConfigs: modeApiConfigs ?? {} as Record<Mode, string>,
|
||||||
customPrompts: customPrompts ?? {},
|
customPrompts: customPrompts ?? {},
|
||||||
enhancementApiConfigId,
|
enhancementApiConfigId,
|
||||||
|
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
|
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
|
||||||
const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || []
|
const history = (await this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
|
||||||
const existingItemIndex = history.findIndex((h) => h.id === item.id)
|
const existingItemIndex = history.findIndex((h) => h.id === item.id)
|
||||||
|
|
||||||
if (existingItemIndex !== -1) {
|
if (existingItemIndex !== -1) {
|
||||||
history[existingItemIndex] = item
|
history[existingItemIndex] = item
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -610,6 +610,8 @@ describe('ClineProvider', () => {
|
|||||||
true,
|
true,
|
||||||
1.0,
|
1.0,
|
||||||
'Test task',
|
'Test task',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export interface ExtensionState {
|
|||||||
mode: Mode
|
mode: Mode
|
||||||
modeApiConfigs?: Record<Mode, string>
|
modeApiConfigs?: Record<Mode, string>
|
||||||
enhancementApiConfigId?: string
|
enhancementApiConfigId?: string
|
||||||
|
experimentalDiffStrategy?: boolean
|
||||||
autoApprovalEnabled?: boolean
|
autoApprovalEnabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export interface WebviewMessage {
|
|||||||
| "getSystemPrompt"
|
| "getSystemPrompt"
|
||||||
| "systemPrompt"
|
| "systemPrompt"
|
||||||
| "enhancementApiConfigId"
|
| "enhancementApiConfigId"
|
||||||
|
| "experimentalDiffStrategy"
|
||||||
| "autoApprovalEnabled"
|
| "autoApprovalEnabled"
|
||||||
text?: string
|
text?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
listApiConfigMeta,
|
listApiConfigMeta,
|
||||||
mode,
|
mode,
|
||||||
setMode,
|
setMode,
|
||||||
|
experimentalDiffStrategy,
|
||||||
|
setExperimentalDiffStrategy,
|
||||||
} = useExtensionState()
|
} = useExtensionState()
|
||||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||||
@@ -103,6 +105,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
apiConfiguration
|
apiConfiguration
|
||||||
})
|
})
|
||||||
vscode.postMessage({ type: "mode", text: mode })
|
vscode.postMessage({ type: "mode", text: mode })
|
||||||
|
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
|
||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,7 +331,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
<VSCodeCheckbox checked={diffEnabled} onChange={(e: any) => setDiffEnabled(e.target.checked)}>
|
<VSCodeCheckbox checked={diffEnabled} onChange={(e: any) => {
|
||||||
|
setDiffEnabled(e.target.checked)
|
||||||
|
if (!e.target.checked) {
|
||||||
|
// Reset experimental strategy when diffs are disabled
|
||||||
|
setExperimentalDiffStrategy(false)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
|
<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
|
||||||
</VSCodeCheckbox>
|
</VSCodeCheckbox>
|
||||||
<p
|
<p
|
||||||
@@ -342,6 +351,19 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
|
|
||||||
{diffEnabled && (
|
{diffEnabled && (
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
|
||||||
|
<VSCodeCheckbox
|
||||||
|
checked={experimentalDiffStrategy}
|
||||||
|
onChange={(e: any) => setExperimentalDiffStrategy(e.target.checked)}>
|
||||||
|
<span style={{ fontWeight: "500" }}>Use experimental unified diff strategy</span>
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
|
||||||
|
Enable the experimental unified diff strategy. This strategy might reduce the number of retries caused by model errors but may cause unexpected behavior or incorrect edits.
|
||||||
|
Only enable if you understand the risks and are willing to carefully review all changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
<span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
|
<span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
|
||||||
<input
|
<input
|
||||||
@@ -363,7 +385,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
|
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: "12px", marginBottom: 10, color: "var(--vscode-descriptionForeground)" }}>
|
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
|
||||||
This slider controls how precisely code sections must match when applying diffs. Lower values allow more flexible matching but increase the risk of incorrect replacements. Use values below 100% with extreme caution.
|
This slider controls how precisely code sections must match when applying diffs. Lower values allow more flexible matching but increase the risk of incorrect replacements. Use values below 100% with extreme caution.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
setCustomPrompts: (value: CustomPrompts) => void
|
setCustomPrompts: (value: CustomPrompts) => void
|
||||||
enhancementApiConfigId?: string
|
enhancementApiConfigId?: string
|
||||||
setEnhancementApiConfigId: (value: string) => void
|
setEnhancementApiConfigId: (value: string) => void
|
||||||
|
experimentalDiffStrategy: boolean
|
||||||
|
setExperimentalDiffStrategy: (value: boolean) => void
|
||||||
autoApprovalEnabled?: boolean
|
autoApprovalEnabled?: boolean
|
||||||
setAutoApprovalEnabled: (value: boolean) => void
|
setAutoApprovalEnabled: (value: boolean) => void
|
||||||
}
|
}
|
||||||
@@ -93,6 +95,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
mode: codeMode,
|
mode: codeMode,
|
||||||
customPrompts: defaultPrompts,
|
customPrompts: defaultPrompts,
|
||||||
enhancementApiConfigId: '',
|
enhancementApiConfigId: '',
|
||||||
|
experimentalDiffStrategy: false,
|
||||||
autoApprovalEnabled: false,
|
autoApprovalEnabled: false,
|
||||||
})
|
})
|
||||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||||
@@ -211,6 +214,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||||
writeDelayMs: state.writeDelayMs,
|
writeDelayMs: state.writeDelayMs,
|
||||||
screenshotQuality: state.screenshotQuality,
|
screenshotQuality: state.screenshotQuality,
|
||||||
|
experimentalDiffStrategy: state.experimentalDiffStrategy ?? false,
|
||||||
setApiConfiguration: (value) => setState((prevState) => ({
|
setApiConfiguration: (value) => setState((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
apiConfiguration: value
|
apiConfiguration: value
|
||||||
@@ -241,6 +245,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
||||||
setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })),
|
setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })),
|
||||||
setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
||||||
|
setExperimentalDiffStrategy: (value) => setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value })),
|
||||||
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user