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