11/**
22 * Frontend-only normalization helper.
33 * - If a unified diff already exists, return it.
4- * - If a Roo SEARCH/REPLACE block is provided, convert to unified diff.
54 * - If it's a new file with raw content, synthesize a unified diff with all lines as additions.
65 * - Otherwise, pass through raw content (DiffView will no-op if not unified).
76 */
@@ -17,17 +16,14 @@ export function extractUnifiedDiff(params: {
1716 if ( ! raw ) return ""
1817
1918 raw = stripCData ( raw )
19+ // Remove diff noise lines like "\ No newline at end of file"
20+ raw = raw . replace ( / ( ^ | \n ) [ \t ] * (?: \\ ) ? N o n e w l i n e a t e n d o f f i l e [ \t ] * (? = \n | $ ) / gi, "$1" )
2021
2122 // Explicit new file: build a unified diff from raw content
2223 if ( ( params . toolName || "" ) . toLowerCase ( ) === "newfilecreated" ) {
2324 return convertNewFileToUnifiedDiff ( raw , filePath )
2425 }
2526
26- // SEARCH/REPLACE blocks → unified
27- if ( isSearchReplace ( raw ) ) {
28- return convertSearchReplaceToUnifiedDiff ( raw , filePath )
29- }
30-
3127 // Already unified?
3228 if ( isUnifiedDiff ( raw ) ) {
3329 return raw
@@ -44,13 +40,6 @@ function isUnifiedDiff(s: string): boolean {
4440 return hasHunk || hasHeaders
4541}
4642
47- /** Detects Roo SEARCH/REPLACE multi-block format */
48- function isSearchReplace ( s : string ) : boolean {
49- return (
50- / ( ^ | \n ) < < < < < < < ? S E A R C H | ( ^ | \n ) < < < < < < < ? ? S E A R C H | ( ^ | \n ) < < < < < < < S E A R C H > / . test ( s ) || / ( ^ | \n ) < < < < < < < ? S E A R C H / . test ( s )
51- )
52- }
53-
5443/** Remove CDATA markers and any HTML-encoded variants */
5544function stripCData ( s : string ) : string {
5645 return (
@@ -61,71 +50,10 @@ function stripCData(s: string): string {
6150 )
6251}
6352
64- /**
65- * Convert Roo SEARCH/REPLACE blocks into unified diff using the diff library.
66- * Matches optional metadata lines and optional '-------' separator.
67- */
68- export function convertSearchReplaceToUnifiedDiff ( content : string , filePath ?: string ) : string {
69- // Backend-compatible regex: captures :start_line: and :end_line:, optional '-------', and SEARCH/REPLACE bodies
70- const blockRegex =
71- / (?: ^ | \n ) (?< ! \\ ) < < < < < < < S E A R C H > ? \s * \n ( (?: : s t a r t _ l i n e : \s * ( \d + ) \s * \n ) ) ? ( (?: : e n d _ l i n e : \s * ( \d + ) \s * \n ) ) ? ( (?< ! \\ ) - - - - - - - \s * \n ) ? ( [ \s \S ] * ?) (?: \n ) ? (?: (?< = \n ) (?< ! \\ ) = = = = = = = \s * \n ) ( [ \s \S ] * ?) (?: \n ) ? (?: (?< = \n ) (?< ! \\ ) > > > > > > > R E P L A C E ) (? = \n | $ ) / g
72-
73- const fileName = filePath || "file"
74- let hasBlocks = false
75- let headerEmitted = false
76- let unified = ""
77-
78- // Helper to normalize EOLs and get stable line arrays without trailing empty caused by final newline
79- const toStableLines = ( s : string ) : string [ ] => {
80- const norm = s . replace ( / \r \n / g, "\n" )
81- if ( norm === "" ) return [ ]
82- const parts = norm . split ( "\n" )
83- return parts [ parts . length - 1 ] === "" ? parts . slice ( 0 , - 1 ) : parts
84- }
85-
86- let match : RegExpExecArray | null
87- while ( ( match = blockRegex . exec ( content ) ) !== null ) {
88- hasBlocks = true
89-
90- // 1: full start_line line, 2: start_line number, 3: full end_line line, 4: end_line number
91- const startLine = match [ 2 ] ? parseInt ( match [ 2 ] , 10 ) : 1
92- const endLine = match [ 4 ] ? parseInt ( match [ 4 ] , 10 ) : undefined
93-
94- // 6: SEARCH body, 7: REPLACE body
95- const searchBody = match [ 6 ] ?? ""
96- const replaceBody = match [ 7 ] ?? ""
97-
98- const searchLines = toStableLines ( searchBody )
99- const replaceLines = toStableLines ( replaceBody )
100-
101- // Old/new hunk metadata. If end_line is present, prefer it for oldLines; otherwise count SEARCH lines.
102- const oldStart = startLine
103- const oldLines = endLine !== undefined ? Math . max ( 0 , endLine - startLine + 1 ) : searchLines . length
104- const newStart = startLine
105- const newLines = replaceLines . length
106-
107- // Emit file headers once so parsePatch can recognize a complete unified diff
108- if ( ! headerEmitted ) {
109- unified += `--- a/${ fileName } \n`
110- unified += `+++ b/${ fileName } \n`
111- headerEmitted = true
112- }
113-
114- // Hunk header
115- unified += `@@ -${ oldStart } ,${ oldLines } +${ newStart } ,${ newLines } @@\n`
116-
117- // We don't have surrounding context here; emit deletions then additions to visualize the change
118- for ( const line of searchLines ) unified += `-${ line } \n`
119- for ( const line of replaceLines ) unified += `+${ line } \n`
120- }
121-
122- return hasBlocks ? unified : content
123- }
124-
12553/** Build a unified diff for a brand new file (all content lines are additions).
12654 * Trailing newline is ignored for line counting and emission.
12755 */
128- export function convertNewFileToUnifiedDiff ( content : string , filePath ?: string ) : string {
56+ function convertNewFileToUnifiedDiff ( content : string , filePath ?: string ) : string {
12957 const fileName = filePath || "file"
13058 // Normalize EOLs to keep counts consistent
13159 const normalized = content . replace ( / \r \n / g, "\n" )
0 commit comments