Skip to content

Commit 49067c0

Browse files
authored
Merge pull request #151 from RooVetGit/strip_line_numbers_from_write_to_file
Strip line numbers from write to file
2 parents 3fcfc8d + 86bb389 commit 49067c0

File tree

8 files changed

+132
-21
lines changed

8 files changed

+132
-21
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Roo Cline Changelog
22

3-
## [2.2.14]
3+
## [2.2.14 - 2.2.15]
44

5-
- Make diff editing more robust to transient errors
5+
- Make diff editing more robust to transient errors / fix bugs
66

77
## [2.2.13]
88

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Roo Cline",
44
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
55
"publisher": "RooVeterinaryInc",
6-
"version": "2.2.14",
6+
"version": "2.2.15",
77
"icon": "assets/icons/rocket.png",
88
"galleryBanner": {
99
"color": "#617A91",

src/core/Cline.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ApiHandler, buildApiHandler } from "../api"
1212
import { ApiStream } from "../api/transform/stream"
1313
import { DiffViewProvider } from "../integrations/editor/DiffViewProvider"
1414
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
15-
import { extractTextFromFile, addLineNumbers } from "../integrations/misc/extract-text"
15+
import { extractTextFromFile, addLineNumbers, stripLineNumbers, everyLineHasLineNumbers } from "../integrations/misc/extract-text"
1616
import { TerminalManager } from "../integrations/terminal/TerminalManager"
1717
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
1818
import { listFiles } from "../services/glob/list-files"
@@ -1090,7 +1090,7 @@ export class Cline {
10901090
await this.diffViewProvider.open(relPath)
10911091
}
10921092
// editor is open, stream content in
1093-
await this.diffViewProvider.update(newContent, false)
1093+
await this.diffViewProvider.update(everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, false)
10941094
break
10951095
} else {
10961096
if (!relPath) {
@@ -1116,7 +1116,7 @@ export class Cline {
11161116
await this.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
11171117
await this.diffViewProvider.open(relPath)
11181118
}
1119-
await this.diffViewProvider.update(newContent, true)
1119+
await this.diffViewProvider.update(everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, true)
11201120
await delay(300) // wait for diff view to update
11211121
this.diffViewProvider.scrollToFirstDiff()
11221122

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,23 @@ function sum(a, b) {
10541054
expect(result.content).toBe('function sum(a, b) {\n return a + b + 1;\n}')
10551055
}
10561056
})
1057+
1058+
it('should not exact match empty lines', () => {
1059+
const originalContent = 'function sum(a, b) {\n\n return a + b;\n}'
1060+
const diffContent = `test.ts
1061+
<<<<<<< SEARCH
1062+
function sum(a, b) {
1063+
=======
1064+
import { a } from "a";
1065+
function sum(a, b) {
1066+
>>>>>>> REPLACE`
1067+
1068+
const result = strategy.applyDiff(originalContent, diffContent)
1069+
expect(result.success).toBe(true)
1070+
if (result.success) {
1071+
expect(result.content).toBe('import { a } from "a";\nfunction sum(a, b) {\n\n return a + b;\n}')
1072+
}
1073+
})
10571074
})
10581075

