summaryrefslogtreecommitdiffhomepage
path: root/internal/lsp/util
diff options
context:
space:
mode:
authorKujtim Hoxha <[email protected]>2025-04-03 15:20:15 +0200
committerKujtim Hoxha <[email protected]>2025-04-03 17:23:41 +0200
commitcfdd687216799cb5b47f099f1e7cd5dd16b3bdd0 (patch)
treea822bfde1463a7080c0ea06dd17796d7a1617d3d /internal/lsp/util
parentafd9ad0560d76c2a6d161dad52553b10ff428905 (diff)
downloadopencode-cfdd687216799cb5b47f099f1e7cd5dd16b3bdd0.tar.gz
opencode-cfdd687216799cb5b47f099f1e7cd5dd16b3bdd0.zip
add initial lsp support
Diffstat (limited to 'internal/lsp/util')
-rw-r--r--internal/lsp/util/edit.go239
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
+}