mirror of
https://github.com/gogrlx/grlx-lsp.git
synced 2026-04-02 03:18:47 -07:00
LSP server for grlx recipe files (.grlx) providing: - Completion for ingredients, methods, properties, requisite types, and step ID references - Diagnostics for unknown ingredients/methods, missing required properties, unknown properties, and invalid requisite types - Hover documentation for all ingredients and methods with property tables - Full schema for all 6 grlx ingredients (cmd, file, group, pkg, service, user) with accurate properties from the grlx source
278 lines
7.1 KiB
Go
278 lines
7.1 KiB
Go
package lsp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"go.lsp.dev/jsonrpc2"
|
|
"go.lsp.dev/protocol"
|
|
|
|
"github.com/gogrlx/grlx-lsp/internal/schema"
|
|
)
|
|
|
|
func (h *Handler) handleCompletion(_ context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
|
|
ctx := context.Background()
|
|
|
|
var params protocol.CompletionParams
|
|
if err := json.Unmarshal(req.Params(), ¶ms); err != nil {
|
|
return reply(ctx, nil, err)
|
|
}
|
|
|
|
doc := h.getDocument(string(params.TextDocument.URI))
|
|
if doc == nil {
|
|
return reply(ctx, &protocol.CompletionList{}, nil)
|
|
}
|
|
|
|
line := lineAt(doc.content, int(params.Position.Line))
|
|
col := int(params.Position.Character)
|
|
if col > len(line) {
|
|
col = len(line)
|
|
}
|
|
prefix := strings.TrimSpace(line[:col])
|
|
|
|
var items []protocol.CompletionItem
|
|
|
|
switch {
|
|
case isTopLevel(doc.content, int(params.Position.Line)):
|
|
items = h.completeTopLevel(prefix)
|
|
case isInRequisites(line):
|
|
items = h.completeRequisiteTypes(prefix)
|
|
case isInRequisiteValue(doc.content, int(params.Position.Line)):
|
|
items = h.completeStepIDs(doc)
|
|
case isPropertyPosition(line):
|
|
items = h.completeProperties(doc, int(params.Position.Line))
|
|
default:
|
|
items = h.completeIngredientMethod(prefix)
|
|
}
|
|
|
|
return reply(ctx, &protocol.CompletionList{
|
|
IsIncomplete: false,
|
|
Items: items,
|
|
}, nil)
|
|
}
|
|
|
|
func (h *Handler) completeTopLevel(_ string) []protocol.CompletionItem {
|
|
var items []protocol.CompletionItem
|
|
for _, key := range schema.TopLevelKeys {
|
|
items = append(items, protocol.CompletionItem{
|
|
Label: key,
|
|
Kind: protocol.CompletionItemKindKeyword,
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
func (h *Handler) completeIngredientMethod(prefix string) []protocol.CompletionItem {
|
|
var items []protocol.CompletionItem
|
|
|
|
// If prefix contains a dot, complete methods for that ingredient
|
|
if dotIdx := strings.Index(prefix, "."); dotIdx >= 0 {
|
|
ingName := prefix[:dotIdx]
|
|
ing := h.registry.FindIngredient(ingName)
|
|
if ing != nil {
|
|
for _, m := range ing.Methods {
|
|
items = append(items, protocol.CompletionItem{
|
|
Label: ingName + "." + m.Name,
|
|
Kind: protocol.CompletionItemKindFunction,
|
|
Detail: m.Description,
|
|
Documentation: buildMethodDoc(ing, &m),
|
|
})
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
// Otherwise, complete all ingredient.method combos
|
|
for _, name := range h.registry.AllDottedNames() {
|
|
parts := strings.SplitN(name, ".", 2)
|
|
ing := h.registry.FindIngredient(parts[0])
|
|
m := h.registry.FindMethod(parts[0], parts[1])
|
|
detail := ""
|
|
if m != nil {
|
|
detail = m.Description
|
|
}
|
|
doc := ""
|
|
if ing != nil && m != nil {
|
|
doc = buildMethodDoc(ing, m)
|
|
}
|
|
items = append(items, protocol.CompletionItem{
|
|
Label: name,
|
|
Kind: protocol.CompletionItemKindFunction,
|
|
Detail: detail,
|
|
Documentation: doc,
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
func (h *Handler) completeProperties(doc *document, line int) []protocol.CompletionItem {
|
|
var items []protocol.CompletionItem
|
|
|
|
// Find which step this line belongs to
|
|
step := h.findStepForLine(doc, line)
|
|
if step == nil {
|
|
return items
|
|
}
|
|
|
|
m := h.registry.FindMethod(step.Ingredient, step.Method)
|
|
if m == nil {
|
|
return items
|
|
}
|
|
|
|
// Collect already-used properties
|
|
used := make(map[string]bool)
|
|
for _, p := range step.Properties {
|
|
used[p.Key] = true
|
|
}
|
|
|
|
for _, prop := range m.Properties {
|
|
if used[prop.Key] {
|
|
continue
|
|
}
|
|
detail := prop.Type
|
|
if prop.Required {
|
|
detail += " (required)"
|
|
}
|
|
items = append(items, protocol.CompletionItem{
|
|
Label: "- " + prop.Key + ": ",
|
|
InsertText: "- " + prop.Key + ": ",
|
|
Kind: protocol.CompletionItemKindProperty,
|
|
Detail: detail,
|
|
})
|
|
}
|
|
|
|
// Also offer requisites
|
|
if !used["requisites"] {
|
|
items = append(items, protocol.CompletionItem{
|
|
Label: "- requisites:",
|
|
Kind: protocol.CompletionItemKindKeyword,
|
|
Detail: "Step dependencies",
|
|
})
|
|
}
|
|
|
|
return items
|
|
}
|
|
|
|
func (h *Handler) completeRequisiteTypes(_ string) []protocol.CompletionItem {
|
|
var items []protocol.CompletionItem
|
|
for _, rt := range schema.AllRequisiteTypes {
|
|
items = append(items, protocol.CompletionItem{
|
|
Label: "- " + rt.Name + ": ",
|
|
Kind: protocol.CompletionItemKindEnum,
|
|
Detail: rt.Description,
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
func (h *Handler) completeStepIDs(doc *document) []protocol.CompletionItem {
|
|
var items []protocol.CompletionItem
|
|
if doc.recipe == nil {
|
|
return items
|
|
}
|
|
for _, id := range doc.recipe.StepIDs() {
|
|
items = append(items, protocol.CompletionItem{
|
|
Label: id,
|
|
Kind: protocol.CompletionItemKindReference,
|
|
})
|
|
}
|
|
return items
|
|
}
|
|
|
|
func buildMethodDoc(ing *schema.Ingredient, m *schema.Method) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(ing.Name + "." + m.Name)
|
|
if m.Description != "" {
|
|
sb.WriteString(" — " + m.Description)
|
|
}
|
|
if len(m.Properties) > 0 {
|
|
sb.WriteString("\n\nProperties:\n")
|
|
for _, p := range m.Properties {
|
|
marker := " "
|
|
if p.Required {
|
|
marker = "* "
|
|
}
|
|
sb.WriteString(marker + p.Key + " (" + p.Type + ")")
|
|
if p.Description != "" {
|
|
sb.WriteString(" — " + p.Description)
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// Heuristics for context detection
|
|
|
|
func isTopLevel(content string, line int) bool {
|
|
lines := strings.Split(content, "\n")
|
|
if line < 0 || line >= len(lines) {
|
|
return false
|
|
}
|
|
l := lines[line]
|
|
// Top-level if no leading whitespace or empty
|
|
return len(l) == 0 || (len(strings.TrimLeft(l, " \t")) == len(l))
|
|
}
|
|
|
|
func isInRequisites(line string) bool {
|
|
trimmed := strings.TrimSpace(line)
|
|
// Inside a requisites block: indented under "- requisites:"
|
|
return strings.HasPrefix(trimmed, "- require") ||
|
|
strings.HasPrefix(trimmed, "- onchanges") ||
|
|
strings.HasPrefix(trimmed, "- onfail")
|
|
}
|
|
|
|
func isInRequisiteValue(content string, line int) bool {
|
|
lines := strings.Split(content, "\n")
|
|
// Look backwards for a requisite condition key
|
|
for i := line; i >= 0 && i >= line-5; i-- {
|
|
trimmed := strings.TrimSpace(lines[i])
|
|
if strings.HasPrefix(trimmed, "- require:") ||
|
|
strings.HasPrefix(trimmed, "- require_any:") ||
|
|
strings.HasPrefix(trimmed, "- onchanges:") ||
|
|
strings.HasPrefix(trimmed, "- onchanges_any:") ||
|
|
strings.HasPrefix(trimmed, "- onfail:") ||
|
|
strings.HasPrefix(trimmed, "- onfail_any:") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isPropertyPosition(line string) bool {
|
|
trimmed := strings.TrimSpace(line)
|
|
// Property lines start with "- " and are indented
|
|
indent := len(line) - len(strings.TrimLeft(line, " "))
|
|
return indent >= 4 && (strings.HasPrefix(trimmed, "- ") || trimmed == "-" || trimmed == "")
|
|
}
|
|
|
|
func (h *Handler) findStepForLine(doc *document, line int) *struct {
|
|
Ingredient string
|
|
Method string
|
|
Properties []struct{ Key string }
|
|
} {
|
|
if doc.recipe == nil {
|
|
return nil
|
|
}
|
|
// Find the step whose method node is on or before this line
|
|
for i := len(doc.recipe.Steps) - 1; i >= 0; i-- {
|
|
s := &doc.recipe.Steps[i]
|
|
if s.MethodNode != nil && s.MethodNode.Line-1 <= line {
|
|
result := &struct {
|
|
Ingredient string
|
|
Method string
|
|
Properties []struct{ Key string }
|
|
}{
|
|
Ingredient: s.Ingredient,
|
|
Method: s.Method,
|
|
}
|
|
for _, p := range s.Properties {
|
|
result.Properties = append(result.Properties, struct{ Key string }{Key: p.Key})
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
return nil
|
|
}
|