55
66import z from "zod"
77import * as path from "path"
8- import { Effect } from "effect"
8+ import { Effect , Semaphore } from "effect"
99import * as Tool from "./tool"
1010import { LSP } from "../lsp"
1111import { createTwoFilesPatch , diffLines } from "diff"
@@ -32,6 +32,18 @@ function convertToLineEnding(text: string, ending: "\n" | "\r\n"): string {
3232 return text . replaceAll ( "\n" , "\r\n" )
3333}
3434
35+ const locks = new Map < string , Semaphore . Semaphore > ( )
36+
37+ function lock ( filePath : string ) {
38+ const resolvedFilePath = AppFileSystem . resolve ( filePath )
39+ const hit = locks . get ( resolvedFilePath )
40+ if ( hit ) return hit
41+
42+ const next = Semaphore . makeUnsafe ( 1 )
43+ locks . set ( resolvedFilePath , next )
44+ return next
45+ }
46+
3547const Parameters = z . object ( {
3648 filePath : z . string ( ) . describe ( "The absolute path to the file to modify" ) ,
3749 oldString : z . string ( ) . describe ( "The text to replace" ) ,
@@ -68,11 +80,50 @@ export const EditTool = Tool.define(
6880 let diff = ""
6981 let contentOld = ""
7082 let contentNew = ""
71- yield * Effect . gen ( function * ( ) {
72- if ( params . oldString === "" ) {
73- const existed = yield * afs . existsSafe ( filePath )
74- contentNew = params . newString
75- diff = trimDiff ( createTwoFilesPatch ( filePath , filePath , contentOld , contentNew ) )
83+ yield * lock ( filePath ) . withPermits ( 1 ) (
84+ Effect . gen ( function * ( ) {
85+ if ( params . oldString === "" ) {
86+ const existed = yield * afs . existsSafe ( filePath )
87+ contentNew = params . newString
88+ diff = trimDiff ( createTwoFilesPatch ( filePath , filePath , contentOld , contentNew ) )
89+ yield * ctx . ask ( {
90+ permission : "edit" ,
91+ patterns : [ path . relative ( Instance . worktree , filePath ) ] ,
92+ always : [ "*" ] ,
93+ metadata : {
94+ filepath : filePath ,
95+ diff,
96+ } ,
97+ } )
98+ yield * afs . writeWithDirs ( filePath , params . newString )
99+ yield * format . file ( filePath )
100+ yield * bus . publish ( File . Event . Edited , { file : filePath } )
101+ yield * bus . publish ( FileWatcher . Event . Updated , {
102+ file : filePath ,
103+ event : existed ? "change" : "add" ,
104+ } )
105+ return
106+ }
107+
108+ const info = yield * afs . stat ( filePath ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( undefined ) ) )
109+ if ( ! info ) throw new Error ( `File ${ filePath } not found` )
110+ if ( info . type === "Directory" ) throw new Error ( `Path is a directory, not a file: ${ filePath } ` )
111+ contentOld = yield * afs . readFileString ( filePath )
112+
113+ const ending = detectLineEnding ( contentOld )
114+ const old = convertToLineEnding ( normalizeLineEndings ( params . oldString ) , ending )
115+ const next = convertToLineEnding ( normalizeLineEndings ( params . newString ) , ending )
116+
117+ contentNew = replace ( contentOld , old , next , params . replaceAll )
118+
119+ diff = trimDiff (
120+ createTwoFilesPatch (
121+ filePath ,
122+ filePath ,
123+ normalizeLineEndings ( contentOld ) ,
124+ normalizeLineEndings ( contentNew ) ,
125+ ) ,
126+ )
76127 yield * ctx . ask ( {
77128 permission : "edit" ,
78129 patterns : [ path . relative ( Instance . worktree , filePath ) ] ,
@@ -82,62 +133,25 @@ export const EditTool = Tool.define(
82133 diff,
83134 } ,
84135 } )
85- yield * afs . writeWithDirs ( filePath , params . newString )
136+
137+ yield * afs . writeWithDirs ( filePath , contentNew )
86138 yield * format . file ( filePath )
87139 yield * bus . publish ( File . Event . Edited , { file : filePath } )
88140 yield * bus . publish ( FileWatcher . Event . Updated , {
89141 file : filePath ,
90- event : existed ? "change" : "add ",
142+ event : "change" ,
91143 } )
92- return
93- }
94-
95- const info = yield * afs . stat ( filePath ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( undefined ) ) )
96- if ( ! info ) throw new Error ( `File ${ filePath } not found` )
97- if ( info . type === "Directory" ) throw new Error ( `Path is a directory, not a file: ${ filePath } ` )
98- contentOld = yield * afs . readFileString ( filePath )
99-
100- const ending = detectLineEnding ( contentOld )
101- const old = convertToLineEnding ( normalizeLineEndings ( params . oldString ) , ending )
102- const next = convertToLineEnding ( normalizeLineEndings ( params . newString ) , ending )
103-
104- contentNew = replace ( contentOld , old , next , params . replaceAll )
105-
106- diff = trimDiff (
107- createTwoFilesPatch (
108- filePath ,
109- filePath ,
110- normalizeLineEndings ( contentOld ) ,
111- normalizeLineEndings ( contentNew ) ,
112- ) ,
113- )
114- yield * ctx . ask ( {
115- permission : "edit" ,
116- patterns : [ path . relative ( Instance . worktree , filePath ) ] ,
117- always : [ "*" ] ,
118- metadata : {
119- filepath : filePath ,
120- diff,
121- } ,
122- } )
123-
124- yield * afs . writeWithDirs ( filePath , contentNew )
125- yield * format . file ( filePath )
126- yield * bus . publish ( File . Event . Edited , { file : filePath } )
127- yield * bus . publish ( FileWatcher . Event . Updated , {
128- file : filePath ,
129- event : "change" ,
130- } )
131- contentNew = yield * afs . readFileString ( filePath )
132- diff = trimDiff (
133- createTwoFilesPatch (
134- filePath ,
135- filePath ,
136- normalizeLineEndings ( contentOld ) ,
137- normalizeLineEndings ( contentNew ) ,
138- ) ,
139- )
140- } ) . pipe ( Effect . orDie )
144+ contentNew = yield * afs . readFileString ( filePath )
145+ diff = trimDiff (
146+ createTwoFilesPatch (
147+ filePath ,
148+ filePath ,
149+ normalizeLineEndings ( contentOld ) ,
150+ normalizeLineEndings ( contentNew ) ,
151+ ) ,
152+ )
153+ } ) . pipe ( Effect . orDie ) ,
154+ )
141155
142156 const filediff : Snapshot . FileDiff = {
143157 file : filePath ,
0 commit comments