fix: make start_line and end_line parsing more lenient to accept brackets

Some models send `:start_line:[245]` instead of `:start_line:245`. This updates
the regex to accept optional brackets around line numbers, improving compatibility
with models like MiniMax-2.1 and Gemini.

Supported formats:
- :start_line:253
- :start_line: 253
- :start_line:[253]
- :start_line: [253]

Fixes #11087
This commit is contained in:
Roo Code 2026-02-02 17:48:38 +00:00
parent b020f6be43
commit 30c730f2b2
3 changed files with 133 additions and 5 deletions

View file

@ -0,0 +1,16 @@
---
"roo-cline": patch
---
fix: make start_line and end_line parsing more lenient to accept brackets
Some models send `:start_line:[245]` or `:start_line: [245]` instead of `:start_line:245`. This change updates the regex to accept optional brackets around the line number, improving compatibility with models like MiniMax-2.1 and Gemini.
Supported formats:
- `:start_line:253`
- `:start_line: 253`
- `:start_line:[253]`
- `:start_line: [253]`
Fixes #11087

View file

@ -92,6 +92,30 @@ describe("MultiSearchReplaceDiffStrategy", () => {
expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
})
it("validates line numbers with brackets (model variation)", () => {
const diff =
"<<<<<<< SEARCH\n" +
":start_line:[10]\n" +
"-------\n" +
"content1\n" +
"=======\n" +
"new1\n" +
">>>>>>> REPLACE"
expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
})
it("validates line numbers with space and brackets (model variation)", () => {
const diff =
"<<<<<<< SEARCH\n" +
":start_line: [10]\n" +
"-------\n" +
"content1\n" +
"=======\n" +
"new1\n" +
">>>>>>> REPLACE"
expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
})
it("detects separator before search", () => {
const diff = "=======\n" + "content\n" + ">>>>>>> REPLACE"
const result = strategy["validateMarkerSequencing"](diff)
@ -826,6 +850,92 @@ function five() {
}
})
})
describe("bracket variations in line numbers", () => {
let strategy: MultiSearchReplaceDiffStrategy
beforeEach(() => {
strategy = new MultiSearchReplaceDiffStrategy(1.0, 5)
})
it("should accept start_line with brackets around the number", async () => {
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
const diffContent =
"test.ts\n" +
"<<<<<<< SEARCH\n" +
":start_line:[1]\n" +
"-------\n" +
"function hello() {\n" +
"=======\n" +
"function helloWorld() {\n" +
">>>>>>> REPLACE"
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe('function helloWorld() {\n console.log("hello")\n}\n')
}
})
it("should accept start_line with space and brackets around the number", async () => {
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
const diffContent =
"test.ts\n" +
"<<<<<<< SEARCH\n" +
":start_line: [1]\n" +
"-------\n" +
"function hello() {\n" +
"=======\n" +
"function helloWorld() {\n" +
">>>>>>> REPLACE"
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe('function helloWorld() {\n console.log("hello")\n}\n')
}
})
it("should accept end_line with brackets around the number", async () => {
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
const diffContent =
"test.ts\n" +
"<<<<<<< SEARCH\n" +
":start_line:[1]\n" +
":end_line:[1]\n" +
"-------\n" +
"function hello() {\n" +
"=======\n" +
"function helloWorld() {\n" +
">>>>>>> REPLACE"
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe('function helloWorld() {\n console.log("hello")\n}\n')
}
})
it("should accept end_line with space and brackets around the number", async () => {
const originalContent = 'function hello() {\n console.log("hello")\n}\n'
const diffContent =
"test.ts\n" +
"<<<<<<< SEARCH\n" +
":start_line: [1]\n" +
":end_line: [1]\n" +
"-------\n" +
"function hello() {\n" +
"=======\n" +
"function helloWorld() {\n" +
">>>>>>> REPLACE"
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe('function helloWorld() {\n console.log("hello")\n}\n')
}
})
})
})
describe("fuzzy matching", () => {

View file

@ -265,11 +265,13 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
2. (?<!\\)<<<<<<< SEARCH\s*\n
Matches the line "<<<<<<< SEARCH" (ignoring any trailing spaces) the negative lookbehind makes sure it isn't escaped.
3. ((?:\:start_line:\s*(\d+)\s*\n))?
Optionally matches a ":start_line:" line. The outer capturing group is group 1 and the inner (\d+) is group 2.
3. ((?:\:start_line:\s*\[?(\d+)\]?\s*\n))?
Optionally matches a ":start_line:" line with optional brackets around the number. The outer capturing group is group 1 and the inner (\d+) is group 2.
Accepts formats: :start_line:5, :start_line: 5, :start_line:[5], :start_line: [5]
4. ((?:\:end_line:\s*(\d+)\s*\n))?
Optionally matches a ":end_line:" line. Group 3 is the whole match and group 4 is the digits.
4. ((?:\:end_line:\s*\[?(\d+)\]?\s*\n))?
Optionally matches a ":end_line:" line with optional brackets around the number. Group 3 is the whole match and group 4 is the digits.
Accepts formats: :end_line:5, :end_line: 5, :end_line:[5], :end_line: [5]
5. ((?<!\\)-------\s*\n)?
Optionally matches the "-------" marker line (group 5).
@ -289,7 +291,7 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
let matches = [
...diffContent.matchAll(
/(?:^|\n)(?<!\\)<<<<<<< SEARCH>?\s*\n((?:\:start_line:\s*(\d+)\s*\n))?((?:\:end_line:\s*(\d+)\s*\n))?((?<!\\)-------\s*\n)?([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)=======\s*\n)([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$)/g,
/(?:^|\n)(?<!\\)<<<<<<< SEARCH>?\s*\n((?:\:start_line:\s*\[?(\d+)\]?\s*\n))?((?:\:end_line:\s*\[?(\d+)\]?\s*\n))?((?<!\\)-------\s*\n)?([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)=======\s*\n)([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$)/g,
),
]