edit.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package util
  2. import (
  3. "bytes"
  4. "fmt"
  5. "os"
  6. "sort"
  7. "strings"
  8. "github.com/sst/opencode/internal/lsp/protocol"
  9. )
  10. func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error {
  11. path := strings.TrimPrefix(string(uri), "file://")
  12. // Read the file content
  13. content, err := os.ReadFile(path)
  14. if err != nil {
  15. return fmt.Errorf("failed to read file: %w", err)
  16. }
  17. // Detect line ending style
  18. var lineEnding string
  19. if bytes.Contains(content, []byte("\r\n")) {
  20. lineEnding = "\r\n"
  21. } else {
  22. lineEnding = "\n"
  23. }
  24. // Track if file ends with a newline
  25. endsWithNewline := len(content) > 0 && bytes.HasSuffix(content, []byte(lineEnding))
  26. // Split into lines without the endings
  27. lines := strings.Split(string(content), lineEnding)
  28. // Check for overlapping edits
  29. for i, edit1 := range edits {
  30. for j := i + 1; j < len(edits); j++ {
  31. if rangesOverlap(edit1.Range, edits[j].Range) {
  32. return fmt.Errorf("overlapping edits detected between edit %d and %d", i, j)
  33. }
  34. }
  35. }
  36. // Sort edits in reverse order
  37. sortedEdits := make([]protocol.TextEdit, len(edits))
  38. copy(sortedEdits, edits)
  39. sort.Slice(sortedEdits, func(i, j int) bool {
  40. if sortedEdits[i].Range.Start.Line != sortedEdits[j].Range.Start.Line {
  41. return sortedEdits[i].Range.Start.Line > sortedEdits[j].Range.Start.Line
  42. }
  43. return sortedEdits[i].Range.Start.Character > sortedEdits[j].Range.Start.Character
  44. })
  45. // Apply each edit
  46. for _, edit := range sortedEdits {
  47. newLines, err := applyTextEdit(lines, edit)
  48. if err != nil {
  49. return fmt.Errorf("failed to apply edit: %w", err)
  50. }
  51. lines = newLines
  52. }
  53. // Join lines with proper line endings
  54. var newContent strings.Builder
  55. for i, line := range lines {
  56. if i > 0 {
  57. newContent.WriteString(lineEnding)
  58. }
  59. newContent.WriteString(line)
  60. }
  61. // Only add a newline if the original file had one and we haven't already added it
  62. if endsWithNewline && !strings.HasSuffix(newContent.String(), lineEnding) {
  63. newContent.WriteString(lineEnding)
  64. }
  65. if err := os.WriteFile(path, []byte(newContent.String()), 0o644); err != nil {
  66. return fmt.Errorf("failed to write file: %w", err)
  67. }
  68. return nil
  69. }
  70. func applyTextEdit(lines []string, edit protocol.TextEdit) ([]string, error) {
  71. startLine := int(edit.Range.Start.Line)
  72. endLine := int(edit.Range.End.Line)
  73. startChar := int(edit.Range.Start.Character)
  74. endChar := int(edit.Range.End.Character)
  75. // Validate positions
  76. if startLine < 0 || startLine >= len(lines) {
  77. return nil, fmt.Errorf("invalid start line: %d", startLine)
  78. }
  79. if endLine < 0 || endLine >= len(lines) {
  80. endLine = len(lines) - 1
  81. }
  82. // Create result slice with initial capacity
  83. result := make([]string, 0, len(lines))
  84. // Copy lines before edit
  85. result = append(result, lines[:startLine]...)
  86. // Get the prefix of the start line
  87. startLineContent := lines[startLine]
  88. if startChar < 0 || startChar > len(startLineContent) {
  89. startChar = len(startLineContent)
  90. }
  91. prefix := startLineContent[:startChar]
  92. // Get the suffix of the end line
  93. endLineContent := lines[endLine]
  94. if endChar < 0 || endChar > len(endLineContent) {
  95. endChar = len(endLineContent)
  96. }
  97. suffix := endLineContent[endChar:]
  98. // Handle the edit
  99. if edit.NewText == "" {
  100. if prefix+suffix != "" {
  101. result = append(result, prefix+suffix)
  102. }
  103. } else {
  104. // Split new text into lines, being careful not to add extra newlines
  105. // newLines := strings.Split(strings.TrimRight(edit.NewText, "\n"), "\n")
  106. newLines := strings.Split(edit.NewText, "\n")
  107. if len(newLines) == 1 {
  108. // Single line change
  109. result = append(result, prefix+newLines[0]+suffix)
  110. } else {
  111. // Multi-line change
  112. result = append(result, prefix+newLines[0])
  113. result = append(result, newLines[1:len(newLines)-1]...)
  114. result = append(result, newLines[len(newLines)-1]+suffix)
  115. }
  116. }
  117. // Add remaining lines
  118. if endLine+1 < len(lines) {
  119. result = append(result, lines[endLine+1:]...)
  120. }
  121. return result, nil
  122. }
  123. // applyDocumentChange applies a DocumentChange (create/rename/delete operations)
  124. func applyDocumentChange(change protocol.DocumentChange) error {
  125. if change.CreateFile != nil {
  126. path := strings.TrimPrefix(string(change.CreateFile.URI), "file://")
  127. if change.CreateFile.Options != nil {
  128. if change.CreateFile.Options.Overwrite {
  129. // Proceed with overwrite
  130. } else if change.CreateFile.Options.IgnoreIfExists {
  131. if _, err := os.Stat(path); err == nil {
  132. return nil // File exists and we're ignoring it
  133. }
  134. }
  135. }
  136. if err := os.WriteFile(path, []byte(""), 0o644); err != nil {
  137. return fmt.Errorf("failed to create file: %w", err)
  138. }
  139. }
  140. if change.DeleteFile != nil {
  141. path := strings.TrimPrefix(string(change.DeleteFile.URI), "file://")
  142. if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive {
  143. if err := os.RemoveAll(path); err != nil {
  144. return fmt.Errorf("failed to delete directory recursively: %w", err)
  145. }
  146. } else {
  147. if err := os.Remove(path); err != nil {
  148. return fmt.Errorf("failed to delete file: %w", err)
  149. }
  150. }
  151. }
  152. if change.RenameFile != nil {
  153. oldPath := strings.TrimPrefix(string(change.RenameFile.OldURI), "file://")
  154. newPath := strings.TrimPrefix(string(change.RenameFile.NewURI), "file://")
  155. if change.RenameFile.Options != nil {
  156. if !change.RenameFile.Options.Overwrite {
  157. if _, err := os.Stat(newPath); err == nil {
  158. return fmt.Errorf("target file already exists and overwrite is not allowed: %s", newPath)
  159. }
  160. }
  161. }
  162. if err := os.Rename(oldPath, newPath); err != nil {
  163. return fmt.Errorf("failed to rename file: %w", err)
  164. }
  165. }
  166. if change.TextDocumentEdit != nil {
  167. textEdits := make([]protocol.TextEdit, len(change.TextDocumentEdit.Edits))
  168. for i, edit := range change.TextDocumentEdit.Edits {
  169. var err error
  170. textEdits[i], err = edit.AsTextEdit()
  171. if err != nil {
  172. return fmt.Errorf("invalid edit type: %w", err)
  173. }
  174. }
  175. return applyTextEdits(change.TextDocumentEdit.TextDocument.URI, textEdits)
  176. }
  177. return nil
  178. }
  179. // ApplyWorkspaceEdit applies the given WorkspaceEdit to the filesystem
  180. func ApplyWorkspaceEdit(edit protocol.WorkspaceEdit) error {
  181. // Handle Changes field
  182. for uri, textEdits := range edit.Changes {
  183. if err := applyTextEdits(uri, textEdits); err != nil {
  184. return fmt.Errorf("failed to apply text edits: %w", err)
  185. }
  186. }
  187. // Handle DocumentChanges field
  188. for _, change := range edit.DocumentChanges {
  189. if err := applyDocumentChange(change); err != nil {
  190. return fmt.Errorf("failed to apply document change: %w", err)
  191. }
  192. }
  193. return nil
  194. }
  195. func rangesOverlap(r1, r2 protocol.Range) bool {
  196. if r1.Start.Line > r2.End.Line || r2.Start.Line > r1.End.Line {
  197. return false
  198. }
  199. if r1.Start.Line == r2.End.Line && r1.Start.Character > r2.End.Character {
  200. return false
  201. }
  202. if r2.Start.Line == r1.End.Line && r2.Start.Character > r1.End.Character {
  203. return false
  204. }
  205. return true
  206. }