11package gopls
22
33import (
4+ "bytes"
45 "fmt"
56 "go/ast"
67 "go/parser"
78 "go/token"
89 "log/slog"
910 "os"
1011 "os/exec"
12+ "strings"
13+ "time"
1114)
1215
1316// Rename renames a symbol in a Go file using gopls.
@@ -32,12 +35,17 @@ func Rename(goplsPath string, filePath string, oldName string, newName string, m
3235
3336 cmd := exec .Command (goplsPath , "rename" , "-w" , positionSpec , newName )
3437 cmd .Dir = moduleRoot
35- output , err := cmd .CombinedOutput ()
38+
39+ var combinedOutput bytes.Buffer
40+ cmd .Stdout = & combinedOutput
41+ cmd .Stderr = & combinedOutput
42+
43+ _ , err = runWithTextFileBusyRetry (cmd )
3644 if err != nil {
37- return fmt .Errorf ("gopls rename failed: %w\n Output: %s" , err , string ( output ))
45+ return fmt .Errorf ("gopls rename failed: %w\n Output: %s" , err , combinedOutput . String ( ))
3846 }
3947
40- slog .Debug ("Successfully renamed symbol" , "old" , oldName , "new" , newName , "output" , string ( output ), "goplsPath" , goplsPath )
48+ slog .Debug ("Successfully renamed symbol" , "old" , oldName , "new" , newName , "output" , combinedOutput . String ( ), "goplsPath" , goplsPath )
4149 return nil
4250}
4351
@@ -106,3 +114,41 @@ func findSymbolOffset(filePath string, symbolName string) (int, error) {
106114 position := fset .Position (symbolPos )
107115 return position .Offset , nil
108116}
117+
118+ // runWithTextFileBusyRetry runs a command and retries if it fails with "text file busy" error.
119+ // This error can occur when a binary was just installed by "go install" and the OS hasn't
120+ // fully released the file handle yet. The function retries up to 3 times with exponential backoff.
121+ // Note: This function uses cmd.Run(), so caller should capture output via cmd.Stdout/Stderr if needed.
122+ func runWithTextFileBusyRetry (cmd * exec.Cmd ) ([]byte , error ) {
123+ const maxRetries = 3
124+ var lastErr error
125+
126+ for attempt := 0 ; attempt < maxRetries ; attempt ++ {
127+ // Create a new command for each attempt since exec.Cmd can only be used once
128+ if attempt > 0 {
129+ newCmd := exec .Command (cmd .Path , cmd .Args [1 :]... )
130+ newCmd .Dir = cmd .Dir
131+ newCmd .Env = cmd .Env
132+ newCmd .Stdout = cmd .Stdout
133+ newCmd .Stderr = cmd .Stderr
134+ cmd = newCmd
135+
136+ // Wait before retrying with exponential backoff
137+ waitTime := time .Duration (50 * (1 << attempt )) * time .Millisecond
138+ slog .Debug ("Retrying command after text file busy error" , "attempt" , attempt + 1 , "wait" , waitTime )
139+ time .Sleep (waitTime )
140+ }
141+
142+ lastErr = cmd .Run ()
143+ if lastErr == nil {
144+ return nil , nil
145+ }
146+
147+ // Check if this is a "text file busy" error
148+ if ! strings .Contains (lastErr .Error (), "text file busy" ) {
149+ return nil , lastErr
150+ }
151+ }
152+
153+ return nil , lastErr
154+ }
0 commit comments