Files
grlx-lsp/internal/lsp/diagnostics.go
Tai Groot d5a9585c58 feat: grlx LSP server in Go
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
2026-03-06 09:14:10 +00:00

184 lines
4.4 KiB
Go

package lsp
import (
"context"
"go.lsp.dev/protocol"
"gopkg.in/yaml.v3"
"github.com/gogrlx/grlx-lsp/internal/recipe"
"github.com/gogrlx/grlx-lsp/internal/schema"
)
func (h *Handler) publishDiagnostics(ctx context.Context, uri string) {
doc := h.getDocument(uri)
if doc == nil || h.conn == nil {
return
}
diags := h.diagnose(doc)
_ = h.conn.Notify(ctx, "textDocument/publishDiagnostics", protocol.PublishDiagnosticsParams{
URI: protocol.DocumentURI(uri),
Diagnostics: diags,
})
}
func (h *Handler) diagnose(doc *document) []protocol.Diagnostic {
var diags []protocol.Diagnostic
if doc.recipe == nil {
return diags
}
// Report parse errors
for _, e := range doc.recipe.Errors {
diags = append(diags, protocol.Diagnostic{
Range: pointRange(e.Line-1, e.Col-1),
Severity: protocol.DiagnosticSeverityError,
Source: "grlx-lsp",
Message: e.Message,
})
}
stepIDs := make(map[string]bool)
for _, s := range doc.recipe.Steps {
stepIDs[s.ID] = true
}
for _, s := range doc.recipe.Steps {
if s.Ingredient == "" {
continue
}
ing := h.registry.FindIngredient(s.Ingredient)
if ing == nil {
diags = append(diags, protocol.Diagnostic{
Range: yamlNodeRange(s.MethodNode),
Severity: protocol.DiagnosticSeverityError,
Source: "grlx-lsp",
Message: "unknown ingredient: " + s.Ingredient,
})
continue
}
m := h.registry.FindMethod(s.Ingredient, s.Method)
if m == nil {
diags = append(diags, protocol.Diagnostic{
Range: yamlNodeRange(s.MethodNode),
Severity: protocol.DiagnosticSeverityError,
Source: "grlx-lsp",
Message: "unknown method: " + s.Ingredient + "." + s.Method,
})
continue
}
diags = append(diags, checkRequired(s, m)...)
diags = append(diags, checkUnknown(s, m)...)
// Validate requisite types and references
for _, req := range s.Requisites {
if !isValidRequisiteType(req.Condition) {
diags = append(diags, protocol.Diagnostic{
Range: yamlNodeRange(req.Node),
Severity: protocol.DiagnosticSeverityError,
Source: "grlx-lsp",
Message: "unknown requisite type: " + req.Condition,
})
}
for _, ref := range req.StepIDs {
if !stepIDs[ref] {
diags = append(diags, protocol.Diagnostic{
Range: yamlNodeRange(req.Node),
Severity: protocol.DiagnosticSeverityWarning,
Source: "grlx-lsp",
Message: "reference to unknown step: " + ref + " (may be defined in an included recipe)",
})
}
}
}
}
return diags
}
func checkRequired(s recipe.Step, m *schema.Method) []protocol.Diagnostic {
var diags []protocol.Diagnostic
propKeys := make(map[string]bool)
for _, p := range s.Properties {
propKeys[p.Key] = true
}
for _, prop := range m.Properties {
if prop.Required && !propKeys[prop.Key] {
diags = append(diags, protocol.Diagnostic{
Range: yamlNodeRange(s.MethodNode),
Severity: protocol.DiagnosticSeverityWarning,
Source: "grlx-lsp",
Message: "missing required property: " + prop.Key,
})
}
}
return diags
}
func checkUnknown(s recipe.Step, m *schema.Method) []protocol.Diagnostic {
var diags []protocol.Diagnostic
validProps := make(map[string]bool)
for _, prop := range m.Properties {
validProps[prop.Key] = true
}
validProps["requisites"] = true
for _, p := range s.Properties {
if !validProps[p.Key] {
diags = append(diags, protocol.Diagnostic{
Range: yamlNodeRange(p.KeyNode),
Severity: protocol.DiagnosticSeverityWarning,
Source: "grlx-lsp",
Message: "unknown property: " + p.Key + " for " + s.Ingredient + "." + s.Method,
})
}
}
return diags
}
func isValidRequisiteType(name string) bool {
for _, rt := range schema.AllRequisiteTypes {
if rt.Name == name {
return true
}
}
return false
}
func pointRange(line, col int) protocol.Range {
if line < 0 {
line = 0
}
if col < 0 {
col = 0
}
return protocol.Range{
Start: protocol.Position{Line: uint32(line), Character: uint32(col)},
End: protocol.Position{Line: uint32(line), Character: uint32(col + 1)},
}
}
func yamlNodeRange(node *yaml.Node) protocol.Range {
if node == nil {
return pointRange(0, 0)
}
line := node.Line - 1
col := node.Column - 1
endCol := col + len(node.Value)
if line < 0 {
line = 0
}
if col < 0 {
col = 0
}
return protocol.Range{
Start: protocol.Position{Line: uint32(line), Character: uint32(col)},
End: protocol.Position{Line: uint32(line), Character: uint32(endCol)},
}
}