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
129 lines
3.0 KiB
Go
129 lines
3.0 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) handleHover(_ context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
|
|
ctx := context.Background()
|
|
|
|
var params protocol.HoverParams
|
|
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, nil, nil)
|
|
}
|
|
|
|
line := lineAt(doc.content, int(params.Position.Line))
|
|
word := wordAtPosition(line, int(params.Position.Character))
|
|
|
|
if word == "" {
|
|
return reply(ctx, nil, nil)
|
|
}
|
|
|
|
// Check if it's an ingredient.method reference
|
|
if strings.Contains(word, ".") {
|
|
parts := strings.SplitN(word, ".", 2)
|
|
if len(parts) == 2 {
|
|
m := h.registry.FindMethod(parts[0], parts[1])
|
|
ing := h.registry.FindIngredient(parts[0])
|
|
if m != nil && ing != nil {
|
|
return reply(ctx, &protocol.Hover{
|
|
Contents: protocol.MarkupContent{
|
|
Kind: protocol.Markdown,
|
|
Value: buildMethodMarkdown(ing.Name, m),
|
|
},
|
|
}, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if it's just an ingredient name
|
|
ing := h.registry.FindIngredient(word)
|
|
if ing != nil {
|
|
var methods []string
|
|
for _, m := range ing.Methods {
|
|
methods = append(methods, ing.Name+"."+m.Name)
|
|
}
|
|
return reply(ctx, &protocol.Hover{
|
|
Contents: protocol.MarkupContent{
|
|
Kind: protocol.Markdown,
|
|
Value: "**" + ing.Name + "** — " + ing.Description + "\n\nMethods: `" + strings.Join(methods, "`, `") + "`",
|
|
},
|
|
}, nil)
|
|
}
|
|
|
|
// Check if it's a requisite type
|
|
for _, rt := range h.registry.RequisiteTypes {
|
|
if rt.Name == word {
|
|
return reply(ctx, &protocol.Hover{
|
|
Contents: protocol.MarkupContent{
|
|
Kind: protocol.Markdown,
|
|
Value: "**" + rt.Name + "** — " + rt.Description,
|
|
},
|
|
}, nil)
|
|
}
|
|
}
|
|
|
|
return reply(ctx, nil, nil)
|
|
}
|
|
|
|
func buildMethodMarkdown(ingredient string, m *schema.Method) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("### " + ingredient + "." + m.Name + "\n\n")
|
|
if m.Description != "" {
|
|
sb.WriteString(m.Description + "\n\n")
|
|
}
|
|
if len(m.Properties) > 0 {
|
|
sb.WriteString("| Property | Type | Required | Description |\n")
|
|
sb.WriteString("|----------|------|----------|-------------|\n")
|
|
for _, p := range m.Properties {
|
|
req := ""
|
|
if p.Required {
|
|
req = "yes"
|
|
}
|
|
desc := p.Description
|
|
if desc == "" {
|
|
desc = "—"
|
|
}
|
|
sb.WriteString("| `" + p.Key + "` | " + p.Type + " | " + req + " | " + desc + " |\n")
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func wordAtPosition(line string, col int) string {
|
|
if col > len(line) {
|
|
col = len(line)
|
|
}
|
|
|
|
start := col
|
|
for start > 0 && isWordChar(line[start-1]) {
|
|
start--
|
|
}
|
|
|
|
end := col
|
|
for end < len(line) && isWordChar(line[end]) {
|
|
end++
|
|
}
|
|
|
|
return line[start:end]
|
|
}
|
|
|
|
func isWordChar(b byte) bool {
|
|
return (b >= 'a' && b <= 'z') ||
|
|
(b >= 'A' && b <= 'Z') ||
|
|
(b >= '0' && b <= '9') ||
|
|
b == '_' || b == '.' || b == '-'
|
|
}
|