@@ -4,6 +4,7 @@ import { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearch.js';
44import { capture } from '../utils/capture.js' ;
55import { EditBlockArgsSchema } from "./schemas.js" ;
66import path from 'path' ;
7+ import { detectLineEnding , normalizeLineEndings } from '../utils/lineEndingHandler.js' ;
78
89interface SearchReplace {
910 search : string ;
@@ -24,6 +25,71 @@ interface FuzzyMatch {
2425 */
2526const FUZZY_THRESHOLD = 0.7 ;
2627
28+ /**
29+ * Extract character code data from diff
30+ * @param expected The string that was searched for
31+ * @param actual The string that was found
32+ * @returns Character code statistics
33+ */
34+ function getCharacterCodeData ( expected : string , actual : string ) : {
35+ report : string ;
36+ uniqueCount : number ;
37+ diffLength : number ;
38+ } {
39+ // Find common prefix and suffix
40+ let prefixLength = 0 ;
41+ const minLength = Math . min ( expected . length , actual . length ) ;
42+
43+ // Determine common prefix length
44+ while ( prefixLength < minLength &&
45+ expected [ prefixLength ] === actual [ prefixLength ] ) {
46+ prefixLength ++ ;
47+ }
48+
49+ // Determine common suffix length
50+ let suffixLength = 0 ;
51+ while ( suffixLength < minLength - prefixLength &&
52+ expected [ expected . length - 1 - suffixLength ] === actual [ actual . length - 1 - suffixLength ] ) {
53+ suffixLength ++ ;
54+ }
55+
56+ // Extract the different parts
57+ const expectedDiff = expected . substring ( prefixLength , expected . length - suffixLength ) ;
58+ const actualDiff = actual . substring ( prefixLength , actual . length - suffixLength ) ;
59+
60+ // Count unique character codes in the diff
61+ const characterCodes = new Map < number , number > ( ) ;
62+ const fullDiff = expectedDiff + actualDiff ;
63+
64+ for ( let i = 0 ; i < fullDiff . length ; i ++ ) {
65+ const charCode = fullDiff . charCodeAt ( i ) ;
66+ characterCodes . set ( charCode , ( characterCodes . get ( charCode ) || 0 ) + 1 ) ;
67+ }
68+
69+ // Create character codes string report
70+ const charCodeReport : string [ ] = [ ] ;
71+ characterCodes . forEach ( ( count , code ) => {
72+ // Include character representation for better readability
73+ const char = String . fromCharCode ( code ) ;
74+ // Make special characters more readable
75+ const charDisplay = code < 32 || code > 126 ? `\\x${ code . toString ( 16 ) . padStart ( 2 , '0' ) } ` : char ;
76+ charCodeReport . push ( `${ code } :${ count } [${ charDisplay } ]` ) ;
77+ } ) ;
78+
79+ // Sort by character code for consistency
80+ charCodeReport . sort ( ( a , b ) => {
81+ const codeA = parseInt ( a . split ( ':' ) [ 0 ] ) ;
82+ const codeB = parseInt ( b . split ( ':' ) [ 0 ] ) ;
83+ return codeA - codeB ;
84+ } ) ;
85+
86+ return {
87+ report : charCodeReport . join ( ',' ) ,
88+ uniqueCount : characterCodes . size ,
89+ diffLength : fullDiff . length
90+ } ;
91+ }
92+
2793export async function performSearchReplace ( filePath : string , block : SearchReplace , expectedReplacements : number = 1 ) : Promise < ServerResult > {
2894 // Check for empty search string to prevent infinite loops
2995 if ( block . search === "" ) {
@@ -49,14 +115,20 @@ export async function performSearchReplace(filePath: string, block: SearchReplac
49115 throw new Error ( 'Wrong content for file ' + filePath ) ;
50116 }
51117
118+ // Detect file's line ending style
119+ const fileLineEnding = detectLineEnding ( content ) ;
120+
121+ // Normalize search string to match file's line endings
122+ const normalizedSearch = normalizeLineEndings ( block . search , fileLineEnding ) ;
123+
52124 // First try exact match
53125 let tempContent = content ;
54126 let count = 0 ;
55- let pos = tempContent . indexOf ( block . search ) ;
127+ let pos = tempContent . indexOf ( normalizedSearch ) ;
56128
57129 while ( pos !== - 1 ) {
58130 count ++ ;
59- pos = tempContent . indexOf ( block . search , pos + 1 ) ;
131+ pos = tempContent . indexOf ( normalizedSearch , pos + 1 ) ;
60132 }
61133
62134 // If exact match found and count matches expected replacements, proceed with exact replacement
@@ -66,14 +138,14 @@ export async function performSearchReplace(filePath: string, block: SearchReplac
66138
67139 // If we're only replacing one occurrence, replace it directly
68140 if ( expectedReplacements === 1 ) {
69- const searchIndex = newContent . indexOf ( block . search ) ;
141+ const searchIndex = newContent . indexOf ( normalizedSearch ) ;
70142 newContent =
71143 newContent . substring ( 0 , searchIndex ) +
72- block . replace +
73- newContent . substring ( searchIndex + block . search . length ) ;
144+ normalizeLineEndings ( block . replace , fileLineEnding ) +
145+ newContent . substring ( searchIndex + normalizedSearch . length ) ;
74146 } else {
75147 // Replace all occurrences using split and join for multiple replacements
76- newContent = newContent . split ( block . search ) . join ( block . replace ) ;
148+ newContent = newContent . split ( normalizedSearch ) . join ( normalizeLineEndings ( block . replace , fileLineEnding ) ) ;
77149 }
78150
79151 await writeFile ( filePath , newContent ) ;
@@ -111,20 +183,29 @@ export async function performSearchReplace(filePath: string, block: SearchReplac
111183 // Calculate execution time in milliseconds
112184 const executionTime = performance . now ( ) - startTime ;
113185
186+ // Generate diff and gather character code data
187+ const diff = highlightDifferences ( block . search , fuzzyResult . value ) ;
188+
189+ // Count character codes in diff
190+ const characterCodeData = getCharacterCodeData ( block . search , fuzzyResult . value ) ;
191+
192+ // Combine all fuzzy search data for single capture
193+ const fuzzySearchData = {
194+ similarity : similarity ,
195+ execution_time_ms : executionTime ,
196+ search_length : block . search . length ,
197+ file_size : content . length ,
198+ threshold : FUZZY_THRESHOLD ,
199+ found_text_length : fuzzyResult . value . length ,
200+ character_codes : characterCodeData . report ,
201+ unique_character_count : characterCodeData . uniqueCount ,
202+ total_diff_length : characterCodeData . diffLength
203+ } ;
204+
114205 // Check if the fuzzy match is "close enough"
115206 if ( similarity >= FUZZY_THRESHOLD ) {
116- // Format differences for clearer output
117- const diff = highlightDifferences ( block . search , fuzzyResult . value ) ;
118-
119- // Capture the fuzzy search event
120- capture ( 'server_fuzzy_search_performed' , {
121- similarity : similarity ,
122- execution_time_ms : executionTime ,
123- search_length : block . search . length ,
124- file_size : content . length ,
125- threshold : FUZZY_THRESHOLD ,
126- found_text_length : fuzzyResult . value . length
127- } ) ;
207+ // Capture the fuzzy search event with all data
208+ capture ( 'server_fuzzy_search_performed' , fuzzySearchData ) ;
128209
129210 // If we allow fuzzy matches, we would make the replacement here
130211 // For now, we'll return a detailed message about the fuzzy match
@@ -138,14 +219,9 @@ export async function performSearchReplace(filePath: string, block: SearchReplac
138219 } ;
139220 } else {
140221 // If the fuzzy match isn't close enough
141- // Still capture the fuzzy search event even for unsuccessful matches
222+ // Still capture the fuzzy search event with all data
142223 capture ( 'server_fuzzy_search_performed' , {
143- similarity : similarity ,
144- execution_time_ms : executionTime ,
145- search_length : block . search . length ,
146- file_size : content . length ,
147- threshold : FUZZY_THRESHOLD ,
148- found_text_length : fuzzyResult . value . length ,
224+ ...fuzzySearchData ,
149225 below_threshold : true
150226 } ) ;
151227
0 commit comments