diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-03 15:20:15 +0200 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-04-03 17:23:41 +0200 |
| commit | cfdd687216799cb5b47f099f1e7cd5dd16b3bdd0 (patch) | |
| tree | a822bfde1463a7080c0ea06dd17796d7a1617d3d /internal/lsp/util | |
| parent | afd9ad0560d76c2a6d161dad52553b10ff428905 (diff) | |
| download | opencode-cfdd687216799cb5b47f099f1e7cd5dd16b3bdd0.tar.gz opencode-cfdd687216799cb5b47f099f1e7cd5dd16b3bdd0.zip | |
add initial lsp support
Diffstat (limited to 'internal/lsp/util')
| -rw-r--r-- | internal/lsp/util/edit.go | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/internal/lsp/util/edit.go b/internal/lsp/util/edit.go new file mode 100644 index 000000000..180429ce5 --- /dev/null +++ b/internal/lsp/util/edit.go @@ -0,0 +1,239 @@ +package util + +import ( + "bytes" + "fmt" + "os" + "sort" + "strings" + + "github.com/kujtimiihoxha/termai/internal/lsp/protocol" +) + +func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error { + path := strings.TrimPrefix(string(uri), "file://") + + // Read the file content + content, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + // Detect line ending style + var lineEnding string + if bytes.Contains(content, []byte("\r\n")) { + lineEnding = "\r\n" + } else { + lineEnding = "\n" + } + + // Track if file ends with a newline + endsWithNewline := len(content) > 0 && bytes.HasSuffix(content, []byte(lineEnding)) + + // Split into lines without the endings + lines := strings.Split(string(content), lineEnding) + + // Check for overlapping edits + for i := 0; i < len(edits); i++ { + for j := i + 1; j < len(edits); j++ { + if rangesOverlap(edits[i].Range, edits[j].Range) { + return fmt.Errorf("overlapping edits detected between edit %d and %d", i, j) + } + } + } + + // Sort edits in reverse order + sortedEdits := make([]protocol.TextEdit, len(edits)) + copy(sortedEdits, edits) + sort.Slice(sortedEdits, func(i, j int) bool { + if sortedEdits[i].Range.Start.Line != sortedEdits[j].Range.Start.Line { + return sortedEdits[i].Range.Start.Line > sortedEdits[j].Range.Start.Line + } + return sortedEdits[i].Range.Start.Character > sortedEdits[j].Range.Start.Character + }) + + // Apply each edit + for _, edit := range sortedEdits { + newLines, err := applyTextEdit(lines, edit, lineEnding) + if err != nil { + return fmt.Errorf("failed to apply edit: %w", err) + } + lines = newLines + } + + // Join lines with proper line endings + var newContent strings.Builder + for i, line := range lines { + if i > 0 { + newContent.WriteString(lineEnding) + } + newContent.WriteString(line) + } + + // Only add a newline if the original file had one and we haven't already added it + if endsWithNewline && !strings.HasSuffix(newContent.String(), lineEnding) { + newContent.WriteString(lineEnding) + } + + if err := os.WriteFile(path, []byte(newContent.String()), 0o644); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + return nil +} + +func applyTextEdit(lines []string, edit protocol.TextEdit, lineEnding string) ([]string, error) { + startLine := int(edit.Range.Start.Line) + endLine := int(edit.Range.End.Line) + startChar := int(edit.Range.Start.Character) + endChar := int(edit.Range.End.Character) + + // Validate positions + if startLine < 0 || startLine >= len(lines) { + return nil, fmt.Errorf("invalid start line: %d", startLine) + } + if endLine < 0 || endLine >= len(lines) { + endLine = len(lines) - 1 + } + + // Create result slice with initial capacity + result := make([]string, 0, len(lines)) + + // Copy lines before edit + result = append(result, lines[:startLine]...) + + // Get the prefix of the start line + startLineContent := lines[startLine] + if startChar < 0 || startChar > len(startLineContent) { + startChar = len(startLineContent) + } + prefix := startLineContent[:startChar] + + // Get the suffix of the end line + endLineContent := lines[endLine] + if endChar < 0 || endChar > len(endLineContent) { + endChar = len(endLineContent) + } + suffix := endLineContent[endChar:] + + // Handle the edit + if edit.NewText == "" { + if prefix+suffix != "" { + result = append(result, prefix+suffix) + } + } else { + // Split new text into lines, being careful not to add extra newlines + // newLines := strings.Split(strings.TrimRight(edit.NewText, "\n"), "\n") + newLines := strings.Split(edit.NewText, "\n") + + if len(newLines) == 1 { + // Single line change + result = append(result, prefix+newLines[0]+suffix) + } else { + // Multi-line change + result = append(result, prefix+newLines[0]) + result = append(result, newLines[1:len(newLines)-1]...) + result = append(result, newLines[len(newLines)-1]+suffix) + } + } + + // Add remaining lines + if endLine+1 < len(lines) { + result = append(result, lines[endLine+1:]...) + } + + return result, nil +} + +// applyDocumentChange applies a DocumentChange (create/rename/delete operations) +func applyDocumentChange(change protocol.DocumentChange) error { + if change.CreateFile != nil { + path := strings.TrimPrefix(string(change.CreateFile.URI), "file://") + if change.CreateFile.Options != nil { + if change.CreateFile.Options.Overwrite { + // Proceed with overwrite + } else if change.CreateFile.Options.IgnoreIfExists { + if _, err := os.Stat(path); err == nil { + return nil // File exists and we're ignoring it + } + } + } + if err := os.WriteFile(path, []byte(""), 0o644); err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + } + + if change.DeleteFile != nil { + path := strings.TrimPrefix(string(change.DeleteFile.URI), "file://") + if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive { + if err := os.RemoveAll(path); err != nil { + return fmt.Errorf("failed to delete directory recursively: %w", err) + } + } else { + if err := os.Remove(path); err != nil { + return fmt.Errorf("failed to delete file: %w", err) + } + } + } + + if change.RenameFile != nil { + oldPath := strings.TrimPrefix(string(change.RenameFile.OldURI), "file://") + newPath := strings.TrimPrefix(string(change.RenameFile.NewURI), "file://") + if change.RenameFile.Options != nil { + if !change.RenameFile.Options.Overwrite { + if _, err := os.Stat(newPath); err == nil { + return fmt.Errorf("target file already exists and overwrite is not allowed: %s", newPath) + } + } + } + if err := os.Rename(oldPath, newPath); err != nil { + return fmt.Errorf("failed to rename file: %w", err) + } + } + + if change.TextDocumentEdit != nil { + textEdits := make([]protocol.TextEdit, len(change.TextDocumentEdit.Edits)) + for i, edit := range change.TextDocumentEdit.Edits { + var err error + textEdits[i], err = edit.AsTextEdit() + if err != nil { + return fmt.Errorf("invalid edit type: %w", err) + } + } + return applyTextEdits(change.TextDocumentEdit.TextDocument.URI, textEdits) + } + + return nil +} + +// ApplyWorkspaceEdit applies the given WorkspaceEdit to the filesystem +func ApplyWorkspaceEdit(edit protocol.WorkspaceEdit) error { + // Handle Changes field + for uri, textEdits := range edit.Changes { + if err := applyTextEdits(uri, textEdits); err != nil { + return fmt.Errorf("failed to apply text edits: %w", err) + } + } + + // Handle DocumentChanges field + for _, change := range edit.DocumentChanges { + if err := applyDocumentChange(change); err != nil { + return fmt.Errorf("failed to apply document change: %w", err) + } + } + + return nil +} + +func rangesOverlap(r1, r2 protocol.Range) bool { + if r1.Start.Line > r2.End.Line || r2.Start.Line > r1.End.Line { + return false + } + if r1.Start.Line == r2.End.Line && r1.Start.Character > r2.End.Character { + return false + } + if r2.Start.Line == r1.End.Line && r2.Start.Character > r1.End.Character { + return false + } + return true +} |
