mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Add option for parallel diff edits
This commit is contained in:
parent
7845c8a296
commit
4058c2bca3
15 changed files with 1068 additions and 68 deletions
5
.changeset/tame-buses-unite.md
Normal file
5
.changeset/tame-buses-unite.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Add option for parallel diff edits
|
||||
73
.clinerules
73
.clinerules
|
|
@ -1 +1,72 @@
|
|||
- Before attempting completion, always make sure that any code changes have test coverage and that the tests pass.
|
||||
# Code Quality Rules
|
||||
|
||||
1. Test Coverage:
|
||||
- Before attempting completion, always make sure that any code changes have test coverage
|
||||
- Ensure all tests pass before submitting changes
|
||||
|
||||
2. Git Commits:
|
||||
- When finishing a task, always output a git commit command
|
||||
- Include a descriptive commit message that follows conventional commit format
|
||||
|
||||
# Adding a New Settings Checkbox
|
||||
|
||||
To add a new settings checkbox that persists its state, follow these steps:
|
||||
|
||||
1. Add the message type to WebviewMessage.ts:
|
||||
- Add the setting name to the WebviewMessage type's type union
|
||||
- Example: `| "multisearchDiffEnabled"`
|
||||
|
||||
2. Add the setting to ExtensionStateContext.tsx:
|
||||
- Add the setting to the ExtensionStateContextType interface
|
||||
- Add the setter function to the interface
|
||||
- Add the setting to the initial state in useState
|
||||
- Add the setting to the contextValue object
|
||||
- Example:
|
||||
```typescript
|
||||
interface ExtensionStateContextType {
|
||||
multisearchDiffEnabled: boolean;
|
||||
setMultisearchDiffEnabled: (value: boolean) => void;
|
||||
}
|
||||
```
|
||||
|
||||
3. Add the setting to ClineProvider.ts:
|
||||
- Add the setting name to the GlobalStateKey type union
|
||||
- Add the setting to the Promise.all array in getState
|
||||
- Add the setting to the return value in getState with a default value
|
||||
- Add the setting to the destructured variables in getStateToPostToWebview
|
||||
- Add the setting to the return value in getStateToPostToWebview
|
||||
- Add a case in setWebviewMessageListener to handle the setting's message type
|
||||
- Example:
|
||||
```typescript
|
||||
case "multisearchDiffEnabled":
|
||||
await this.updateGlobalState("multisearchDiffEnabled", message.bool)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
```
|
||||
|
||||
4. Add the checkbox UI to SettingsView.tsx:
|
||||
- Import the setting and its setter from ExtensionStateContext
|
||||
- Add the VSCodeCheckbox component with the setting's state and onChange handler
|
||||
- Add appropriate labels and description text
|
||||
- Example:
|
||||
```typescript
|
||||
<VSCodeCheckbox
|
||||
checked={multisearchDiffEnabled}
|
||||
onChange={(e: any) => setMultisearchDiffEnabled(e.target.checked)}
|
||||
>
|
||||
<span style={{ fontWeight: "500" }}>Enable multi-search diff matching</span>
|
||||
</VSCodeCheckbox>
|
||||
```
|
||||
|
||||
5. Add the setting to handleSubmit in SettingsView.tsx:
|
||||
- Add a vscode.postMessage call to send the setting's value when clicking Done
|
||||
- Example:
|
||||
```typescript
|
||||
vscode.postMessage({ type: "multisearchDiffEnabled", bool: multisearchDiffEnabled })
|
||||
```
|
||||
|
||||
These steps ensure that:
|
||||
- The setting's state is properly typed throughout the application
|
||||
- The setting persists between sessions
|
||||
- The setting's value is properly synchronized between the webview and extension
|
||||
- The setting has a proper UI representation in the settings view
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export class Cline {
|
|||
task?: string | undefined,
|
||||
images?: string[] | undefined,
|
||||
historyItem?: HistoryItem | undefined,
|
||||
multisearchDiffEnabled?: boolean,
|
||||
) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.api = buildApiHandler(apiConfiguration)
|
||||
|
|
@ -113,7 +114,7 @@ export class Cline {
|
|||
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.diffStrategy = getDiffStrategy(this.api.getModel().id, fuzzyMatchThreshold ?? 1.0, multisearchDiffEnabled ?? false)
|
||||
}
|
||||
if (historyItem) {
|
||||
this.taskId = historyItem.id
|
||||
|
|
|
|||
|
|
@ -316,8 +316,8 @@ 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();
|
||||
});
|
||||
|
||||
|
|
@ -335,8 +335,8 @@ 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,16 +1,19 @@
|
|||
import type { DiffStrategy } from './types'
|
||||
import { UnifiedDiffStrategy } from './strategies/unified'
|
||||
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
|
||||
import { SearchReplaceMultisearchDiffStrategy } from './strategies/search-replace-multisearch'
|
||||
/**
|
||||
* 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, multisearchDiffEnabled?: boolean): DiffStrategy {
|
||||
// Use SearchReplaceMultisearchDiffStrategy when multisearch diff is enabled
|
||||
// Otherwise fall back to regular SearchReplaceDiffStrategy
|
||||
return multisearchDiffEnabled
|
||||
? new SearchReplaceMultisearchDiffStrategy(fuzzyMatchThreshold ?? 1.0)
|
||||
: new SearchReplaceDiffStrategy(fuzzyMatchThreshold ?? 1.0)
|
||||
}
|
||||
|
||||
export type { DiffStrategy }
|
||||
export { UnifiedDiffStrategy, SearchReplaceDiffStrategy }
|
||||
export { UnifiedDiffStrategy, SearchReplaceDiffStrategy, SearchReplaceMultisearchDiffStrategy }
|
||||
|
|
|
|||
61
src/core/diff/__tests__/utils.test.ts
Normal file
61
src/core/diff/__tests__/utils.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { levenshteinDistance, getSimilarity } from "../utils"
|
||||
|
||||
describe("levenshteinDistance", () => {
|
||||
it("should return 0 for identical strings", () => {
|
||||
expect(levenshteinDistance("hello", "hello")).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle single character differences", () => {
|
||||
expect(levenshteinDistance("hello", "hallo")).toBe(1)
|
||||
})
|
||||
|
||||
it("should handle insertions", () => {
|
||||
expect(levenshteinDistance("hello", "hello!")).toBe(1)
|
||||
})
|
||||
|
||||
it("should handle deletions", () => {
|
||||
expect(levenshteinDistance("hello!", "hello")).toBe(1)
|
||||
})
|
||||
|
||||
it("should handle completely different strings", () => {
|
||||
expect(levenshteinDistance("hello", "world")).toBe(4)
|
||||
})
|
||||
|
||||
it("should handle empty strings", () => {
|
||||
expect(levenshteinDistance("", "")).toBe(0)
|
||||
expect(levenshteinDistance("hello", "")).toBe(5)
|
||||
expect(levenshteinDistance("", "hello")).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSimilarity", () => {
|
||||
it("should return 1 for identical strings", () => {
|
||||
expect(getSimilarity("hello world", "hello world")).toBe(1)
|
||||
})
|
||||
|
||||
it("should handle empty search string", () => {
|
||||
expect(getSimilarity("hello world", "")).toBe(1)
|
||||
})
|
||||
|
||||
it("should normalize whitespace", () => {
|
||||
expect(getSimilarity("hello world", "hello world")).toBe(1)
|
||||
expect(getSimilarity("hello\tworld", "hello world")).toBe(1)
|
||||
expect(getSimilarity("hello\nworld", "hello world")).toBe(1)
|
||||
})
|
||||
|
||||
it("should preserve case sensitivity", () => {
|
||||
expect(getSimilarity("Hello World", "hello world")).toBeLessThan(1)
|
||||
})
|
||||
|
||||
it("should handle partial matches", () => {
|
||||
const similarity = getSimilarity("hello world", "hello there")
|
||||
expect(similarity).toBeGreaterThan(0)
|
||||
expect(similarity).toBeLessThan(1)
|
||||
})
|
||||
|
||||
it("should handle completely different strings", () => {
|
||||
const similarity = getSimilarity("hello world", "goodbye universe")
|
||||
expect(similarity).toBeGreaterThan(0)
|
||||
expect(similarity).toBeLessThan(0.5)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,569 @@
|
|||
import { SearchReplaceMultisearchDiffStrategy } from '../search-replace-multisearch'
|
||||
|
||||
describe('SearchReplaceMultisearchDiffStrategy', () => {
|
||||
describe('constructor', () => {
|
||||
it('should use default values when no parameters provided', () => {
|
||||
const strategy = new SearchReplaceMultisearchDiffStrategy()
|
||||
expect(strategy['fuzzyThreshold']).toBe(1.0)
|
||||
expect(strategy['bufferLines']).toBe(20)
|
||||
})
|
||||
|
||||
it('should use provided values', () => {
|
||||
const strategy = new SearchReplaceMultisearchDiffStrategy(0.9, 10)
|
||||
expect(strategy['fuzzyThreshold']).toBe(0.9)
|
||||
expect(strategy['bufferLines']).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fuzzy matching', () => {
|
||||
let strategy: SearchReplaceMultisearchDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new SearchReplaceMultisearchDiffStrategy(0.8) // 80% similarity threshold
|
||||
})
|
||||
|
||||
it('should match content with small differences (>80% similar)', () => {
|
||||
const originalContent = 'function getData() {\n const results = fetchData();\n return results.filter(Boolean);\n}\n'
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
function getData() {
|
||||
const result = fetchData();
|
||||
return results.filter(Boolean);
|
||||
}
|
||||
=======
|
||||
function getData() {
|
||||
const data = fetchData();
|
||||
return data.filter(Boolean);
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = 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 (<80% similar)', () => {
|
||||
const originalContent = 'function processUsers(data) {\n return data.map(user => user.name);\n}\n'
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
function handleItems(items) {
|
||||
return items.map(item => item.username);
|
||||
}
|
||||
=======
|
||||
function processData(data) {
|
||||
return data.map(d => d.value);
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should normalize whitespace in similarity comparison', () => {
|
||||
const originalContent = 'function sum(a, b) {\n return a + b;\n}\n'
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
=======
|
||||
function sum(a, b) {
|
||||
return a + b + 1;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = 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}\n')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('buffer zone search', () => {
|
||||
let strategy: SearchReplaceMultisearchDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new SearchReplaceMultisearchDiffStrategy(1.0, 5) // Exact matching with 5 line buffer
|
||||
})
|
||||
|
||||
it('should find matches within buffer zone', () => {
|
||||
const originalContent = `
|
||||
function one() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function two() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
function three() {
|
||||
return 3;
|
||||
}`.trim()
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (5)
|
||||
function three() {
|
||||
return 3;
|
||||
}
|
||||
=======
|
||||
function three() {
|
||||
return "three";
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
// Even though we target line 5, it should find the match at lines 9-11
|
||||
// because it's within the 5-line buffer zone
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function one() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function two() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
function three() {
|
||||
return "three";
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not find matches outside buffer zone', () => {
|
||||
const originalContent = `
|
||||
function one() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function two() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
function three() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
function four() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
function five() {
|
||||
return 5;
|
||||
}`.trim()
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (5)
|
||||
function five() {
|
||||
return 5;
|
||||
}
|
||||
=======
|
||||
function five() {
|
||||
return "five";
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
// Targeting line 5, function five() is more than 5 lines away
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('multiple search/replace blocks', () => {
|
||||
let strategy: SearchReplaceMultisearchDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new SearchReplaceMultisearchDiffStrategy()
|
||||
})
|
||||
|
||||
it('should handle overlapping search blocks', () => {
|
||||
const originalContent = `
|
||||
function process() {
|
||||
const data = getData();
|
||||
const result = transform(data);
|
||||
return format(result);
|
||||
}`.trim()
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
function process() {
|
||||
const data = getData();
|
||||
const result = transform(data);
|
||||
=======
|
||||
function process() {
|
||||
const input = getData();
|
||||
const output = transform(input);
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH (3)
|
||||
const result = transform(data);
|
||||
return format(result);
|
||||
}
|
||||
=======
|
||||
const output = transform(input);
|
||||
return format(output);
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
expect(result.error).toContain('Start line must be greater than previous block')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle multiple potential matches', () => {
|
||||
const originalContent = `
|
||||
function log(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
function debug(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
function error(msg) {
|
||||
console.log(msg);
|
||||
}`.trim()
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (2)
|
||||
function log(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
=======
|
||||
function log(msg) {
|
||||
console.log('[LOG]', msg);
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH (6)
|
||||
function debug(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
=======
|
||||
function debug(msg) {
|
||||
console.log('[DEBUG]', msg);
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function log(msg) {
|
||||
console.log('[LOG]', msg);
|
||||
}
|
||||
|
||||
function debug(msg) {
|
||||
console.log('[DEBUG]', msg);
|
||||
}
|
||||
|
||||
function error(msg) {
|
||||
console.log(msg);
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle replacements affecting later matches', () => {
|
||||
const originalContent = `
|
||||
const config = {
|
||||
port: 3000,
|
||||
host: 'localhost',
|
||||
timeout: 5000
|
||||
};
|
||||
|
||||
function getPort() {
|
||||
return config.port;
|
||||
}
|
||||
|
||||
function getTimeout() {
|
||||
return config.timeout;
|
||||
}`.trim()
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
const config = {
|
||||
port: 3000,
|
||||
host: 'localhost',
|
||||
timeout: 5000
|
||||
};
|
||||
=======
|
||||
const CONFIG = {
|
||||
PORT: 3000,
|
||||
HOST: 'localhost',
|
||||
TIMEOUT: 5000
|
||||
};
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH (8)
|
||||
function getPort() {
|
||||
return config.port;
|
||||
}
|
||||
=======
|
||||
function getPort() {
|
||||
return CONFIG.PORT;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`const CONFIG = {
|
||||
PORT: 3000,
|
||||
HOST: 'localhost',
|
||||
TIMEOUT: 5000
|
||||
};
|
||||
|
||||
function getPort() {
|
||||
return CONFIG.PORT;
|
||||
}
|
||||
|
||||
function getTimeout() {
|
||||
return config.timeout;
|
||||
}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('line number adjustments', () => {
|
||||
let strategy: SearchReplaceMultisearchDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new SearchReplaceMultisearchDiffStrategy()
|
||||
})
|
||||
|
||||
it('should adjust line numbers for subsequent blocks when lines are added', () => {
|
||||
const originalContent = `
|
||||
function one() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function two() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
function three() {
|
||||
return 3;
|
||||
}`.trim()
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
function one() {
|
||||
return 1;
|
||||
}
|
||||
=======
|
||||
function one() {
|
||||
console.log("Starting...");
|
||||
return 1;
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH (5)
|
||||
function two() {
|
||||
return 2;
|
||||
}
|
||||
=======
|
||||
function two() {
|
||||
console.log("Processing...");
|
||||
return 2;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function one() {
|
||||
console.log("Starting...");
|
||||
return 1;
|
||||
}
|
||||
|
||||
function two() {
|
||||
console.log("Processing...");
|
||||
return 2;
|
||||
}
|
||||
|
||||
function three() {
|
||||
return 3;
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should adjust line numbers for subsequent blocks when lines are removed', () => {
|
||||
const originalContent = `
|
||||
function one() {
|
||||
// Debug line 1
|
||||
// Debug line 2
|
||||
return 1;
|
||||
}
|
||||
|
||||
function two() {
|
||||
return 2;
|
||||
}`.trim()
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
function one() {
|
||||
// Debug line 1
|
||||
// Debug line 2
|
||||
return 1;
|
||||
}
|
||||
=======
|
||||
function one() {
|
||||
return 1;
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH (7)
|
||||
function two() {
|
||||
return 2;
|
||||
}
|
||||
=======
|
||||
function two() {
|
||||
console.log("Processing...");
|
||||
return 2;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function one() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function two() {
|
||||
console.log("Processing...");
|
||||
return 2;
|
||||
}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
let strategy: SearchReplaceMultisearchDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new SearchReplaceMultisearchDiffStrategy()
|
||||
})
|
||||
|
||||
it('should return error for invalid line range', () => {
|
||||
const originalContent = 'function test() {\n return true;\n}\n'
|
||||
const diffContent = `<<<<<<< SEARCH (5)
|
||||
function test() {
|
||||
return true;
|
||||
}
|
||||
=======
|
||||
function test() {
|
||||
return false;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
expect(result.error).toContain('Line range 5-7 is invalid')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return error for empty search content', () => {
|
||||
const originalContent = 'function test() {\n return true;\n}\n'
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
=======
|
||||
function test() {
|
||||
return false;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
expect(result.error).toContain('Empty search content is not allowed')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return error with debug info when no match found', () => {
|
||||
const originalContent = 'function test() {\n return true;\n}\n'
|
||||
const diffContent = `<<<<<<< SEARCH (1)
|
||||
function test() {
|
||||
return different;
|
||||
}
|
||||
=======
|
||||
function test() {
|
||||
return false;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
expect(result.error).toContain('Debug Info:')
|
||||
expect(result.error).toContain('Similarity Score:')
|
||||
expect(result.error).toContain('Required Threshold:')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('indentation handling', () => {
|
||||
let strategy: SearchReplaceMultisearchDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new SearchReplaceMultisearchDiffStrategy()
|
||||
})
|
||||
|
||||
it('should preserve indentation when adding lines', () => {
|
||||
const originalContent = `
|
||||
class Example {
|
||||
constructor() {
|
||||
this.value = 0;
|
||||
}
|
||||
}`.trim()
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (2)
|
||||
constructor() {
|
||||
this.value = 0;
|
||||
}
|
||||
=======
|
||||
constructor() {
|
||||
// Initialize value
|
||||
this.value = 0;
|
||||
this.ready = true;
|
||||
}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`class Example {
|
||||
constructor() {
|
||||
// Initialize value
|
||||
this.value = 0;
|
||||
this.ready = true;
|
||||
}
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle mixed indentation styles', () => {
|
||||
const originalContent = `class Example {
|
||||
\tconstructor() {
|
||||
\t this.value = 0;
|
||||
\t}
|
||||
}`
|
||||
|
||||
const diffContent = `<<<<<<< SEARCH (2)
|
||||
\tconstructor() {
|
||||
\t this.value = 0;
|
||||
\t}
|
||||
=======
|
||||
\tconstructor() {
|
||||
\t // Add comment
|
||||
\t this.value = 1;
|
||||
\t}
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`class Example {
|
||||
\tconstructor() {
|
||||
\t // Add comment
|
||||
\t this.value = 1;
|
||||
\t}
|
||||
}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
248
src/core/diff/strategies/search-replace-multisearch.ts
Normal file
248
src/core/diff/strategies/search-replace-multisearch.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import { DiffStrategy, DiffResult } from "../types"
|
||||
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
|
||||
import { getSimilarity } from "../utils"
|
||||
|
||||
const BUFFER_LINES = 20; // Number of extra context lines to show before and after matches
|
||||
|
||||
export class SearchReplaceMultisearchDiffStrategy implements DiffStrategy {
|
||||
private fuzzyThreshold: number;
|
||||
private bufferLines: number;
|
||||
|
||||
constructor(fuzzyThreshold?: number, bufferLines?: number) {
|
||||
// Use provided threshold or default to exact matching (1.0)
|
||||
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
|
||||
this.bufferLines = bufferLines ?? BUFFER_LINES;
|
||||
}
|
||||
|
||||
getToolDescription(cwd: string): string {
|
||||
return `## apply_diff
|
||||
Description: Request to replace existing code using a search and replace block.
|
||||
This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with.
|
||||
The tool will maintain proper indentation and formatting while making changes.
|
||||
Multiple search/replace blocks can be specified in a single diff, but they must be in order.
|
||||
The SEARCH section must exactly match existing content including whitespace and indentation.
|
||||
If you're not confident in the exact content to search for, use the read_file tool first to get the exact content.
|
||||
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
|
||||
- diff: (required) The search/replace block defining the changes.
|
||||
|
||||
Line Number Behavior:
|
||||
- Line numbers are specified in the SEARCH marker: <<<<<<< SEARCH (start_line)
|
||||
- For multiple blocks, line numbers are automatically adjusted based on lines added/removed by previous blocks
|
||||
- Example: If block 1 adds 2 lines and block 2's target was at line 10, it will be automatically adjusted to line 12
|
||||
|
||||
Diff format:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH (start_line)
|
||||
[exact content to find including whitespace]
|
||||
=======
|
||||
[new content to replace with]
|
||||
>>>>>>> REPLACE
|
||||
\`\`\`
|
||||
|
||||
Example with multiple blocks:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH (1)
|
||||
function one() {
|
||||
return 1;
|
||||
}
|
||||
=======
|
||||
function one() {
|
||||
console.log("Starting...");
|
||||
return 1;
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
<<<<<<< SEARCH (5)
|
||||
function two() {
|
||||
return 2;
|
||||
}
|
||||
=======
|
||||
function two() {
|
||||
console.log("Processing...");
|
||||
return 2;
|
||||
}
|
||||
>>>>>>> REPLACE
|
||||
\`\`\`
|
||||
|
||||
In this example:
|
||||
1. First block starts at line 1 and matches 3 lines (the function definition)
|
||||
2. First block adds 1 line (console.log), so subsequent line numbers are shifted by +1
|
||||
3. Second block starts at line 5, but is automatically adjusted to line 6 due to the previous +1 shift
|
||||
|
||||
Usage:
|
||||
<apply_diff>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
[search/replace blocks here]
|
||||
</diff>
|
||||
</apply_diff>`
|
||||
}
|
||||
|
||||
applyDiff(originalContent: string, diffContent: string): DiffResult {
|
||||
// Extract all search and replace blocks with start line numbers and compute end lines
|
||||
const blockPattern = /<<<<<<< SEARCH \((\d+)(?:-\d+)?\)\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/g;
|
||||
const rawBlocks = Array.from(diffContent.matchAll(blockPattern));
|
||||
const blocks = rawBlocks.map(([full, startStr, searchContent, replaceContent]) => {
|
||||
const start = parseInt(startStr, 10);
|
||||
const searchLines = searchContent.split(/\r?\n/);
|
||||
const end = start + searchLines.length - 1;
|
||||
return [full, startStr, end.toString(), searchContent, replaceContent];
|
||||
});
|
||||
|
||||
// Validate blocks
|
||||
if (blocks.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid diff format - missing required SEARCH/REPLACE sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH (start-end)\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers and line numbers"
|
||||
};
|
||||
}
|
||||
|
||||
let prevEnd = -1;
|
||||
|
||||
// Then validate individual blocks
|
||||
for (const block of blocks) {
|
||||
const [_, startStr, endStr, searchContent] = block;
|
||||
// Check for empty search content
|
||||
const searchLines = searchContent.split(/\r?\n/);
|
||||
if (searchLines.length === 0 || searchLines.every(line => line.trim() === '')) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Empty search content is not allowed\n\nDebug Info:\n- Each SEARCH block must contain content to match"
|
||||
};
|
||||
}
|
||||
|
||||
// Validate line numbers
|
||||
const startLine = parseInt(startStr, 10);
|
||||
const endLine = parseInt(endStr, 10);
|
||||
if (startLine < 1 || endLine < startLine || startLine <= prevEnd) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Invalid line range ${startLine}-${endLine}\n\nDebug Info:\n- Start line must be >= 1\n- End line must be >= start line\n- Start line must be greater than previous block's end line`
|
||||
};
|
||||
}
|
||||
|
||||
prevEnd = endLine;
|
||||
}
|
||||
|
||||
// Process blocks sequentially
|
||||
let lineAdjustment = 0;
|
||||
let currentContent = originalContent;
|
||||
const lineEnding = currentContent.includes('\r\n') ? '\r\n' : '\n';
|
||||
|
||||
for (const [_, startStr, endStr, searchContent, replaceContent] of blocks) {
|
||||
let currentSearchContent = searchContent;
|
||||
let currentReplaceContent = replaceContent;
|
||||
|
||||
// Parse line numbers and apply adjustment
|
||||
const startLine = parseInt(startStr, 10);
|
||||
const endLine = parseInt(endStr, 10);
|
||||
const adjustedStartLine = startLine + lineAdjustment;
|
||||
const adjustedEndLine = endLine + lineAdjustment;
|
||||
|
||||
// Split content into lines for validation
|
||||
const originalLines = currentContent.split(/\r?\n/);
|
||||
|
||||
// Validate line range
|
||||
if (adjustedStartLine < 1 || adjustedEndLine > originalLines.length) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Line range ${startLine}-${endLine} is invalid\n\nDebug Info:\n- Original Range: lines ${startLine}-${endLine}\n- Adjusted Range: lines ${adjustedStartLine}-${adjustedEndLine}\n- Line Adjustment: ${lineAdjustment}\n- File Bounds: lines 1-${originalLines.length}`
|
||||
};
|
||||
}
|
||||
|
||||
// Strip line numbers if present
|
||||
if (everyLineHasLineNumbers(currentSearchContent) && everyLineHasLineNumbers(currentReplaceContent)) {
|
||||
currentSearchContent = stripLineNumbers(currentSearchContent);
|
||||
currentReplaceContent = stripLineNumbers(currentReplaceContent);
|
||||
}
|
||||
|
||||
// Split search and replace content into lines
|
||||
const searchLines = currentSearchContent.split(/\r?\n/);
|
||||
const replaceLines = currentReplaceContent.split(/\r?\n/);
|
||||
|
||||
// Initialize search variables
|
||||
let matchIndex = -1;
|
||||
let bestMatchScore = 0;
|
||||
let bestMatchContent = "";
|
||||
const searchChunk = searchLines.join('\n');
|
||||
|
||||
// Try exact match at adjusted line first
|
||||
const exactMatchChunk = originalLines.slice(adjustedStartLine - 1, adjustedEndLine).join('\n');
|
||||
const exactMatchScore = getSimilarity(exactMatchChunk, searchChunk);
|
||||
if (exactMatchScore >= this.fuzzyThreshold) {
|
||||
matchIndex = adjustedStartLine - 1;
|
||||
bestMatchScore = exactMatchScore;
|
||||
bestMatchContent = exactMatchChunk;
|
||||
}
|
||||
|
||||
// If exact match fails, try buffer zone
|
||||
if (matchIndex === -1) {
|
||||
// Search within buffer zone
|
||||
const searchStartIndex = Math.max(0, adjustedStartLine - this.bufferLines - 1);
|
||||
const searchEndIndex = Math.min(originalLines.length - searchLines.length + 1, adjustedEndLine + this.bufferLines);
|
||||
|
||||
// Sequential search through buffer zone
|
||||
for (let i = searchStartIndex; i <= searchEndIndex; i++) {
|
||||
const originalChunk = originalLines.slice(i, i + searchLines.length).join('\n');
|
||||
const similarity = getSimilarity(originalChunk, searchChunk);
|
||||
if (similarity >= this.fuzzyThreshold && similarity > bestMatchScore) {
|
||||
bestMatchScore = similarity;
|
||||
matchIndex = i;
|
||||
bestMatchContent = originalChunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no match found, fail with debug info
|
||||
if (matchIndex === -1) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No matches found within buffer zone\n\nDebug Info:\n- Buffer Zone: ${this.bufferLines} lines\n- Target Line: ${startLine}\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: lines ${adjustedStartLine}-${adjustedEndLine}\n- Content to match:\n${searchChunk}\n- Best match found:\n${bestMatchContent}`
|
||||
};
|
||||
}
|
||||
|
||||
// Get matched lines and handle indentation
|
||||
const matchedLines = originalLines.slice(matchIndex, matchIndex + searchLines.length);
|
||||
const originalIndents = matchedLines.map((line: string) => {
|
||||
const match = line.match(/^[\t ]*/);
|
||||
return match ? match[0] : '';
|
||||
});
|
||||
|
||||
const searchIndents = searchLines.map((line: string) => {
|
||||
const match = line.match(/^[\t ]*/);
|
||||
return match ? match[0] : '';
|
||||
});
|
||||
|
||||
const indentedReplaceLines = replaceLines.map((line: string, i: number) => {
|
||||
const matchedIndent = originalIndents[0] || '';
|
||||
const currentIndentMatch = line.match(/^[\t ]*/);
|
||||
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : '';
|
||||
const searchBaseIndent = searchIndents[0] || '';
|
||||
|
||||
const searchBaseLevel = searchBaseIndent.length;
|
||||
const currentLevel = currentIndent.length;
|
||||
const relativeLevel = currentLevel - searchBaseLevel;
|
||||
|
||||
const finalIndent = relativeLevel < 0
|
||||
? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
|
||||
: matchedIndent + currentIndent.slice(searchBaseLevel);
|
||||
|
||||
return finalIndent + line.trim();
|
||||
});
|
||||
|
||||
// Update content and line adjustment for next iteration
|
||||
const beforeMatch = originalLines.slice(0, matchIndex);
|
||||
const afterMatch = originalLines.slice(matchIndex + searchLines.length);
|
||||
currentContent = [...beforeMatch, ...indentedReplaceLines, ...afterMatch].join(lineEnding);
|
||||
|
||||
// Update line adjustment for next block
|
||||
lineAdjustment += replaceLines.length - searchLines.length;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: currentContent
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +1,9 @@
|
|||
import { DiffStrategy, DiffResult } from "../types"
|
||||
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
|
||||
import { getSimilarity } from "../utils"
|
||||
|
||||
const BUFFER_LINES = 20; // Number of extra context lines to show before and after matches
|
||||
|
||||
function levenshteinDistance(a: string, b: string): number {
|
||||
const matrix: number[][] = [];
|
||||
|
||||
// Initialize matrix
|
||||
for (let i = 0; i <= a.length; i++) {
|
||||
matrix[i] = [i];
|
||||
}
|
||||
for (let j = 0; j <= b.length; j++) {
|
||||
matrix[0][j] = j;
|
||||
}
|
||||
|
||||
// Fill matrix
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
if (a[i-1] === b[j-1]) {
|
||||
matrix[i][j] = matrix[i-1][j-1];
|
||||
} else {
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i-1][j-1] + 1, // substitution
|
||||
matrix[i][j-1] + 1, // insertion
|
||||
matrix[i-1][j] + 1 // deletion
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[a.length][b.length];
|
||||
}
|
||||
|
||||
function getSimilarity(original: string, search: string): number {
|
||||
if (search === '') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Normalize strings by removing extra whitespace but preserve case
|
||||
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const normalizedOriginal = normalizeStr(original);
|
||||
const normalizedSearch = normalizeStr(search);
|
||||
|
||||
if (normalizedOriginal === normalizedSearch) { return 1; }
|
||||
|
||||
// Calculate Levenshtein distance
|
||||
const distance = levenshteinDistance(normalizedOriginal, normalizedSearch);
|
||||
|
||||
// Calculate similarity ratio (0 to 1, where 1 is exact match)
|
||||
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length);
|
||||
return 1 - (distance / maxLength);
|
||||
}
|
||||
|
||||
export class SearchReplaceDiffStrategy implements DiffStrategy {
|
||||
private fuzzyThreshold: number;
|
||||
private bufferLines: number;
|
||||
|
|
|
|||
49
src/core/diff/utils.ts
Normal file
49
src/core/diff/utils.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
export function levenshteinDistance(a: string, b: string): number {
|
||||
const matrix: number[][] = [];
|
||||
|
||||
// Initialize matrix
|
||||
for (let i = 0; i <= a.length; i++) {
|
||||
matrix[i] = [i];
|
||||
}
|
||||
for (let j = 0; j <= b.length; j++) {
|
||||
matrix[0][j] = j;
|
||||
}
|
||||
|
||||
// Fill matrix
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
if (a[i-1] === b[j-1]) {
|
||||
matrix[i][j] = matrix[i-1][j-1];
|
||||
} else {
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i-1][j-1] + 1, // substitution
|
||||
matrix[i][j-1] + 1, // insertion
|
||||
matrix[i-1][j] + 1 // deletion
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[a.length][b.length];
|
||||
}
|
||||
|
||||
export function getSimilarity(original: string, search: string): number {
|
||||
if (search === '') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Normalize strings by removing extra whitespace but preserve case
|
||||
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const normalizedOriginal = normalizeStr(original);
|
||||
const normalizedSearch = normalizeStr(search);
|
||||
|
||||
if (normalizedOriginal === normalizedSearch) { return 1; }
|
||||
|
||||
// Calculate Levenshtein distance
|
||||
const distance = levenshteinDistance(normalizedOriginal, normalizedSearch);
|
||||
|
||||
// Calculate similarity ratio (0 to 1, where 1 is exact match)
|
||||
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length);
|
||||
return 1 - (distance / maxLength);
|
||||
}
|
||||
|
|
@ -71,6 +71,7 @@ type GlobalStateKey =
|
|||
| "alwaysAllowMcp"
|
||||
| "browserLargeViewport"
|
||||
| "fuzzyMatchThreshold"
|
||||
| "multisearchDiffEnabled"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
|
|
@ -219,7 +220,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold
|
||||
fuzzyMatchThreshold,
|
||||
multisearchDiffEnabled
|
||||
} = await this.getState()
|
||||
|
||||
this.cline = new Cline(
|
||||
|
|
@ -229,7 +231,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
diffEnabled,
|
||||
fuzzyMatchThreshold,
|
||||
task,
|
||||
images
|
||||
images,
|
||||
undefined,
|
||||
multisearchDiffEnabled
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -239,7 +243,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
fuzzyMatchThreshold
|
||||
fuzzyMatchThreshold,
|
||||
multisearchDiffEnabled
|
||||
} = await this.getState()
|
||||
|
||||
this.cline = new Cline(
|
||||
|
|
@ -250,7 +255,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
fuzzyMatchThreshold,
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem
|
||||
historyItem,
|
||||
multisearchDiffEnabled
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -622,6 +628,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
await this.updateGlobalState("fuzzyMatchThreshold", message.value)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "multisearchDiffEnabled":
|
||||
await this.updateGlobalState("multisearchDiffEnabled", message.bool)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
|
@ -937,9 +947,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
|
||||
async getStateToPostToWebview() {
|
||||
const {
|
||||
apiConfiguration,
|
||||
lastShownAnnouncementId,
|
||||
const {
|
||||
apiConfiguration,
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowWrite,
|
||||
|
|
@ -951,6 +961,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
taskHistory,
|
||||
soundVolume,
|
||||
browserLargeViewport,
|
||||
multisearchDiffEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
const allowedCommands = vscode.workspace
|
||||
|
|
@ -977,6 +988,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
allowedCommands,
|
||||
soundVolume: soundVolume ?? 0.5,
|
||||
browserLargeViewport: browserLargeViewport ?? false,
|
||||
multisearchDiffEnabled: multisearchDiffEnabled ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1072,6 +1084,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
soundVolume,
|
||||
browserLargeViewport,
|
||||
fuzzyMatchThreshold,
|
||||
multisearchDiffEnabled,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||
|
|
@ -1112,6 +1125,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
this.getGlobalState("soundVolume") as Promise<number | undefined>,
|
||||
this.getGlobalState("browserLargeViewport") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
|
||||
this.getGlobalState("multisearchDiffEnabled") as Promise<boolean | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
|
|
@ -1170,6 +1184,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
soundVolume,
|
||||
browserLargeViewport: browserLargeViewport ?? false,
|
||||
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
|
||||
multisearchDiffEnabled: multisearchDiffEnabled ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface ExtensionMessage {
|
|||
| "partialMessage"
|
||||
| "openRouterModels"
|
||||
| "mcpServers"
|
||||
| "multilineDiffEnabled"
|
||||
text?: string
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
|
|
@ -34,6 +35,7 @@ export interface ExtensionMessage {
|
|||
partialMessage?: ClineMessage
|
||||
openRouterModels?: Record<string, ModelInfo>
|
||||
mcpServers?: McpServer[]
|
||||
bool?: boolean
|
||||
}
|
||||
|
||||
export interface ExtensionState {
|
||||
|
|
@ -55,6 +57,7 @@ export interface ExtensionState {
|
|||
diffEnabled?: boolean
|
||||
browserLargeViewport?: boolean
|
||||
fuzzyMatchThreshold?: number
|
||||
multisearchDiffEnabled?: boolean
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export interface WebviewMessage {
|
|||
| "toggleToolAlwaysAllow"
|
||||
| "toggleMcpServer"
|
||||
| "fuzzyMatchThreshold"
|
||||
| "multisearchDiffEnabled"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
askResponse?: ClineAskResponse
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
allowedCommands,
|
||||
fuzzyMatchThreshold,
|
||||
setFuzzyMatchThreshold,
|
||||
multisearchDiffEnabled,
|
||||
setMultisearchDiffEnabled,
|
||||
} = useExtensionState()
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
|
|
@ -67,6 +69,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
|
||||
vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport })
|
||||
vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
|
||||
vscode.postMessage({ type: "multisearchDiffEnabled", bool: multisearchDiffEnabled })
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
|
@ -365,6 +368,22 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{diffEnabled && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<VSCodeCheckbox checked={multisearchDiffEnabled} onChange={(e: any) => setMultisearchDiffEnabled(e.target.checked)}>
|
||||
<span style={{ fontWeight: "500" }}>Enable parallel diff edits</span>
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
When enabled, Cline will attempt to apply multiple diff edits in parallel, making editing faster.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 5 }}>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setDiffEnabled: (value: boolean) => void
|
||||
setBrowserLargeViewport: (value: boolean) => void
|
||||
setFuzzyMatchThreshold: (value: number) => void
|
||||
setMultisearchDiffEnabled: (value: boolean) => void
|
||||
}
|
||||
|
||||
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
|
@ -48,6 +49,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
soundVolume: 0.5,
|
||||
diffEnabled: false,
|
||||
fuzzyMatchThreshold: 1.0,
|
||||
multisearchDiffEnabled: false,
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
const [showWelcome, setShowWelcome] = useState(false)
|
||||
|
|
@ -136,6 +138,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
filePaths,
|
||||
soundVolume: state.soundVolume,
|
||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||
multisearchDiffEnabled: state.multisearchDiffEnabled,
|
||||
setApiConfiguration: (value) => setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: value
|
||||
|
|
@ -153,6 +156,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
|
||||
setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })),
|
||||
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
|
||||
setMultisearchDiffEnabled: (value: boolean) => setState((prevState) => ({ ...prevState, multisearchDiffEnabled: value })),
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue