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}