@@ -23,6 +23,27 @@ interface InsertOperationFailure {
2323 note ?: string ;
2424}
2525
26+ interface InsertContentOptions {
27+ before ?: string ;
28+ after ?: string ;
29+ lineOffset ?: number ;
30+ }
31+
32+ interface GuardResolutionSuccess {
33+ success : true ;
34+ index : number ;
35+ }
36+
37+ function guardFailure ( error : string ) : InsertOperationFailure {
38+ return {
39+ success : false ,
40+ error,
41+ note : READ_AND_RETRY_NOTE ,
42+ } ;
43+ }
44+
45+ type GuardAnchors = Pick < InsertContentOptions , "before" | "after" > ;
46+
2647export const createFileEditInsertTool : ToolFactory = ( config : ToolConfiguration ) => {
2748 return tool ( {
2849 description : TOOL_DEFINITIONS . file_edit_insert . description ,
@@ -108,18 +129,20 @@ export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration)
108129function insertContent (
109130 originalContent : string ,
110131 contentToInsert : string ,
111- { before , after , lineOffset } : { before ?: string ; after ?: string ; lineOffset ?: number }
132+ options : InsertContentOptions
112133) : InsertOperationSuccess | InsertOperationFailure {
134+ const { before, after, lineOffset } = options ;
135+
136+ if ( before !== undefined && after !== undefined ) {
137+ return guardFailure ( "Provide only one of before or after (not both)." ) ;
138+ }
139+
113140 if ( before !== undefined || after !== undefined ) {
114141 return insertWithGuards ( originalContent , contentToInsert , { before, after } ) ;
115142 }
116143
117144 if ( lineOffset === undefined ) {
118- return {
119- success : false ,
120- error : "line_offset must be provided when before/after guards are omitted." ,
121- note : READ_AND_RETRY_NOTE ,
122- } ;
145+ return guardFailure ( "line_offset must be provided when before/after guards are omitted." ) ;
123146 }
124147
125148 return insertWithLineOffset ( originalContent , contentToInsert , lineOffset ) ;
@@ -128,46 +151,17 @@ function insertContent(
128151function insertWithGuards (
129152 originalContent : string ,
130153 contentToInsert : string ,
131- { before , after } : { before ?: string ; after ?: string }
154+ anchors : GuardAnchors
132155) : InsertOperationSuccess | InsertOperationFailure {
133- let anchorIndex : number | undefined ;
134-
135- if ( before !== undefined ) {
136- const beforeIndexResult = findUniqueSubstringIndex ( originalContent , before , "before" ) ;
137- if ( ! beforeIndexResult . success ) {
138- return beforeIndexResult ;
139- }
140- anchorIndex = beforeIndexResult . index + before . length ;
141- }
142-
143- if ( after !== undefined ) {
144- const afterIndexResult = findUniqueSubstringIndex ( originalContent , after , "after" ) ;
145- if ( ! afterIndexResult . success ) {
146- return afterIndexResult ;
147- }
148-
149- if ( anchorIndex === undefined ) {
150- anchorIndex = afterIndexResult . index ;
151- } else if ( anchorIndex !== afterIndexResult . index ) {
152- return {
153- success : false ,
154- error :
155- "Guard mismatch: before and after substrings do not point to the same insertion point." ,
156- note : READ_AND_RETRY_NOTE ,
157- } ;
158- }
159- }
160-
161- if ( anchorIndex === undefined ) {
162- return {
163- success : false ,
164- error : "Unable to determine insertion point from guards." ,
165- note : READ_AND_RETRY_NOTE ,
166- } ;
156+ const anchorResult = resolveGuardAnchor ( originalContent , anchors ) ;
157+ if ( ! anchorResult . success ) {
158+ return anchorResult ;
167159 }
168160
169161 const newContent =
170- originalContent . slice ( 0 , anchorIndex ) + contentToInsert + originalContent . slice ( anchorIndex ) ;
162+ originalContent . slice ( 0 , anchorResult . index ) +
163+ contentToInsert +
164+ originalContent . slice ( anchorResult . index ) ;
171165
172166 return {
173167 success : true ,
@@ -180,49 +174,62 @@ function findUniqueSubstringIndex(
180174 haystack : string ,
181175 needle : string ,
182176 label : "before" | "after"
183- ) : InsertOperationFailure | { success : true ; index : number } {
177+ ) : GuardResolutionSuccess | InsertOperationFailure {
184178 const firstIndex = haystack . indexOf ( needle ) ;
185179 if ( firstIndex === - 1 ) {
186- return {
187- success : false ,
188- error : `Guard mismatch: unable to find ${ label } substring in the current file.` ,
189- note : READ_AND_RETRY_NOTE ,
190- } ;
180+ return guardFailure ( `Guard mismatch: unable to find ${ label } substring in the current file.` ) ;
191181 }
192182
193183 const secondIndex = haystack . indexOf ( needle , firstIndex + needle . length ) ;
194184 if ( secondIndex !== - 1 ) {
195- return {
196- success : false ,
197- error : `Guard mismatch: ${ label } substring matched multiple times. Provide a more specific string.` ,
198- note : READ_AND_RETRY_NOTE ,
199- } ;
185+ return guardFailure (
186+ `Guard mismatch: ${ label } substring matched multiple times. Provide a more specific string.`
187+ ) ;
200188 }
201189
202190 return { success : true , index : firstIndex } ;
203191}
204192
193+ function resolveGuardAnchor (
194+ originalContent : string ,
195+ { before, after } : GuardAnchors
196+ ) : GuardResolutionSuccess | InsertOperationFailure {
197+ if ( before !== undefined ) {
198+ const beforeIndexResult = findUniqueSubstringIndex ( originalContent , before , "before" ) ;
199+ if ( ! beforeIndexResult . success ) {
200+ return beforeIndexResult ;
201+ }
202+ return { success : true , index : beforeIndexResult . index + before . length } ;
203+ }
204+
205+ if ( after !== undefined ) {
206+ const afterIndexResult = findUniqueSubstringIndex ( originalContent , after , "after" ) ;
207+ if ( ! afterIndexResult . success ) {
208+ return afterIndexResult ;
209+ }
210+ return { success : true , index : afterIndexResult . index } ;
211+ }
212+
213+ return guardFailure ( "Unable to determine insertion point from guards." ) ;
214+ }
215+
205216function insertWithLineOffset (
206217 originalContent : string ,
207218 contentToInsert : string ,
208219 lineOffset : number
209220) : InsertOperationSuccess | InsertOperationFailure {
210- const lines = originalContent . split ( "\n" ) ;
211- if ( lineOffset > lines . length ) {
221+ const lineCount = countLines ( originalContent ) ;
222+ if ( lineOffset > lineCount ) {
212223 return {
213224 success : false ,
214- error : `line_offset ${ lineOffset } is beyond file length (${ lines . length } lines)` ,
215- note : `${ EDIT_FAILED_NOTE_PREFIX } The file has ${ lines . length } lines. ${ NOTE_READ_FILE_RETRY } ` ,
225+ error : `line_offset ${ lineOffset } is beyond file length (${ lineCount } lines)` ,
226+ note : `${ EDIT_FAILED_NOTE_PREFIX } The file has ${ lineCount } lines. ${ NOTE_READ_FILE_RETRY } ` ,
216227 } ;
217228 }
218229
219230 const insertionIndex = getIndexForLineOffset ( originalContent , lineOffset ) ;
220231 if ( insertionIndex === null ) {
221- return {
222- success : false ,
223- error : `Unable to compute insertion point for line_offset ${ lineOffset } .` ,
224- note : READ_AND_RETRY_NOTE ,
225- } ;
232+ return guardFailure ( `Unable to compute insertion point for line_offset ${ lineOffset } .` ) ;
226233 }
227234
228235 const newContent =
@@ -264,3 +271,17 @@ function getIndexForLineOffset(content: string, lineOffset: number): number | nu
264271
265272 return null ;
266273}
274+
275+ function countLines ( content : string ) : number {
276+ if ( content . length === 0 ) {
277+ return 1 ;
278+ }
279+
280+ let count = 1 ;
281+ for ( const char of content ) {
282+ if ( char === "\n" ) {
283+ count += 1 ;
284+ }
285+ }
286+ return count ;
287+ }
0 commit comments