mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 12:51:17 -05:00
Merge branch 'RooVetGit:main' into main
This commit is contained in:
@@ -97,6 +97,7 @@ export class Cline {
|
||||
apiConfiguration: ApiConfiguration,
|
||||
customInstructions?: string,
|
||||
diffEnabled?: boolean,
|
||||
debugDiffEnabled?: boolean,
|
||||
task?: string,
|
||||
images?: string[],
|
||||
historyItem?: HistoryItem,
|
||||
@@ -109,7 +110,7 @@ export class Cline {
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
this.customInstructions = customInstructions
|
||||
if (diffEnabled && this.api.getModel().id) {
|
||||
this.diffStrategy = getDiffStrategy(this.api.getModel().id)
|
||||
this.diffStrategy = getDiffStrategy(this.api.getModel().id, debugDiffEnabled)
|
||||
}
|
||||
if (historyItem) {
|
||||
this.taskId = historyItem.id
|
||||
@@ -1237,7 +1238,12 @@ export class Cline {
|
||||
const originalContent = await fs.readFile(absolutePath, "utf-8")
|
||||
|
||||
// Apply the diff to the original content
|
||||
const diffResult = this.diffStrategy?.applyDiff(originalContent, diffContent) ?? {
|
||||
const diffResult = this.diffStrategy?.applyDiff(
|
||||
originalContent,
|
||||
diffContent,
|
||||
parseInt(block.params.start_line ?? ''),
|
||||
parseInt(block.params.end_line ?? '')
|
||||
) ?? {
|
||||
success: false,
|
||||
error: "No diff strategy available"
|
||||
}
|
||||
|
||||
@@ -278,7 +278,8 @@ describe('Cline', () => {
|
||||
mockProvider,
|
||||
mockApiConfig,
|
||||
'custom instructions',
|
||||
false,
|
||||
false, // diffEnabled
|
||||
false, // debugDiffEnabled
|
||||
'test task'
|
||||
);
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ export const toolParamNames = [
|
||||
"question",
|
||||
"result",
|
||||
"diff",
|
||||
"start_line",
|
||||
"end_line",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
@@ -6,10 +6,10 @@ import { SearchReplaceDiffStrategy } from './strategies/search-replace'
|
||||
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
|
||||
* @returns The appropriate diff strategy for the model
|
||||
*/
|
||||
export function getDiffStrategy(model: string): DiffStrategy {
|
||||
export function getDiffStrategy(model: string, debugEnabled?: boolean): DiffStrategy {
|
||||
// For now, return SearchReplaceDiffStrategy for all models (with a fuzzy threshold of 0.9)
|
||||
// This architecture allows for future optimizations based on model capabilities
|
||||
return new SearchReplaceDiffStrategy(0.9)
|
||||
return new SearchReplaceDiffStrategy(0.9, debugEnabled)
|
||||
}
|
||||
|
||||
export type { DiffStrategy }
|
||||
|
||||
@@ -591,6 +591,26 @@ this.init();
|
||||
expect(result.content).toBe('function test() {\n return false;\n}\n')
|
||||
}
|
||||
})
|
||||
|
||||
it('should strip line numbers with leading spaces', () => {
|
||||
const originalContent = 'function test() {\n return true;\n}\n'
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
1 | function test() {
|
||||
2 | return true;
|
||||
3 | }
|
||||
=======
|
||||
1 | function test() {
|
||||
2 | return false;
|
||||
3 | }
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = 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', () => {
|
||||
const originalContent = 'function test() {\n return true;\n}\n'
|
||||
@@ -711,6 +731,212 @@ this.init();
|
||||
})
|
||||
});
|
||||
|
||||
describe('insertion/deletion', () => {
|
||||
let strategy: SearchReplaceDiffStrategy
|
||||
|
||||
beforeEach(() => {
|
||||
strategy = new SearchReplaceDiffStrategy()
|
||||
})
|
||||
|
||||
describe('deletion', () => {
|
||||
it('should delete code when replace block is empty', () => {
|
||||
const originalContent = `function test() {
|
||||
console.log("hello");
|
||||
// Comment to remove
|
||||
console.log("world");
|
||||
}`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
// Comment to remove
|
||||
=======
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function test() {
|
||||
console.log("hello");
|
||||
console.log("world");
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should delete multiple lines when replace block is empty', () => {
|
||||
const originalContent = `class Example {
|
||||
constructor() {
|
||||
// Initialize
|
||||
this.value = 0;
|
||||
// Set defaults
|
||||
this.name = "";
|
||||
// End init
|
||||
}
|
||||
}`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
// Initialize
|
||||
this.value = 0;
|
||||
// Set defaults
|
||||
this.name = "";
|
||||
// End init
|
||||
=======
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`class Example {
|
||||
constructor() {
|
||||
}
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should preserve indentation when deleting nested code', () => {
|
||||
const originalContent = `function outer() {
|
||||
if (true) {
|
||||
// Remove this
|
||||
console.log("test");
|
||||
// And this
|
||||
}
|
||||
return true;
|
||||
}`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
// Remove this
|
||||
console.log("test");
|
||||
// And this
|
||||
=======
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function outer() {
|
||||
if (true) {
|
||||
}
|
||||
return true;
|
||||
}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertion', () => {
|
||||
it('should insert code at specified line when search block is empty', () => {
|
||||
const originalContent = `function test() {
|
||||
const x = 1;
|
||||
return x;
|
||||
}`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
console.log("Adding log");
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 2, 2)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function test() {
|
||||
console.log("Adding log");
|
||||
const x = 1;
|
||||
return x;
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should preserve indentation when inserting at nested location', () => {
|
||||
const originalContent = `function test() {
|
||||
if (true) {
|
||||
const x = 1;
|
||||
}
|
||||
}`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
console.log("Before");
|
||||
console.log("After");
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 3, 3)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function test() {
|
||||
if (true) {
|
||||
console.log("Before");
|
||||
console.log("After");
|
||||
const x = 1;
|
||||
}
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle insertion at start of file', () => {
|
||||
const originalContent = `function test() {
|
||||
return true;
|
||||
}`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
// Copyright 2024
|
||||
// License: MIT
|
||||
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 1, 1)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`// Copyright 2024
|
||||
// License: MIT
|
||||
|
||||
function test() {
|
||||
return true;
|
||||
}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle insertion at end of file', () => {
|
||||
const originalContent = `function test() {
|
||||
return true;
|
||||
}`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
|
||||
// End of file
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent, 4, 4)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`function test() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// End of file`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should insert at the start of the file if no start_line is provided for insertion', () => {
|
||||
const originalContent = `function test() {
|
||||
return true;
|
||||
}`
|
||||
const diffContent = `test.ts
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
console.log("test");
|
||||
>>>>>>> REPLACE`
|
||||
|
||||
const result = strategy.applyDiff(originalContent, diffContent)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.content).toBe(`console.log("test");
|
||||
function test() {
|
||||
return true;
|
||||
}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('fuzzy matching', () => {
|
||||
let strategy: SearchReplaceDiffStrategy
|
||||
|
||||
@@ -1241,8 +1467,8 @@ function two() {
|
||||
|
||||
it('should document start_line and end_line parameters', () => {
|
||||
const description = 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.')
|
||||
expect(description).toContain('start_line: (required) The line number where the search block starts (inclusive).')
|
||||
expect(description).toContain('end_line: (required) The line number where the search block ends (inclusive).')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { DiffStrategy, DiffResult } from "../types"
|
||||
import { addLineNumbers } from "../../../integrations/misc/extract-text"
|
||||
|
||||
const BUFFER_LINES = 5; // Number of extra context lines to show before and after matches
|
||||
|
||||
function levenshteinDistance(a: string, b: string): number {
|
||||
const matrix: number[][] = [];
|
||||
@@ -30,6 +33,10 @@ function levenshteinDistance(a: string, b: string): number {
|
||||
}
|
||||
|
||||
function getSimilarity(original: string, search: string): number {
|
||||
if (original === '' || search === '') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Normalize strings by removing extra whitespace but preserve case
|
||||
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
|
||||
|
||||
@@ -48,10 +55,12 @@ function getSimilarity(original: string, search: string): number {
|
||||
|
||||
export class SearchReplaceDiffStrategy implements DiffStrategy {
|
||||
private fuzzyThreshold: number;
|
||||
public debugEnabled: boolean;
|
||||
|
||||
constructor(fuzzyThreshold?: number) {
|
||||
constructor(fuzzyThreshold?: number, debugEnabled?: boolean) {
|
||||
// Default to exact matching (1.0) unless fuzzy threshold specified
|
||||
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
|
||||
this.debugEnabled = debugEnabled ?? false;
|
||||
}
|
||||
|
||||
getToolDescription(cwd: string): string {
|
||||
@@ -66,8 +75,8 @@ If you're not confident in the exact content to search for, use the read_file to
|
||||
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.
|
||||
- start_line: (required) The line number where the search block starts.
|
||||
- end_line: (required) The line number where the search block ends.
|
||||
- start_line: (required) The line number where the search block starts (inclusive).
|
||||
- end_line: (required) The line number where the search block ends (inclusive).
|
||||
|
||||
Diff format:
|
||||
\`\`\`
|
||||
@@ -89,62 +98,107 @@ Original file:
|
||||
5 | return total
|
||||
\`\`\`
|
||||
|
||||
Search/Replace content:
|
||||
1. Search/replace a specific chunk of code:
|
||||
\`\`\`
|
||||
<apply_diff>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
def calculate_total(items):
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item
|
||||
return total
|
||||
=======
|
||||
def calculate_total(items):
|
||||
"""Calculate total with 10% markup"""
|
||||
return sum(item * 1.1 for item in items)
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
<start_line>2</start_line>
|
||||
<end_line>5</end_line>
|
||||
</apply_diff>
|
||||
\`\`\`
|
||||
|
||||
Usage:
|
||||
Result:
|
||||
\`\`\`
|
||||
1 | def calculate_total(items):
|
||||
2 | """Calculate total with 10% markup"""
|
||||
3 | return sum(item * 1.1 for item in items)
|
||||
\`\`\`
|
||||
|
||||
2. Insert code at a specific line (start_line and end_line must be the same, and the content gets inserted before whatever is currently at that line):
|
||||
\`\`\`
|
||||
<apply_diff>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
Your search/replace content here
|
||||
<<<<<<< SEARCH
|
||||
=======
|
||||
"""TODO: Write a test for this"""
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
<start_line>1</start_line>
|
||||
<start_line>2</start_line>
|
||||
<end_line>2</end_line>
|
||||
</apply_diff>
|
||||
\`\`\`
|
||||
|
||||
Result:
|
||||
\`\`\`
|
||||
1 | def calculate_total(items):
|
||||
2 | """TODO: Write a test for this"""
|
||||
3 | """Calculate total with 10% markup"""
|
||||
4 | return sum(item * 1.1 for item in items)
|
||||
\`\`\`
|
||||
|
||||
3. Delete code at a specific line range:
|
||||
\`\`\`
|
||||
<apply_diff>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
<<<<<<< SEARCH
|
||||
total = 0
|
||||
for item in items:
|
||||
total += item
|
||||
return total
|
||||
=======
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
<start_line>2</start_line>
|
||||
<end_line>5</end_line>
|
||||
</apply_diff>`
|
||||
</apply_diff>
|
||||
\`\`\`
|
||||
|
||||
Result:
|
||||
\`\`\`
|
||||
1 | def calculate_total(items):
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult {
|
||||
// Extract the search and replace blocks
|
||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
|
||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
|
||||
if (!match) {
|
||||
// Log detailed format information
|
||||
console.log('Invalid Diff Format Debug:', {
|
||||
expectedFormat: "<<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE",
|
||||
tip: "Make sure to include both SEARCH and REPLACE sections with correct markers"
|
||||
});
|
||||
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers` : '';
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid diff format - missing required SEARCH/REPLACE sections"
|
||||
error: `Invalid diff format - missing required SEARCH/REPLACE sections${debugInfo}`
|
||||
};
|
||||
}
|
||||
|
||||
let [_, searchContent, replaceContent] = match;
|
||||
|
||||
|
||||
// Detect line ending from original content
|
||||
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
|
||||
|
||||
// Strip line numbers from search and replace content if every line starts with a line number
|
||||
const hasLineNumbers = (content: string) => {
|
||||
const lines = content.split(/\r?\n/);
|
||||
return lines.length > 0 && lines.every(line => /^\d+\s+\|(?!\|)/.test(line));
|
||||
return lines.length > 0 && lines.every(line => /^\s*\d+\s+\|(?!\|)/.test(line));
|
||||
};
|
||||
|
||||
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
|
||||
const stripLineNumbers = (content: string) => {
|
||||
return content.replace(/^\d+\s+\|(?!\|)/gm, '')
|
||||
return content.replace(/^\s*\d+\s+\|(?!\|)/gm, '');
|
||||
};
|
||||
|
||||
searchContent = stripLineNumbers(searchContent);
|
||||
@@ -152,8 +206,8 @@ Your search/replace content here
|
||||
}
|
||||
|
||||
// Split content into lines, handling both \n and \r\n
|
||||
const searchLines = searchContent.split(/\r?\n/);
|
||||
const replaceLines = replaceContent.split(/\r?\n/);
|
||||
const searchLines = searchContent === '' ? [] : searchContent.split(/\r?\n/);
|
||||
const replaceLines = replaceContent === '' ? [] : replaceContent.split(/\r?\n/);
|
||||
const originalLines = originalContent.split(/\r?\n/);
|
||||
|
||||
// First try exact line range if provided
|
||||
@@ -161,12 +215,14 @@ Your search/replace content here
|
||||
let bestMatchScore = 0;
|
||||
let bestMatchContent = "";
|
||||
|
||||
if (startLine !== undefined && endLine !== undefined) {
|
||||
if (startLine && endLine) {
|
||||
// Convert to 0-based index
|
||||
const exactStartIndex = startLine - 1;
|
||||
const exactEndIndex = endLine - 1;
|
||||
|
||||
if (exactStartIndex < 0 || exactEndIndex >= originalLines.length) {
|
||||
if (exactStartIndex < 0 || exactEndIndex > originalLines.length || exactStartIndex > exactEndIndex) {
|
||||
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}` : '';
|
||||
|
||||
// Log detailed debug information
|
||||
console.log('Invalid Line Range Debug:', {
|
||||
requestedRange: { start: startLine, end: endLine },
|
||||
@@ -175,7 +231,7 @@ Your search/replace content here
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)`,
|
||||
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)${debugInfo}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,13 +252,13 @@ Your search/replace content here
|
||||
let searchStartIndex = 0;
|
||||
let searchEndIndex = originalLines.length;
|
||||
|
||||
if (startLine !== undefined || endLine !== undefined) {
|
||||
if (startLine || endLine) {
|
||||
// Convert to 0-based index and add buffer
|
||||
if (startLine !== undefined) {
|
||||
searchStartIndex = Math.max(0, startLine - 6);
|
||||
if (startLine) {
|
||||
searchStartIndex = Math.max(0, startLine - (BUFFER_LINES + 1));
|
||||
}
|
||||
if (endLine !== undefined) {
|
||||
searchEndIndex = Math.min(originalLines.length, endLine + 5);
|
||||
if (endLine) {
|
||||
searchEndIndex = Math.min(originalLines.length, endLine + BUFFER_LINES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,17 +280,27 @@ Your search/replace content here
|
||||
// Require similarity to meet threshold
|
||||
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
||||
const searchChunk = searchLines.join('\n');
|
||||
// Log detailed debug information to console
|
||||
console.log('Search/Replace Debug Info:', {
|
||||
similarity: bestMatchScore,
|
||||
threshold: this.fuzzyThreshold,
|
||||
searchContent: searchChunk,
|
||||
bestMatch: bestMatchContent || undefined
|
||||
});
|
||||
const originalContentSection = startLine !== undefined && endLine !== undefined
|
||||
? `\n\nOriginal Content:\n${addLineNumbers(
|
||||
originalLines.slice(
|
||||
Math.max(0, startLine - 1 - BUFFER_LINES),
|
||||
Math.min(originalLines.length, endLine + BUFFER_LINES)
|
||||
).join('\n'),
|
||||
Math.max(1, startLine - BUFFER_LINES)
|
||||
)}`
|
||||
: `\n\nOriginal Content:\n${addLineNumbers(originalLines.join('\n'))}`;
|
||||
|
||||
const bestMatchSection = bestMatchContent
|
||||
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
|
||||
: `\n\nBest Match Found:\n(no match)`;
|
||||
|
||||
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : 'start to end'}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}` : '';
|
||||
|
||||
const lineRange = startLine || endLine ?
|
||||
` at ${startLine ? `start: ${startLine}` : 'start'} to ${endLine ? `end: ${endLine}` : 'end'}` : '';
|
||||
return {
|
||||
success: false,
|
||||
error: `No sufficiently similar match found${startLine !== undefined ? ` near lines ${startLine}-${endLine}` : ''} (${Math.round(bestMatchScore * 100)}% similar, needs ${Math.round(this.fuzzyThreshold * 100)}%)`
|
||||
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)${debugInfo}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -256,7 +322,7 @@ Your search/replace content here
|
||||
// Apply the replacement while preserving exact indentation
|
||||
const indentedReplaceLines = replaceLines.map((line, i) => {
|
||||
// Get the matched line's exact indentation
|
||||
const matchedIndent = originalIndents[0];
|
||||
const matchedIndent = originalIndents[0] || '';
|
||||
|
||||
// Get the current line's indentation relative to the search content
|
||||
const currentIndentMatch = line.match(/^[\t ]*/);
|
||||
|
||||
@@ -13,6 +13,11 @@ export type DiffResult =
|
||||
}};
|
||||
|
||||
export interface DiffStrategy {
|
||||
/**
|
||||
* Whether to enable detailed debug logging
|
||||
*/
|
||||
debugEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Get the tool description for this diff strategy
|
||||
* @param cwd The current working directory
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Cline } from "../Cline"
|
||||
import { openMention } from "../mentions"
|
||||
import { getNonce } from "./getNonce"
|
||||
import { getUri } from "./getUri"
|
||||
import { playSound, setSoundEnabled } from "../../utils/sound"
|
||||
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -66,7 +66,9 @@ type GlobalStateKey =
|
||||
| "openRouterUseMiddleOutTransform"
|
||||
| "allowedCommands"
|
||||
| "soundEnabled"
|
||||
| "soundVolume"
|
||||
| "diffEnabled"
|
||||
| "debugDiffEnabled"
|
||||
| "alwaysAllowMcp"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
@@ -136,6 +138,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.outputChannel.appendLine("Resolving webview view")
|
||||
this.view = webviewView
|
||||
|
||||
// Initialize sound enabled state
|
||||
this.getState().then(({ soundEnabled }) => {
|
||||
setSoundEnabled(soundEnabled ?? false)
|
||||
})
|
||||
|
||||
webviewView.webview.options = {
|
||||
// Allow scripts in the webview
|
||||
enableScripts: true,
|
||||
@@ -207,28 +214,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
|
||||
async initClineWithTask(task?: string, images?: string[]) {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
task,
|
||||
images
|
||||
)
|
||||
}
|
||||
|
||||
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
debugDiffEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
this.cline = new Cline(
|
||||
@@ -236,6 +226,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
debugDiffEnabled,
|
||||
task,
|
||||
images
|
||||
)
|
||||
}
|
||||
|
||||
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||
await this.clearTask()
|
||||
const {
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
debugDiffEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
this.cline = new Cline(
|
||||
this,
|
||||
apiConfiguration,
|
||||
customInstructions,
|
||||
diffEnabled,
|
||||
debugDiffEnabled,
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem,
|
||||
@@ -592,11 +603,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
setSoundEnabled(soundEnabled) // Add this line to update the sound utility
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "soundVolume":
|
||||
const soundVolume = message.value ?? 0.5
|
||||
await this.updateGlobalState("soundVolume", soundVolume)
|
||||
setSoundVolume(soundVolume)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "diffEnabled":
|
||||
const diffEnabled = message.bool ?? true
|
||||
await this.updateGlobalState("diffEnabled", diffEnabled)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
case "debugDiffEnabled":
|
||||
const debugDiffEnabled = message.bool ?? false
|
||||
await this.updateGlobalState("debugDiffEnabled", debugDiffEnabled)
|
||||
await this.postStateToWebview()
|
||||
break
|
||||
}
|
||||
},
|
||||
null,
|
||||
@@ -923,7 +945,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
alwaysAllowMcp,
|
||||
soundEnabled,
|
||||
diffEnabled,
|
||||
debugDiffEnabled,
|
||||
taskHistory,
|
||||
soundVolume,
|
||||
} = await this.getState()
|
||||
|
||||
const allowedCommands = vscode.workspace
|
||||
@@ -946,8 +970,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
.sort((a, b) => b.ts - a.ts),
|
||||
soundEnabled: soundEnabled ?? false,
|
||||
diffEnabled: diffEnabled ?? false,
|
||||
debugDiffEnabled: debugDiffEnabled ?? false,
|
||||
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||
allowedCommands,
|
||||
soundVolume: soundVolume ?? 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1040,6 +1066,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
allowedCommands,
|
||||
soundEnabled,
|
||||
diffEnabled,
|
||||
debugDiffEnabled,
|
||||
soundVolume,
|
||||
] = await Promise.all([
|
||||
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
|
||||
this.getGlobalState("apiModelId") as Promise<string | undefined>,
|
||||
@@ -1077,6 +1105,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
|
||||
this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("debugDiffEnabled") as Promise<boolean | undefined>,
|
||||
this.getGlobalState("soundVolume") as Promise<number | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -1130,8 +1160,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
alwaysAllowMcp: alwaysAllowMcp ?? false,
|
||||
taskHistory,
|
||||
allowedCommands,
|
||||
soundEnabled,
|
||||
diffEnabled,
|
||||
soundEnabled: soundEnabled ?? false,
|
||||
diffEnabled: diffEnabled ?? false,
|
||||
debugDiffEnabled: debugDiffEnabled ?? false,
|
||||
soundVolume,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
src/integrations/misc/__tests__/extract-text.test.ts
Normal file
32
src/integrations/misc/__tests__/extract-text.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { addLineNumbers } from '../extract-text';
|
||||
|
||||
describe('addLineNumbers', () => {
|
||||
it('should add line numbers starting from 1 by default', () => {
|
||||
const input = 'line 1\nline 2\nline 3';
|
||||
const expected = '1 | line 1\n2 | line 2\n3 | line 3';
|
||||
expect(addLineNumbers(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should add line numbers starting from specified line number', () => {
|
||||
const input = 'line 1\nline 2\nline 3';
|
||||
const expected = '10 | line 1\n11 | line 2\n12 | line 3';
|
||||
expect(addLineNumbers(input, 10)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle empty content', () => {
|
||||
expect(addLineNumbers('')).toBe('1 | ');
|
||||
expect(addLineNumbers('', 5)).toBe('5 | ');
|
||||
});
|
||||
|
||||
it('should handle single line content', () => {
|
||||
expect(addLineNumbers('single line')).toBe('1 | single line');
|
||||
expect(addLineNumbers('single line', 42)).toBe('42 | single line');
|
||||
});
|
||||
|
||||
it('should pad line numbers based on the highest line number', () => {
|
||||
const input = 'line 1\nline 2';
|
||||
// When starting from 99, highest line will be 100, so needs 3 spaces padding
|
||||
const expected = ' 99 | line 1\n100 | line 2';
|
||||
expect(addLineNumbers(input, 99)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -53,15 +53,12 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
|
||||
|
||||
return addLineNumbers(extractedText)
|
||||
}
|
||||
|
||||
export function addLineNumbers(content: string): string {
|
||||
export function addLineNumbers(content: string, startLine: number = 1): string {
|
||||
const lines = content.split('\n')
|
||||
const maxLineNumberWidth = String(lines.length).length
|
||||
const maxLineNumberWidth = String(startLine + lines.length - 1).length
|
||||
return lines
|
||||
.map((line, index) => {
|
||||
const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
|
||||
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
|
||||
return `${lineNumber} | ${line}`
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -51,7 +51,9 @@ export interface ExtensionState {
|
||||
uriScheme?: string
|
||||
allowedCommands?: string[]
|
||||
soundEnabled?: boolean
|
||||
soundVolume?: number
|
||||
diffEnabled?: boolean
|
||||
debugDiffEnabled?: boolean
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
|
||||
@@ -32,7 +32,9 @@ export interface WebviewMessage {
|
||||
| "alwaysAllowMcp"
|
||||
| "playSound"
|
||||
| "soundEnabled"
|
||||
| "soundVolume"
|
||||
| "diffEnabled"
|
||||
| "debugDiffEnabled"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "toggleToolAlwaysAllow"
|
||||
@@ -43,6 +45,7 @@ export interface WebviewMessage {
|
||||
apiConfiguration?: ApiConfiguration
|
||||
images?: string[]
|
||||
bool?: boolean
|
||||
value?: number
|
||||
commands?: string[]
|
||||
audioType?: AudioType
|
||||
// For toggleToolAutoApprove
|
||||
|
||||
@@ -21,6 +21,7 @@ export const isWAV = (filepath: string): boolean => {
|
||||
}
|
||||
|
||||
let isSoundEnabled = false
|
||||
let volume = .5
|
||||
|
||||
/**
|
||||
* Set sound configuration
|
||||
@@ -30,6 +31,14 @@ export const setSoundEnabled = (enabled: boolean): void => {
|
||||
isSoundEnabled = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sound volume
|
||||
* @param volume number
|
||||
*/
|
||||
export const setSoundVolume = (newVolume: number): void => {
|
||||
volume = newVolume
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound file
|
||||
* @param filepath string
|
||||
@@ -54,11 +63,9 @@ export const playSound = (filepath: string): void => {
|
||||
return // Skip playback within minimum interval to prevent continuous playback
|
||||
}
|
||||
|
||||
const player = require("play-sound")()
|
||||
player.play(filepath, function (err: any) {
|
||||
if (err) {
|
||||
throw new Error("Failed to play sound effect")
|
||||
}
|
||||
const sound = require("sound-play")
|
||||
sound.play(filepath, volume).catch(() => {
|
||||
throw new Error("Failed to play sound effect")
|
||||
})
|
||||
|
||||
lastPlayedTime = currentTime
|
||||
|
||||
Reference in New Issue
Block a user