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