Skip to content

Commit adf747d

Browse files
committed
Better multi-line search/replace matching
1 parent b71e1e5 commit adf747d

File tree

2 files changed

+112
-17
lines changed

2 files changed

+112
-17
lines changed

src/core/diff/strategies/__tests__/search-replace.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,81 @@ function test() {
106106
`)
107107
})
108108

109+
it('should handle tab-based indentation', () => {
110+
const originalContent = "function test() {\n\treturn true;\n}\n"
111+
const diffContent = `test.ts
112+
<<<<<<< SEARCH
113+
function test() {
114+
\treturn true;
115+
}
116+
=======
117+
function test() {
118+
\treturn false;
119+
}
120+
>>>>>>> REPLACE`
121+
122+
const result = strategy.applyDiff(originalContent, diffContent)
123+
expect(result).toBe("function test() {\n\treturn false;\n}\n")
124+
})
125+
126+
it('should preserve mixed tabs and spaces', () => {
127+
const originalContent = "\tclass Example {\n\t constructor() {\n\t\tthis.value = 0;\n\t }\n\t}"
128+
const diffContent = `test.ts
129+
<<<<<<< SEARCH
130+
\tclass Example {
131+
\t constructor() {
132+
\t\tthis.value = 0;
133+
\t }
134+
\t}
135+
=======
136+
\tclass Example {
137+
\t constructor() {
138+
\t\tthis.value = 1;
139+
\t }
140+
\t}
141+
>>>>>>> REPLACE`
142+
143+
const result = strategy.applyDiff(originalContent, diffContent)
144+
expect(result).toBe("\tclass Example {\n\t constructor() {\n\t\tthis.value = 1;\n\t }\n\t}")
145+
})
146+
147+
it('should handle additional indentation with tabs', () => {
148+
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
149+
const diffContent = `test.ts
150+
<<<<<<< SEARCH
151+
function test() {
152+
\treturn true;
153+
}
154+
=======
155+
function test() {
156+
\t// Add comment
157+
\treturn false;
158+
}
159+
>>>>>>> REPLACE`
160+
161+
const result = strategy.applyDiff(originalContent, diffContent)
162+
expect(result).toBe("\tfunction test() {\n\t\t// Add comment\n\t\treturn false;\n\t}")
163+
})
164+
165+
it('should preserve exact indentation characters when adding lines', () => {
166+
const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}"
167+
const diffContent = `test.ts
168+
<<<<<<< SEARCH
169+
\tfunction test() {
170+
\t\treturn true;
171+
\t}
172+
=======
173+
\tfunction test() {
174+
\t\t// First comment
175+
\t\t// Second comment
176+
\t\treturn true;
177+
\t}
178+
>>>>>>> REPLACE`
179+
180+
const result = strategy.applyDiff(originalContent, diffContent)
181+
expect(result).toBe("\tfunction test() {\n\t\t// First comment\n\t\t// Second comment\n\t\treturn true;\n\t}")
182+
})
183+
109184
it('should return false if search content does not match', () => {
110185
const originalContent = `function hello() {
111186
console.log("hello")

src/core/diff/strategies/search-replace.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ Parameters:
1717
Format:
1818
1. First line must be the file path
1919
2. Followed by search/replace blocks:
20-
\`\`\`
21-
<<<<<<< SEARCH
22-
[exact content to find including whitespace]
23-
=======
24-
[new content to replace with]
25-
>>>>>>> REPLACE
26-
\`\`\`
20+
\`\`\`
21+
<<<<<<< SEARCH
22+
[exact content to find including whitespace]
23+
=======
24+
[new content to replace with]
25+
>>>>>>> REPLACE
26+
\`\`\`
2727
2828
Example:
2929
@@ -105,18 +105,38 @@ Your search/replace content here
105105
// Get the matched lines from the original content
106106
const matchedLines = originalLines.slice(matchIndex, matchIndex + searchLines.length);
107107

108-
// For each line in the match, get its indentation
109-
const indentations = matchedLines.map(line => {
110-
const match = line.match(/^(\s*)/);
111-
return match ? match[1] : '';
108+
// Get the exact indentation (preserving tabs/spaces) of each line
109+
const originalIndents = matchedLines.map(line => {
110+
const match = line.match(/^[\t ]*/);
111+
return match ? match[0] : '';
112112
});
113113

114-
// Apply the replacement while preserving indentation
114+
// Get the exact indentation of each line in the search block
115+
const searchIndents = searchLines.map(line => {
116+
const match = line.match(/^[\t ]*/);
117+
return match ? match[0] : '';
118+
});
119+
120+
// Apply the replacement while preserving exact indentation
115121
const indentedReplace = replaceLines.map((line, i) => {
116-
// Use the indentation from the corresponding line in the matched block
117-
// If we have more lines than the original, use the last indentation
118-
const indent = indentations[Math.min(i, indentations.length - 1)];
119-
return indent + line.trim();
122+
// Get the corresponding original and search indentations
123+
const originalIndent = originalIndents[Math.min(i, originalIndents.length - 1)];
124+
const searchIndent = searchIndents[Math.min(i, searchIndents.length - 1)];
125+
126+
// Get the current line's indentation
127+
const currentIndentMatch = line.match(/^[\t ]*/);
128+
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : '';
129+
130+
// If this line has the same indentation level as the search block,
131+
// use the original indentation. Otherwise, calculate the difference
132+
// and preserve the exact type of whitespace characters
133+
if (currentIndent.length === searchIndent.length) {
134+
return originalIndent + line.trim();
135+
} else {
136+
// Calculate additional indentation needed
137+
const additionalIndent = currentIndent.slice(searchIndent.length);
138+
return originalIndent + additionalIndent + line.trim();
139+
}
120140
});
121141

122142
// Construct the final content
@@ -125,4 +145,4 @@ Your search/replace content here
125145

126146
return [...beforeMatch, ...indentedReplace, ...afterMatch].join('\n');
127147
}
128-
}
148+
}

0 commit comments

Comments
 (0)