edit.go 6.9 KB

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