@@ -2,8 +2,11 @@ package environment
22
33import (
44 "context"
5+ "crypto/sha256"
56 "fmt"
67 "strings"
8+
9+ godiffpatch "github.com/sourcegraph/go-diff-patch"
710)
811
912func (env * Environment ) FileRead (ctx context.Context , targetFile string , shouldReadEntireFile bool , startLineOneIndexedInclusive int , endLineOneIndexedInclusive int ) (string , error ) {
@@ -45,6 +48,81 @@ func (env *Environment) FileWrite(ctx context.Context, explanation, targetFile,
4548 return nil
4649}
4750
51+ func (env * Environment ) FileEdit (ctx context.Context , explanation , targetFile , search , replace , matchID string ) error {
52+ contents , err := env .container ().File (targetFile ).Contents (ctx )
53+ if err != nil {
54+ return err
55+ }
56+
57+ // Find all matches of the search text
58+ matches := []int {}
59+ cursor := 0
60+ for {
61+ index := strings .Index (contents [cursor :], search )
62+ if index == - 1 {
63+ break
64+ }
65+ actualIndex := cursor + index
66+ matches = append (matches , actualIndex )
67+ cursor = actualIndex + 1
68+ }
69+
70+ if len (matches ) == 0 {
71+ return fmt .Errorf ("search text not found in file %s" , targetFile )
72+ }
73+
74+ // If there are multiple matches and no matchID is provided, return an error with all matches
75+ if len (matches ) > 1 && matchID == "" {
76+ var matchDescriptions []string
77+ for i , matchIndex := range matches {
78+ // Generate a unique ID for each match
79+ id := generateMatchID (targetFile , search , replace , i )
80+
81+ // Get context around the match (3 lines before and after)
82+ context := getMatchContext (contents , matchIndex )
83+
84+ matchDescriptions = append (matchDescriptions , fmt .Sprintf ("Match %d (ID: %s):\n %s" , i + 1 , id , context ))
85+ }
86+
87+ return fmt .Errorf ("multiple matches found for search text in %s. Please specify which_match parameter with one of the following IDs:\n \n %s" ,
88+ targetFile , strings .Join (matchDescriptions , "\n \n " ))
89+ }
90+
91+ // Determine which match to replace
92+ var targetMatchIndex int
93+ if len (matches ) == 1 {
94+ targetMatchIndex = matches [0 ]
95+ } else {
96+ // Find the match with the specified ID
97+ found := false
98+ for i , matchIndex := range matches {
99+ id := generateMatchID (targetFile , search , replace , i )
100+ if id == matchID {
101+ targetMatchIndex = matchIndex
102+ found = true
103+ break
104+ }
105+ }
106+ if ! found {
107+ return fmt .Errorf ("match ID %s not found" , matchID )
108+ }
109+ }
110+
111+ // Replace the specific match
112+ newContents := contents [:targetMatchIndex ] + replace + contents [targetMatchIndex + len (search ):]
113+
114+ // Apply the changes using `Directory.withPatch` so we don't have to spit out
115+ // the entire contents
116+ patch := godiffpatch .GeneratePatch (targetFile , contents , newContents )
117+ ctr := env .container ()
118+ err = env .apply (ctx , ctr .WithDirectory ("." , ctr .Directory ("." ).WithPatch (patch )))
119+ if err != nil {
120+ return fmt .Errorf ("failed applying file edit, skipping git propagation: %w" , err )
121+ }
122+ env .Notes .Add ("Edit %s" , targetFile )
123+ return nil
124+ }
125+
48126func (env * Environment ) FileDelete (ctx context.Context , explanation , targetFile string ) error {
49127 err := env .apply (ctx , env .container ().WithoutFile (targetFile ))
50128 if err != nil {
@@ -65,3 +143,43 @@ func (env *Environment) FileList(ctx context.Context, path string) (string, erro
65143 }
66144 return out .String (), nil
67145}
146+
147+ // generateMatchID creates a unique ID for a match based on file, search, replace, and index
148+ func generateMatchID (targetFile , search , replace string , index int ) string {
149+ data := fmt .Sprintf ("%s:%s:%s:%d" , targetFile , search , replace , index )
150+ hash := sha256 .Sum256 ([]byte (data ))
151+ return fmt .Sprintf ("%x" , hash )[:8 ] // Use first 8 characters of hash
152+ }
153+
154+ // getMatchContext returns the context around a match (3 lines before and after)
155+ func getMatchContext (contents string , matchIndex int ) string {
156+ lines := strings .Split (contents , "\n " )
157+
158+ // Find which line contains the match
159+ currentPos := 0
160+ matchLine := 0
161+ for i , line := range lines {
162+ if currentPos + len (line ) >= matchIndex {
163+ matchLine = i
164+ break
165+ }
166+ currentPos += len (line ) + 1 // +1 for newline
167+ }
168+
169+ // Get context lines (3 before, match line, 3 after)
170+ start := max (0 , matchLine - 3 )
171+ end := min (len (lines ), matchLine + 4 )
172+
173+ contextLines := make ([]string , 0 , end - start )
174+ for i := start ; i < end ; i ++ {
175+ prefix := " "
176+ if i == matchLine {
177+ prefix = "> " // Mark the line containing the match
178+ }
179+ // Include line numbers, which may help the model determine the right match
180+ prefix += fmt .Sprintf ("%4d | " , i + 1 )
181+ contextLines = append (contextLines , fmt .Sprintf ("%s%s" , prefix , lines [i ]))
182+ }
183+
184+ return strings .Join (contextLines , "\n " )
185+ }
0 commit comments