10591076
describe('line-constrained search', () => {

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DiffStrategy, DiffResult } from "../types"
2-
import { addLineNumbers } from "../../../integrations/misc/extract-text"
2+
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
33

44
const BUFFER_LINES = 20; // Number of extra context lines to show before and after matches
55

@@ -33,7 +33,7 @@ function levenshteinDistance(a: string, b: string): number {
3333
}
3434

3535
function getSimilarity(original: string, search: string): number {
36-
if (original === '' || search === '') {
36+
if (search === '') {
3737
return 1;
3838
}
3939

@@ -140,16 +140,7 @@ Your search/replace content here
140140
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
141141

142142
// Strip line numbers from search and replace content if every line starts with a line number
143-
const hasLineNumbers = (content: string) => {
144-
const lines = content.split(/\r?\n/);
145-
return lines.length > 0 && lines.every(line => /^\s*\d+\s+\|(?!\|)/.test(line));
146-
};
147-
148-
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
149-
const stripLineNumbers = (content: string) => {
150-
return content.replace(/^\s*\d+\s+\|(?!\|)/gm, '');
151-
};
152-
143+
if (everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) {
153144
searchContent = stripLineNumbers(searchContent);
154145
replaceContent = stripLineNumbers(replaceContent);
155146
}

src/integrations/misc/__tests__/extract-text.test.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { addLineNumbers } from '../extract-text';
1+
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from '../extract-text';
22

33
describe('addLineNumbers', () => {
44
it('should add line numbers starting from 1 by default', () => {
@@ -29,4 +29,81 @@ describe('addLineNumbers', () => {
2929
const expected = ' 99 | line 1\n100 | line 2';
3030
expect(addLineNumbers(input, 99)).toBe(expected);
3131
});
32+
});
33+
34+
describe('everyLineHasLineNumbers', () => {
35+
it('should return true for content with line numbers', () => {
36+
const input = '1 | line one\n2 | line two\n3 | line three';
37+
expect(everyLineHasLineNumbers(input)).toBe(true);
38+
});
39+
40+
it('should return true for content with padded line numbers', () => {
41+
const input = ' 1 | line one\n 2 | line two\n 3 | line three';
42+
expect(everyLineHasLineNumbers(input)).toBe(true);
43+
});
44+
45+
it('should return false for content without line numbers', () => {
46+
const input = 'line one\nline two\nline three';
47+
expect(everyLineHasLineNumbers(input)).toBe(false);
48+
});
49+
50+
it('should return false for mixed content', () => {
51+
const input = '1 | line one\nline two\n3 | line three';
52+
expect(everyLineHasLineNumbers(input)).toBe(false);
53+
});
54+
55+
it('should handle empty content', () => {
56+
expect(everyLineHasLineNumbers('')).toBe(false);
57+
});
58+
59+
it('should return false for content with pipe but no line numbers', () => {
60+
const input = 'a | b\nc | d';
61+
expect(everyLineHasLineNumbers(input)).toBe(false);
62+
});
63+
});
64+
65+
describe('stripLineNumbers', () => {
66+
it('should strip line numbers from content', () => {
67+
const input = '1 | line one\n2 | line two\n3 | line three';
68+
const expected = 'line one\nline two\nline three';
69+
expect(stripLineNumbers(input)).toBe(expected);
70+
});
71+
72+
it('should strip padded line numbers', () => {
73+
const input = ' 1 | line one\n 2 | line two\n 3 | line three';
74+
const expected = 'line one\nline two\nline three';
75+
expect(stripLineNumbers(input)).toBe(expected);
76+
});
77+
78+
it('should handle content without line numbers', () => {
79+
const input = 'line one\nline two\nline three';
80+
expect(stripLineNumbers(input)).toBe(input);
81+
});
82+
83+
it('should handle empty content', () => {
84+
expect(stripLineNumbers('')).toBe('');
85+
});
86+
87+
it('should preserve content with pipe but no line numbers', () => {
88+
const input = 'a | b\nc | d';
89+
expect(stripLineNumbers(input)).toBe(input);
90+
});
91+
92+
it('should handle windows-style line endings', () => {
93+
const input = '1 | line one\r\n2 | line two\r\n3 | line three';
94+
const expected = 'line one\r\nline two\r\nline three';
95+
expect(stripLineNumbers(input)).toBe(expected);
96+
});
97+
98+
it('should handle content with varying line number widths', () => {
99+
const input = ' 1 | line one\n 10 | line two\n100 | line three';
100+
const expected = 'line one\nline two\nline three';
101+
expect(stripLineNumbers(input)).toBe(expected);
102+
});
103+
104+
it('should preserve indentation after line numbers', () => {
105+
const input = '1 | indented line\n2 | another indented';
106+
const expected = ' indented line\n another indented';
107+
expect(stripLineNumbers(input)).toBe(expected);
108+
});
32109
});

src/integrations/misc/extract-text.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
5353

5454
return addLineNumbers(extractedText)
5555
}
56+
5657
export function addLineNumbers(content: string, startLine: number = 1): string {
5758
const lines = content.split('\n')
5859
const maxLineNumberWidth = String(startLine + lines.length - 1).length
@@ -61,4 +62,29 @@ export function addLineNumbers(content: string, startLine: number = 1): string {
6162
const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
6263
return `${lineNumber} | ${line}`
6364
}).join('\n')
65+
}
66+
// Checks if every line in the content has line numbers prefixed (e.g., "1 | content" or "123 | content")
67+
// Line numbers must be followed by a single pipe character (not double pipes)
68+
export function everyLineHasLineNumbers(content: string): boolean {
69+
const lines = content.split(/\r?\n/)
70+
return lines.length > 0 && lines.every(line => /^\s*\d+\s+\|(?!\|)/.test(line))
71+
}
72+
73+
// Strips line numbers from content while preserving the actual content
74+
// Handles formats like "1 | content", " 12 | content", "123 | content"
75+
// Preserves content that naturally starts with pipe characters
76+
export function stripLineNumbers(content: string): string {
77+
// Split into lines to handle each line individually
78+
const lines = content.split(/\r?\n/)
79+
80+
// Process each line
81+
const processedLines = lines.map(line => {
82+
// Match line number pattern and capture everything after the pipe
83+
const match = line.match(/^\s*\d+\s+\|(?!\|)\s?(.*)$/)
84+
return match ? match[1] : line
85+
})
86+
87+
// Join back with original line endings
88+
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n'
89+
return processedLines.join(lineEnding)
6490
}

0 commit comments

Comments
 (0)