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",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/tmp": "^0.2.6",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/vscode": "^1.95.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
@@ -27,7 +28,9 @@
|
||||
"default-shell": "^2.2.0",
|
||||
"delay": "^6.0.0",
|
||||
"diff": "^5.2.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"globby": "^14.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
@@ -39,8 +42,11 @@
|
||||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"sound-play": "^1.1.0",
|
||||
"string-similarity": "^4.0.4",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tmp": "^0.2.3",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
@@ -50,9 +56,11 @@
|
||||
"@changesets/cli": "^2.27.10",
|
||||
"@changesets/types": "^6.0.0",
|
||||
"@types/diff": "^5.2.1",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
@@ -4108,6 +4116,21 @@
|
||||
"@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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz",
|
||||
@@ -6069,6 +6092,13 @@
|
||||
"integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==",
|
||||
"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": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
@@ -6146,6 +6176,19 @@
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||
"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": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz",
|
||||
@@ -7913,6 +7956,12 @@
|
||||
"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": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
||||
@@ -8716,6 +8765,19 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
@@ -8807,6 +8869,15 @@
|
||||
"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": {
|
||||
"version": "1.17.1",
|
||||
"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",
|
||||
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13926,6 +13998,21 @@
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -14202,6 +14289,13 @@
|
||||
"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": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
@@ -14544,15 +14638,12 @@
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"os-tmpdir": "~1.0.2"
|
||||
},
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
|
||||
@@ -202,9 +202,11 @@
|
||||
"@changesets/cli": "^2.27.10",
|
||||
"@changesets/types": "^6.0.0",
|
||||
"@types/diff": "^5.2.1",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
@@ -230,6 +232,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/tmp": "^0.2.6",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/vscode": "^1.95.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
@@ -240,7 +243,9 @@
|
||||
"default-shell": "^2.2.0",
|
||||
"delay": "^6.0.0",
|
||||
"diff": "^5.2.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"globby": "^14.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
@@ -252,8 +257,11 @@
|
||||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"sound-play": "^1.1.0",
|
||||
"string-similarity": "^4.0.4",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tmp": "^0.2.3",
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
|
||||
@@ -52,6 +52,7 @@ import { detectCodeOmission } from "../integrations/editor/detect-omission"
|
||||
import { BrowserSession } from "../services/browser/BrowserSession"
|
||||
import { OpenRouterHandler } from "../api/providers/openrouter"
|
||||
import { McpHub } from "../services/mcp/McpHub"
|
||||
import crypto from "crypto"
|
||||
|
||||
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
|
||||
@@ -71,6 +72,7 @@ export class Cline {
|
||||
customInstructions?: string
|
||||
diffStrategy?: DiffStrategy
|
||||
diffEnabled: boolean = false
|
||||
fuzzyMatchThreshold: number = 1.0
|
||||
|
||||
apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
|
||||
clineMessages: ClineMessage[] = []
|
||||
@@ -105,28 +107,46 @@ export class Cline {
|
||||
fuzzyMatchThreshold?: number,
|
||||
task?: 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.terminalManager = new TerminalManager()
|
||||
this.urlContentFetcher = new UrlContentFetcher(provider.context)
|
||||
this.browserSession = new BrowserSession(provider.context)
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
this.customInstructions = customInstructions
|
||||
this.diffEnabled = enableDiff ?? false
|
||||
if (this.diffEnabled && this.api.getModel().id) {
|
||||
this.diffStrategy = getDiffStrategy(this.api.getModel().id, fuzzyMatchThreshold ?? 1.0)
|
||||
}
|
||||
this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
|
||||
if (historyItem) {
|
||||
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
|
||||
@@ -1326,7 +1346,7 @@ export class Cline {
|
||||
const originalContent = await fs.readFile(absolutePath, "utf-8")
|
||||
|
||||
// Apply the diff to the original content
|
||||
const diffResult = this.diffStrategy?.applyDiff(
|
||||
const diffResult = await this.diffStrategy?.applyDiff(
|
||||
originalContent,
|
||||
diffContent,
|
||||
parseInt(block.params.start_line ?? ''),
|
||||
|
||||
@@ -322,7 +322,7 @@ describe('Cline', () => {
|
||||
|
||||
expect(cline.diffEnabled).toBe(true);
|
||||
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();
|
||||
});
|
||||
@@ -341,7 +341,7 @@ describe('Cline', () => {
|
||||
|
||||
expect(cline.diffEnabled).toBe(true);
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type { DiffStrategy } from './types'
|
||||
import { UnifiedDiffStrategy } from './strategies/unified'
|
||||
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
|
||||
import { NewUnifiedDiffStrategy } from './strategies/new-unified'
|
||||
/**
|
||||
* Get the appropriate diff strategy for the given model
|
||||
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
|
||||
* @returns The appropriate diff strategy for the model
|
||||
*/
|
||||
export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number): DiffStrategy {
|
||||
// For now, return SearchReplaceDiffStrategy for all models
|
||||
// This architecture allows for future optimizations based on model capabilities
|
||||
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold ?? 1.0)
|
||||
export function getDiffStrategy(model: string, fuzzyMatchThreshold?: number, experimentalDiffStrategy: boolean = false): DiffStrategy {
|
||||
if (experimentalDiffStrategy) {
|
||||
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
|
||||
}
|
||||
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
it('should replace matching content', () => {
|
||||
it('should replace matching content', async () => {
|
||||
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -21,14 +21,14 @@ function hello() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -41,14 +41,14 @@ function example() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -61,14 +61,14 @@ function test() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -81,14 +81,14 @@ function test() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -105,14 +105,14 @@ function test() {
|
||||
\t}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -126,14 +126,14 @@ function test() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -148,14 +148,14 @@ function test() {
|
||||
\t}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
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}")
|
||||
}
|
||||
})
|
||||
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -168,14 +168,14 @@ function test() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -188,19 +188,19 @@ function hello() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
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 diffContent = `test.ts\nInvalid diff format`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -215,14 +215,14 @@ function hello() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
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')
|
||||
}
|
||||
})
|
||||
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -235,14 +235,14 @@ function hello() {
|
||||
end
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -255,14 +255,14 @@ function hello() {
|
||||
}}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
class Example {
|
||||
constructor() {
|
||||
@@ -296,7 +296,7 @@ class Example {
|
||||
}
|
||||
>>>>>>> REPLACE`.trim();
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent);
|
||||
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 {
|
||||
constructor() {
|
||||
this.value = 0;
|
||||
@@ -340,7 +340,7 @@ class Example {
|
||||
}
|
||||
>>>>>>> REPLACE`;
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent);
|
||||
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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():
|
||||
if condition:
|
||||
do_something()
|
||||
@@ -376,7 +376,7 @@ class Example {
|
||||
process(item)
|
||||
>>>>>>> REPLACE`;
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent);
|
||||
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 x = 1;
|
||||
|
||||
@@ -409,7 +409,7 @@ class Example {
|
||||
if (x) {
|
||||
>>>>>>> REPLACE`;
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent);
|
||||
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 {
|
||||
method() {
|
||||
if (true) {
|
||||
@@ -450,7 +450,7 @@ class Example {
|
||||
}
|
||||
>>>>>>> REPLACE`;
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent);
|
||||
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 {
|
||||
constructor() {
|
||||
if (true) {
|
||||
@@ -485,7 +485,7 @@ class Example {
|
||||
this.setup();
|
||||
>>>>>>> REPLACE`;
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent);
|
||||
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 {
|
||||
constructor() {
|
||||
if (true) {
|
||||
@@ -514,7 +514,7 @@ class Example {
|
||||
this.init();
|
||||
>>>>>>> REPLACE`;
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent);
|
||||
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 {
|
||||
constructor() {
|
||||
if (true) {
|
||||
@@ -548,7 +548,7 @@ this.init();
|
||||
this.validate();
|
||||
>>>>>>> REPLACE`;
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent);
|
||||
const result = await strategy.applyDiff(originalContent, diffContent);
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function one() {
|
||||
return "target";
|
||||
@@ -595,7 +595,7 @@ function five() {
|
||||
// Search around the middle (function three)
|
||||
// Even though all functions contain the target text,
|
||||
// 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)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function one() {
|
||||
@@ -629,7 +629,7 @@ function five() {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -642,14 +642,14 @@ function five() {
|
||||
3 | }
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -662,14 +662,14 @@ function five() {
|
||||
3 | }
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -682,11 +682,11 @@ function five() {
|
||||
3 | }
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -699,14 +699,14 @@ function five() {
|
||||
3 | |data|updated|
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -719,14 +719,14 @@ function five() {
|
||||
3 | }
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -739,14 +739,14 @@ function five() {
|
||||
22 | }
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -759,14 +759,14 @@ function five() {
|
||||
| Updated
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -779,7 +779,7 @@ function five() {
|
||||
3 | | NewData
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe('1 | | Pipe\n2 | |---|\n3 | | NewData\n')
|
||||
@@ -796,7 +796,7 @@ function five() {
|
||||
})
|
||||
|
||||
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() {
|
||||
console.log("hello");
|
||||
// Comment to remove
|
||||
@@ -808,7 +808,7 @@ function five() {
|
||||
=======
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 {
|
||||
constructor() {
|
||||
// Initialize
|
||||
@@ -838,7 +838,7 @@ function five() {
|
||||
=======
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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() {
|
||||
if (true) {
|
||||
// Remove this
|
||||
@@ -865,7 +865,7 @@ function five() {
|
||||
=======
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function outer() {
|
||||
@@ -878,7 +878,7 @@ function five() {
|
||||
})
|
||||
|
||||
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 x = 1;
|
||||
return x;
|
||||
@@ -889,7 +889,7 @@ function five() {
|
||||
console.log("Adding log");
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 2, 2)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent, 2, 2)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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() {
|
||||
if (true) {
|
||||
const x = 1;
|
||||
@@ -913,7 +913,7 @@ function five() {
|
||||
console.log("After");
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 3, 3)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent, 3, 3)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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() {
|
||||
return true;
|
||||
}`
|
||||
@@ -938,7 +938,7 @@ function five() {
|
||||
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 1, 1)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent, 1, 1)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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() {
|
||||
return true;
|
||||
}`
|
||||
@@ -961,7 +961,7 @@ function test() {
|
||||
// End of file
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 4, 4)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent, 4, 4)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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() {
|
||||
return true;
|
||||
}`
|
||||
@@ -982,7 +982,7 @@ function test() {
|
||||
console.log("test");
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
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
|
||||
})
|
||||
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -1011,14 +1011,14 @@ function getData() {
|
||||
|
||||
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)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -1031,11 +1031,11 @@ function processData(data) {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -1048,14 +1048,14 @@ function sum(a, b) {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
@@ -1065,7 +1065,7 @@ import { a } from "a";
|
||||
function sum(a, b) {
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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)
|
||||
})
|
||||
|
||||
it('should find and replace within specified line range', () => {
|
||||
it('should find and replace within specified line range', async () => {
|
||||
const originalContent = `
|
||||
function one() {
|
||||
return 1;
|
||||
@@ -1105,7 +1105,7 @@ function two() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 5, 7)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent, 5, 7)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function one() {
|
||||
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
|
||||
// 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)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function one() {
|
||||
return 1;
|
||||
@@ -1201,11 +1201,11 @@ function five() {
|
||||
|
||||
// Searching around function two() (lines 5-7)
|
||||
// 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)
|
||||
})
|
||||
|
||||
it('should handle search range at start of file', () => {
|
||||
it('should handle search range at start of file', async () => {
|
||||
const originalContent = `
|
||||
function one() {
|
||||
return 1;
|
||||
@@ -1226,7 +1226,7 @@ function one() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 1, 3)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent, 1, 3)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function one() {
|
||||
return 1;
|
||||
@@ -1260,7 +1260,7 @@ function two() {
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 5, 7)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent, 5, 7)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function processData(data) {
|
||||
return data.map(x => x * 2);
|
||||
@@ -1306,7 +1306,7 @@ function processData(data) {
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
// 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)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function one() {
|
||||
return 1;
|
||||
@@ -1356,7 +1356,7 @@ function three() {
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
// 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)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function one() {
|
||||
return 1;
|
||||
@@ -1399,7 +1399,7 @@ function one() {
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
// 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)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function one() {
|
||||
return 1;
|
||||
@@ -1446,7 +1446,7 @@ function process() {
|
||||
|
||||
// Should match the second instance exactly at lines 10-12
|
||||
// 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)
|
||||
if (result.success) {
|
||||
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 = `
|
||||
function one() {
|
||||
return 1;
|
||||
@@ -1494,7 +1494,7 @@ function process() {
|
||||
|
||||
// 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
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 3, 5)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent, 3, 5)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function one() {
|
||||
@@ -1519,14 +1519,14 @@ function two() {
|
||||
strategy = new SearchReplaceDiffStrategy()
|
||||
})
|
||||
|
||||
it('should include the current working directory', () => {
|
||||
it('should include the current working directory', async () => {
|
||||
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}`)
|
||||
})
|
||||
|
||||
it('should include required format elements', () => {
|
||||
const description = strategy.getToolDescription('/test')
|
||||
it('should include required format elements', async () => {
|
||||
const description = await strategy.getToolDescription('/test')
|
||||
expect(description).toContain('<<<<<<< SEARCH')
|
||||
expect(description).toContain('=======')
|
||||
expect(description).toContain('>>>>>>> REPLACE')
|
||||
@@ -1534,8 +1534,8 @@ function two() {
|
||||
expect(description).toContain('</apply_diff>')
|
||||
})
|
||||
|
||||
it('should document start_line and end_line parameters', () => {
|
||||
const description = strategy.getToolDescription('/test')
|
||||
it('should document start_line and end_line parameters', async () => {
|
||||
const description = await strategy.getToolDescription('/test')
|
||||
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.')
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('UnifiedDiffStrategy', () => {
|
||||
})
|
||||
|
||||
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';
|
||||
|
||||
function calculateTotal(items: number[]): number {
|
||||
@@ -58,14 +58,14 @@ function calculateTotal(items: number[]): number {
|
||||
|
||||
export { calculateTotal };`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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 {
|
||||
add(a: number, b: number): number {
|
||||
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)
|
||||
if (result.success) {
|
||||
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';
|
||||
import { Button } from './components';
|
||||
|
||||
@@ -133,14 +133,14 @@ function App() {
|
||||
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)
|
||||
if (result.success) {
|
||||
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';
|
||||
|
||||
function processFile(path: string) {
|
||||
@@ -198,14 +198,14 @@ async function processFile(path: string) {
|
||||
|
||||
export { processFile };`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle empty original content', () => {
|
||||
it('should handle empty original content', async () => {
|
||||
const originalContent = ''
|
||||
const diffContent = `--- empty.ts
|
||||
+++ empty.ts
|
||||
@@ -218,7 +218,7 @@ export { processFile };`
|
||||
return \`Hello, \${name}!\`;
|
||||
}\n`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
const result = await strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
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>`
|
||||
}
|
||||
|
||||
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
|
||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
|
||||
if (!match) {
|
||||
|
||||
@@ -108,7 +108,7 @@ Your diff here
|
||||
</apply_diff>`
|
||||
}
|
||||
|
||||
applyDiff(originalContent: string, diffContent: string): DiffResult {
|
||||
async applyDiff(originalContent: string, diffContent: string): Promise<DiffResult> {
|
||||
try {
|
||||
const result = applyPatch(originalContent, diffContent)
|
||||
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.
|
||||
* @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"
|
||||
| "customPrompts"
|
||||
| "enhancementApiConfigId"
|
||||
| "experimentalDiffStrategy"
|
||||
| "autoApprovalEnabled"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
@@ -254,6 +255,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
experimentalDiffStrategy
|
||||
} = await this.getState()
|
||||
|
||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||
@@ -268,7 +270,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
images
|
||||
images,
|
||||
undefined,
|
||||
experimentalDiffStrategy
|
||||
)
|
||||
}
|
||||
|
||||
@@ -281,6 +285,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold,
|
||||
mode,
|
||||
customInstructions: globalInstructions,
|
||||
experimentalDiffStrategy
|
||||
} = await this.getState()
|
||||
|
||||
const modeInstructions = customPrompts?.[mode]?.customInstructions
|
||||
@@ -296,7 +301,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
fuzzyMatchThreshold,
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem
|
||||
historyItem,
|
||||
experimentalDiffStrategy
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1070,6 +1076,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
vscode.window.showErrorMessage("Failed to get list api configuration")
|
||||
}
|
||||
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,
|
||||
@@ -1541,7 +1554,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
uiMessagesFilePath: string
|
||||
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)
|
||||
if (historyItem) {
|
||||
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
|
||||
@@ -1606,7 +1619,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
async deleteTaskFromState(id: string) {
|
||||
// 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)
|
||||
await this.updateGlobalState("taskHistory", updatedTaskHistory)
|
||||
|
||||
@@ -1647,6 +1660,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
mode,
|
||||
customPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
autoApprovalEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
@@ -1687,6 +1701,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
mode: mode ?? codeMode,
|
||||
customPrompts: customPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||
}
|
||||
}
|
||||
@@ -1803,6 +1818,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
modeApiConfigs,
|
||||
customPrompts,
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy,
|
||||
autoApprovalEnabled,
|
||||
] = await Promise.all([
|
||||
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("customPrompts") as Promise<CustomPrompts | undefined>,
|
||||
this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
|
||||
this.getGlobalState("experimentalDiffStrategy") 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>,
|
||||
customPrompts: customPrompts ?? {},
|
||||
enhancementApiConfigId,
|
||||
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
|
||||
autoApprovalEnabled: autoApprovalEnabled ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if (existingItemIndex !== -1) {
|
||||
history[existingItemIndex] = item
|
||||
} else {
|
||||
|
||||
@@ -610,6 +610,8 @@ describe('ClineProvider', () => {
|
||||
true,
|
||||
1.0,
|
||||
'Test task',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface ExtensionState {
|
||||
mode: Mode
|
||||
modeApiConfigs?: Record<Mode, string>
|
||||
enhancementApiConfigId?: string
|
||||
experimentalDiffStrategy?: boolean
|
||||
autoApprovalEnabled?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface WebviewMessage {
|
||||
| "getSystemPrompt"
|
||||
| "systemPrompt"
|
||||
| "enhancementApiConfigId"
|
||||
| "experimentalDiffStrategy"
|
||||
| "autoApprovalEnabled"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
|
||||
@@ -61,6 +61,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
listApiConfigMeta,
|
||||
mode,
|
||||
setMode,
|
||||
experimentalDiffStrategy,
|
||||
setExperimentalDiffStrategy,
|
||||
} = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
@@ -103,6 +105,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
apiConfiguration
|
||||
})
|
||||
vscode.postMessage({ type: "mode", text: mode })
|
||||
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
@@ -328,7 +331,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
@@ -342,6 +351,19 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
|
||||
{diffEnabled && (
|
||||
<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' }}>
|
||||
<span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
|
||||
<input
|
||||
@@ -363,7 +385,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
|
||||
</span>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
||||
setCustomPrompts: (value: CustomPrompts) => void
|
||||
enhancementApiConfigId?: string
|
||||
setEnhancementApiConfigId: (value: string) => void
|
||||
experimentalDiffStrategy: boolean
|
||||
setExperimentalDiffStrategy: (value: boolean) => void
|
||||
autoApprovalEnabled?: boolean
|
||||
setAutoApprovalEnabled: (value: boolean) => void
|
||||
}
|
||||
@@ -93,6 +95,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
mode: codeMode,
|
||||
customPrompts: defaultPrompts,
|
||||
enhancementApiConfigId: '',
|
||||
experimentalDiffStrategy: false,
|
||||
autoApprovalEnabled: false,
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
@@ -211,6 +214,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||
writeDelayMs: state.writeDelayMs,
|
||||
screenshotQuality: state.screenshotQuality,
|
||||
experimentalDiffStrategy: state.experimentalDiffStrategy ?? false,
|
||||
setApiConfiguration: (value) => setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: value
|
||||
@@ -241,6 +245,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
|
||||
setCustomPrompts: (value) => setState((prevState) => ({ ...prevState, customPrompts: value })),
|
||||
setEnhancementApiConfigId: (value) => setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
|
||||
setExperimentalDiffStrategy: (value) => setState((prevState) => ({ ...prevState, experimentalDiffStrategy: value })),
|
||||
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user