492 lines
12 KiB
Go
492 lines
12 KiB
Go
// Multicall binary for LSP tools.
|
|
// Invoked via symlinks: lsp_hover, lsp_definition, lsp_references,
|
|
// lsp_completion, lsp_diagnostics, lsp_symbols, lsp_rename.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"ollie/tools/lsp/internal/lsp"
|
|
"ollie/tools/lsp/internal/cmdutil"
|
|
)
|
|
|
|
func main() {
|
|
name := filepath.Base(os.Args[0])
|
|
switch name {
|
|
case "lsp_hover":
|
|
cmdHover()
|
|
case "lsp_definition":
|
|
cmdDefinition()
|
|
case "lsp_references":
|
|
cmdReferences()
|
|
case "lsp_completion":
|
|
cmdCompletion()
|
|
case "lsp_diagnostics":
|
|
cmdDiagnostics()
|
|
case "lsp_symbols":
|
|
cmdSymbols()
|
|
case "lsp_rename":
|
|
cmdRename()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "lsp: unknown tool %q\n", name)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// --- lsp_hover ---
|
|
|
|
func cmdHover() {
|
|
cmdutil.Run("hover", func(result json.RawMessage) {
|
|
var hover struct {
|
|
Contents struct {
|
|
Kind string `json:"kind"`
|
|
Value string `json:"value"`
|
|
} `json:"contents"`
|
|
}
|
|
if err := json.Unmarshal(result, &hover); err != nil {
|
|
var plain struct {
|
|
Contents string `json:"contents"`
|
|
}
|
|
if json.Unmarshal(result, &plain) == nil {
|
|
fmt.Println(plain.Contents)
|
|
return
|
|
}
|
|
fmt.Println("(no hover information)")
|
|
return
|
|
}
|
|
fmt.Println(hover.Contents.Value)
|
|
})
|
|
}
|
|
|
|
// --- lsp_definition ---
|
|
|
|
func cmdDefinition() {
|
|
cmdutil.Run("definition", func(result json.RawMessage) {
|
|
type position struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
}
|
|
type rang struct {
|
|
Start position `json:"start"`
|
|
}
|
|
var locations []struct {
|
|
URI string `json:"uri"`
|
|
TargetURI string `json:"targetUri"`
|
|
Range *rang `json:"range"`
|
|
TargetRange *rang `json:"targetRange"`
|
|
}
|
|
if err := json.Unmarshal(result, &locations); err != nil {
|
|
var single struct {
|
|
URI string `json:"uri"`
|
|
TargetURI string `json:"targetUri"`
|
|
Range *rang `json:"range"`
|
|
TargetRange *rang `json:"targetRange"`
|
|
}
|
|
if err := json.Unmarshal(result, &single); err != nil {
|
|
fmt.Println("(no definition found)")
|
|
return
|
|
}
|
|
locations = append(locations, single)
|
|
}
|
|
|
|
for _, loc := range locations {
|
|
uri := loc.URI
|
|
if uri == "" {
|
|
uri = loc.TargetURI
|
|
}
|
|
path := lsp.URIToPath(uri)
|
|
r := loc.Range
|
|
if r == nil {
|
|
r = loc.TargetRange
|
|
}
|
|
if r == nil {
|
|
fmt.Println(path)
|
|
continue
|
|
}
|
|
fmt.Printf("%s:%d:%d\n", path, r.Start.Line+1, r.Start.Character+1)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- lsp_references ---
|
|
|
|
func cmdReferences() {
|
|
cmdutil.Run("references", func(result json.RawMessage) {
|
|
var locations []struct {
|
|
URI string `json:"uri"`
|
|
Range struct {
|
|
Start struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
} `json:"start"`
|
|
} `json:"range"`
|
|
}
|
|
if err := json.Unmarshal(result, &locations); err != nil {
|
|
fmt.Println("(no references found)")
|
|
return
|
|
}
|
|
|
|
fmt.Printf("STATUS=ok MATCH_COUNT=%d\n", len(locations))
|
|
for _, loc := range locations {
|
|
path := lsp.URIToPath(loc.URI)
|
|
fmt.Printf("%s:%d:%d\n", path, loc.Range.Start.Line+1, loc.Range.Start.Character+1)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- lsp_completion ---
|
|
|
|
func cmdCompletion() {
|
|
cmdutil.Run("completion", func(result json.RawMessage) {
|
|
type completionItem struct {
|
|
Label string `json:"label"`
|
|
Kind int `json:"kind"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
printItem := func(item completionItem) {
|
|
kind := completionKind(item.Kind)
|
|
if item.Detail != "" {
|
|
fmt.Printf("%s %s [%s]\n", item.Label, item.Detail, kind)
|
|
} else {
|
|
fmt.Printf("%s [%s]\n", item.Label, kind)
|
|
}
|
|
}
|
|
|
|
var list struct {
|
|
Items []completionItem `json:"items"`
|
|
}
|
|
if json.Unmarshal(result, &list) == nil && len(list.Items) > 0 {
|
|
for _, item := range list.Items {
|
|
printItem(item)
|
|
}
|
|
return
|
|
}
|
|
|
|
var items []completionItem
|
|
if json.Unmarshal(result, &items) == nil {
|
|
for _, item := range items {
|
|
printItem(item)
|
|
}
|
|
return
|
|
}
|
|
|
|
fmt.Println("(no completions)")
|
|
})
|
|
}
|
|
|
|
// --- lsp_diagnostics ---
|
|
|
|
func cmdDiagnostics() {
|
|
cmdutil.RunCustom(func() {
|
|
var args struct {
|
|
Path string `json:"path"`
|
|
}
|
|
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
file, _ := filepath.Abs(args.Path)
|
|
|
|
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
|
|
Method: "diagnostics",
|
|
File: file,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if result == nil || string(result) == "null" || string(result) == "[]" {
|
|
fmt.Println("(no diagnostics)")
|
|
return
|
|
}
|
|
|
|
type diagnostic struct {
|
|
Range struct {
|
|
Start struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
} `json:"start"`
|
|
} `json:"range"`
|
|
Severity int `json:"severity"`
|
|
Message string `json:"message"`
|
|
}
|
|
var diags []diagnostic
|
|
if err := json.Unmarshal(result, &diags); err != nil {
|
|
fmt.Println("(no diagnostics)")
|
|
return
|
|
}
|
|
|
|
for _, d := range diags {
|
|
sev := severity(d.Severity)
|
|
line := d.Range.Start.Line + 1
|
|
col := d.Range.Start.Character + 1
|
|
fmt.Printf("%s:%d:%d: %s: %s\n", file, line, col, sev, d.Message)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- lsp_symbols ---
|
|
|
|
func cmdSymbols() {
|
|
cmdutil.RunCustom(func() {
|
|
var args struct {
|
|
Path string `json:"path"`
|
|
Query string `json:"query"`
|
|
}
|
|
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
file, _ := filepath.Abs(args.Path)
|
|
workspace := args.Query != ""
|
|
|
|
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
|
|
Method: "symbols",
|
|
File: file,
|
|
Query: args.Query,
|
|
Workspace: workspace,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if result == nil || string(result) == "null" {
|
|
fmt.Println("(no symbols found)")
|
|
return
|
|
}
|
|
|
|
type rang struct {
|
|
Start struct {
|
|
Line int `json:"line"`
|
|
} `json:"start"`
|
|
}
|
|
type documentSymbol struct {
|
|
Name string `json:"name"`
|
|
Kind int `json:"kind"`
|
|
Range rang `json:"range"`
|
|
Children []documentSymbol `json:"children"`
|
|
}
|
|
type workspaceSymbol struct {
|
|
Name string `json:"name"`
|
|
Kind int `json:"kind"`
|
|
Location struct {
|
|
URI string `json:"uri"`
|
|
Range rang `json:"range"`
|
|
} `json:"location"`
|
|
}
|
|
|
|
var docSymbols []documentSymbol
|
|
if json.Unmarshal(result, &docSymbols) == nil && len(docSymbols) > 0 {
|
|
var printDoc func([]documentSymbol, int)
|
|
printDoc = func(symbols []documentSymbol, indent int) {
|
|
prefix := strings.Repeat(" ", indent)
|
|
for _, s := range symbols {
|
|
line := s.Range.Start.Line + 1
|
|
fmt.Printf("%s%s L%d [%s]\n", prefix, s.Name, line, symbolKind(s.Kind))
|
|
if len(s.Children) > 0 {
|
|
printDoc(s.Children, indent+1)
|
|
}
|
|
}
|
|
}
|
|
printDoc(docSymbols, 0)
|
|
return
|
|
}
|
|
|
|
var wsSymbols []workspaceSymbol
|
|
if json.Unmarshal(result, &wsSymbols) == nil && len(wsSymbols) > 0 {
|
|
for _, s := range wsSymbols {
|
|
path := lsp.URIToPath(s.Location.URI)
|
|
line := s.Location.Range.Start.Line + 1
|
|
fmt.Printf("%s:%d %s [%s]\n", path, line, s.Name, symbolKind(s.Kind))
|
|
}
|
|
return
|
|
}
|
|
|
|
fmt.Println("(no symbols found)")
|
|
})
|
|
}
|
|
|
|
// --- lsp_rename ---
|
|
|
|
func cmdRename() {
|
|
cmdutil.RunCustom(func() {
|
|
var args struct {
|
|
Path string `json:"path"`
|
|
Line string `json:"line"`
|
|
Col string `json:"col"`
|
|
NewName string `json:"new_name"`
|
|
}
|
|
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
file, _ := filepath.Abs(args.Path)
|
|
line, _ := strconv.Atoi(args.Line)
|
|
col, _ := strconv.Atoi(args.Col)
|
|
|
|
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
|
|
Method: "rename",
|
|
File: file,
|
|
Line: line - 1,
|
|
Col: col - 1,
|
|
NewName: args.NewName,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if result == nil || string(result) == "null" {
|
|
fmt.Println("(rename produced no changes)")
|
|
return
|
|
}
|
|
|
|
summary := applyWorkspaceEdit(result)
|
|
if summary == "" {
|
|
fmt.Println("(rename produced no changes)")
|
|
} else {
|
|
fmt.Print(summary)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func applyWorkspaceEdit(raw json.RawMessage) string {
|
|
type textEdit struct {
|
|
Range struct {
|
|
Start struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
} `json:"start"`
|
|
End struct {
|
|
Line int `json:"line"`
|
|
Character int `json:"character"`
|
|
} `json:"end"`
|
|
} `json:"range"`
|
|
NewText string `json:"newText"`
|
|
}
|
|
var edit struct {
|
|
Changes map[string][]textEdit `json:"changes"`
|
|
DocumentChanges []struct {
|
|
TextDocument struct {
|
|
URI string `json:"uri"`
|
|
} `json:"textDocument"`
|
|
Edits []textEdit `json:"edits"`
|
|
} `json:"documentChanges"`
|
|
}
|
|
if err := json.Unmarshal(raw, &edit); err != nil {
|
|
return ""
|
|
}
|
|
|
|
changes := make(map[string][]textEdit)
|
|
if len(edit.DocumentChanges) > 0 {
|
|
for _, dc := range edit.DocumentChanges {
|
|
path := lsp.URIToPath(dc.TextDocument.URI)
|
|
changes[path] = append(changes[path], dc.Edits...)
|
|
}
|
|
} else {
|
|
for uri, edits := range edit.Changes {
|
|
path := lsp.URIToPath(uri)
|
|
changes[path] = edits
|
|
}
|
|
}
|
|
|
|
var sb strings.Builder
|
|
totalEdits := 0
|
|
for path, edits := range changes {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
lines := strings.SplitAfter(string(data), "\n")
|
|
|
|
sort.Slice(edits, func(i, j int) bool {
|
|
if edits[i].Range.Start.Line != edits[j].Range.Start.Line {
|
|
return edits[i].Range.Start.Line > edits[j].Range.Start.Line
|
|
}
|
|
return edits[i].Range.Start.Character > edits[j].Range.Start.Character
|
|
})
|
|
|
|
for _, e := range edits {
|
|
sl, sc := e.Range.Start.Line, e.Range.Start.Character
|
|
el, ec := e.Range.End.Line, e.Range.End.Character
|
|
if sl >= len(lines) {
|
|
continue
|
|
}
|
|
|
|
before := strings.Join(lines[:sl], "") + lines[sl][:min(sc, len(lines[sl]))]
|
|
var after string
|
|
if el < len(lines) {
|
|
after = lines[el][min(ec, len(lines[el])):] + strings.Join(lines[el+1:], "")
|
|
}
|
|
content := before + e.NewText + after
|
|
lines = strings.SplitAfter(content, "\n")
|
|
}
|
|
|
|
os.WriteFile(path, []byte(strings.Join(lines, "")), 0644)
|
|
totalEdits += len(edits)
|
|
fmt.Fprintf(&sb, "%s: %d edit(s)\n", path, len(edits))
|
|
}
|
|
fmt.Fprintf(&sb, "total: %d file(s), %d edit(s)\n", len(changes), totalEdits)
|
|
return sb.String()
|
|
}
|
|
|
|
func completionKind(k int) string {
|
|
kinds := map[int]string{
|
|
1: "Text", 2: "Method", 3: "Function", 4: "Constructor",
|
|
5: "Field", 6: "Variable", 7: "Class", 8: "Interface",
|
|
9: "Module", 10: "Property", 11: "Unit", 12: "Value",
|
|
13: "Enum", 14: "Keyword", 15: "Snippet", 16: "Color",
|
|
17: "File", 18: "Reference", 19: "Folder", 20: "EnumMember",
|
|
21: "Constant", 22: "Struct", 23: "Event", 24: "Operator",
|
|
25: "TypeParameter",
|
|
}
|
|
if name, ok := kinds[k]; ok {
|
|
return name
|
|
}
|
|
return "Unknown"
|
|
}
|
|
|
|
func symbolKind(k int) string {
|
|
kinds := map[int]string{
|
|
1: "File", 2: "Module", 3: "Namespace", 4: "Package",
|
|
5: "Class", 6: "Method", 7: "Property", 8: "Field",
|
|
9: "Constructor", 10: "Enum", 11: "Interface", 12: "Function",
|
|
13: "Variable", 14: "Constant", 15: "String", 16: "Number",
|
|
17: "Boolean", 18: "Array", 19: "Object", 20: "Key",
|
|
21: "Null", 22: "EnumMember", 23: "Struct", 24: "Event",
|
|
25: "Operator", 26: "TypeParameter",
|
|
}
|
|
if name, ok := kinds[k]; ok {
|
|
return name
|
|
}
|
|
return fmt.Sprintf("Kind(%d)", k)
|
|
}
|
|
|
|
func severity(s int) string {
|
|
switch s {
|
|
case 1:
|
|
return "error"
|
|
case 2:
|
|
return "warning"
|
|
case 3:
|
|
return "info"
|
|
case 4:
|
|
return "hint"
|
|
default:
|
|
return "diagnostic"
|
|
}
|
|
}
|