@@ -451,6 +451,189 @@ describe("tool.edit", () => {
451451 } )
452452 } )
453453
454+ describe ( "line endings" , ( ) => {
455+ const old = "alpha\nbeta\ngamma"
456+ const next = "alpha\nbeta-updated\ngamma"
457+ const alt = "alpha\nbeta\nomega"
458+
459+ const normalize = ( text : string , ending : "\n" | "\r\n" ) => {
460+ const normalized = text . replaceAll ( "\r\n" , "\n" )
461+ if ( ending === "\n" ) return normalized
462+ return normalized . replaceAll ( "\n" , "\r\n" )
463+ }
464+
465+ const count = ( content : string ) => {
466+ const crlf = content . match ( / \r \n / g) ?. length ?? 0
467+ const lf = content . match ( / \n / g) ?. length ?? 0
468+ return {
469+ crlf,
470+ lf : lf - crlf ,
471+ }
472+ }
473+
474+ const expectLf = ( content : string ) => {
475+ const counts = count ( content )
476+ expect ( counts . crlf ) . toBe ( 0 )
477+ expect ( counts . lf ) . toBeGreaterThan ( 0 )
478+ }
479+
480+ const expectCrlf = ( content : string ) => {
481+ const counts = count ( content )
482+ expect ( counts . lf ) . toBe ( 0 )
483+ expect ( counts . crlf ) . toBeGreaterThan ( 0 )
484+ }
485+
486+ type Input = {
487+ content : string
488+ oldString : string
489+ newString : string
490+ replaceAll ?: boolean
491+ }
492+
493+ const apply = async ( input : Input ) => {
494+ await using tmp = await tmpdir ( {
495+ init : async ( dir ) => {
496+ await Bun . write ( path . join ( dir , "test.txt" ) , input . content )
497+ } ,
498+ } )
499+
500+ return await Instance . provide ( {
501+ directory : tmp . path ,
502+ fn : async ( ) => {
503+ const edit = await EditTool . init ( )
504+ const filePath = path . join ( tmp . path , "test.txt" )
505+ FileTime . read ( ctx . sessionID , filePath )
506+ await edit . execute (
507+ {
508+ filePath,
509+ oldString : input . oldString ,
510+ newString : input . newString ,
511+ replaceAll : input . replaceAll ,
512+ } ,
513+ ctx ,
514+ )
515+ return await Bun . file ( filePath ) . text ( )
516+ } ,
517+ } )
518+ }
519+
520+ test ( "preserves LF with LF multi-line strings" , async ( ) => {
521+ const content = normalize ( old + "\n" , "\n" )
522+ const output = await apply ( {
523+ content,
524+ oldString : normalize ( old , "\n" ) ,
525+ newString : normalize ( next , "\n" ) ,
526+ } )
527+ expect ( output ) . toBe ( normalize ( next + "\n" , "\n" ) )
528+ expectLf ( output )
529+ } )
530+
531+ test ( "preserves CRLF with CRLF multi-line strings" , async ( ) => {
532+ const content = normalize ( old + "\n" , "\r\n" )
533+ const output = await apply ( {
534+ content,
535+ oldString : normalize ( old , "\r\n" ) ,
536+ newString : normalize ( next , "\r\n" ) ,
537+ } )
538+ expect ( output ) . toBe ( normalize ( next + "\n" , "\r\n" ) )
539+ expectCrlf ( output )
540+ } )
541+
542+ test ( "preserves LF when old/new use CRLF" , async ( ) => {
543+ const content = normalize ( old + "\n" , "\n" )
544+ const output = await apply ( {
545+ content,
546+ oldString : normalize ( old , "\r\n" ) ,
547+ newString : normalize ( next , "\r\n" ) ,
548+ } )
549+ expect ( output ) . toBe ( normalize ( next + "\n" , "\n" ) )
550+ expectLf ( output )
551+ } )
552+
553+ test ( "preserves CRLF when old/new use LF" , async ( ) => {
554+ const content = normalize ( old + "\n" , "\r\n" )
555+ const output = await apply ( {
556+ content,
557+ oldString : normalize ( old , "\n" ) ,
558+ newString : normalize ( next , "\n" ) ,
559+ } )
560+ expect ( output ) . toBe ( normalize ( next + "\n" , "\r\n" ) )
561+ expectCrlf ( output )
562+ } )
563+
564+ test ( "preserves LF when newString uses CRLF" , async ( ) => {
565+ const content = normalize ( old + "\n" , "\n" )
566+ const output = await apply ( {
567+ content,
568+ oldString : normalize ( old , "\n" ) ,
569+ newString : normalize ( next , "\r\n" ) ,
570+ } )
571+ expect ( output ) . toBe ( normalize ( next + "\n" , "\n" ) )
572+ expectLf ( output )
573+ } )
574+
575+ test ( "preserves CRLF when newString uses LF" , async ( ) => {
576+ const content = normalize ( old + "\n" , "\r\n" )
577+ const output = await apply ( {
578+ content,
579+ oldString : normalize ( old , "\r\n" ) ,
580+ newString : normalize ( next , "\n" ) ,
581+ } )
582+ expect ( output ) . toBe ( normalize ( next + "\n" , "\r\n" ) )
583+ expectCrlf ( output )
584+ } )
585+
586+ test ( "preserves LF with mixed old/new line endings" , async ( ) => {
587+ const content = normalize ( old + "\n" , "\n" )
588+ const output = await apply ( {
589+ content,
590+ oldString : "alpha\nbeta\r\ngamma" ,
591+ newString : "alpha\r\nbeta\nomega" ,
592+ } )
593+ expect ( output ) . toBe ( normalize ( alt + "\n" , "\n" ) )
594+ expectLf ( output )
595+ } )
596+
597+ test ( "preserves CRLF with mixed old/new line endings" , async ( ) => {
598+ const content = normalize ( old + "\n" , "\r\n" )
599+ const output = await apply ( {
600+ content,
601+ oldString : "alpha\r\nbeta\ngamma" ,
602+ newString : "alpha\nbeta\r\nomega" ,
603+ } )
604+ expect ( output ) . toBe ( normalize ( alt + "\n" , "\r\n" ) )
605+ expectCrlf ( output )
606+ } )
607+
608+ test ( "replaceAll preserves LF for multi-line blocks" , async ( ) => {
609+ const blockOld = "alpha\nbeta"
610+ const blockNew = "alpha\nbeta-updated"
611+ const content = normalize ( blockOld + "\n" + blockOld + "\n" , "\n" )
612+ const output = await apply ( {
613+ content,
614+ oldString : normalize ( blockOld , "\n" ) ,
615+ newString : normalize ( blockNew , "\n" ) ,
616+ replaceAll : true ,
617+ } )
618+ expect ( output ) . toBe ( normalize ( blockNew + "\n" + blockNew + "\n" , "\n" ) )
619+ expectLf ( output )
620+ } )
621+
622+ test ( "replaceAll preserves CRLF for multi-line blocks" , async ( ) => {
623+ const blockOld = "alpha\nbeta"
624+ const blockNew = "alpha\nbeta-updated"
625+ const content = normalize ( blockOld + "\n" + blockOld + "\n" , "\r\n" )
626+ const output = await apply ( {
627+ content,
628+ oldString : normalize ( blockOld , "\r\n" ) ,
629+ newString : normalize ( blockNew , "\r\n" ) ,
630+ replaceAll : true ,
631+ } )
632+ expect ( output ) . toBe ( normalize ( blockNew + "\n" + blockNew + "\n" , "\r\n" ) )
633+ expectCrlf ( output )
634+ } )
635+ } )
636+
454637 describe ( "concurrent editing" , ( ) => {
455638 test ( "serializes concurrent edits to same file" , async ( ) => {
456639 await using tmp = await tmpdir ( )
0 commit comments