feat(lsp): document directives on hover and widen completion

Assisted-by: GLM 5.3
This commit is contained in:
2026-10-02 00:40:54 +02:00
parent 69dcbec8ef
commit 42bc1669d7
3 changed files with 290 additions and 15 deletions
+100 -6
View File
@@ -35,6 +35,46 @@ var textflagMacros = map[string]bool{
"NOFRAME": true, "REFLECTMETHOD": true, "TOPFRAME": true, "ABIWRAPPER": true,
}
// directiveDocs documents the directives and pseudo-operations for hover and
// completion, condensed from the language reference in docs/asm/.
var directiveDocs = map[string]string{
"TEXT": "TEXT symbol(SB), [flags,] $framesize-argsize: define a function. The frame size " +
"counts the local frame in bytes, the argument size the caller's argument area. End the " +
"body with RET: execution never falls through into the next TEXT.",
"DATA": "DATA symbol+offset(SB)/width, value: initialise one field of a data symbol. The " +
"width is 1, 2, 4 or 8 for an integer (any width for a string); bytes no DATA line " +
"names stay zero.",
"GLOBL": "GLOBL symbol(SB), [flags,] $size: declare a global symbol with its total size in " +
"bytes. Every DATA initialiser needs its GLOBL; uninitialised bytes are zero.",
"PCALIGN": "PCALIGN $n: pad the code so the next instruction lands on an n-byte boundary, " +
"a power of two at least the target's instruction alignment.",
"FUNCDATA": "FUNCDATA $index, symbol(SB): a garbage-collector annotation indexed by the " +
"funcdata.h ids; assembly normally reaches it through the GO_ARGS and " +
"NO_LOCAL_POINTERS macros.",
"PCDATA": "PCDATA $index, $value: a pc-value table annotation for the collector and " +
"traceback. The compiler writes these; hand-written values are meaningless outside " +
"the funcdata.h macros.",
"BYTE": "BYTE $imm: emit literal bytes into the instruction stream, the escape hatch for " +
"an encoding the assembler does not know.",
"WORD": "WORD $imm: emit a literal 16-bit word into the instruction stream.",
"LONG": "LONG $imm: emit a literal 32-bit word into the instruction stream.",
"QUAD": "QUAD $imm: emit a literal 64-bit word into the instruction stream.",
"FLOAT": "FLOAT $imm: emit a literal floating-point constant into the data section.",
}
// docSummary reduces a directive description to the sentence after its
// grammar, the short form a completion entry shows.
func docSummary(desc string) string {
_, rest, ok := strings.Cut(desc, ":")
if !ok {
return desc
}
if i := strings.Index(rest, "."); i >= 0 {
rest = rest[:i]
}
return strings.TrimSpace(rest)
}
// completion builds the completion list for a document.
func (s *Server) completion(p completionParams) []CompletionItem {
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
@@ -44,6 +84,15 @@ func (s *Server) completion(p completionParams) []CompletionItem {
{Label: "DATA", Kind: ciKeyword, Detail: "initialise a data symbol"},
{Label: "GLOBL", Kind: ciKeyword, Detail: "declare a global symbol"},
}
for name, desc := range directiveDocs {
if name == "TEXT" || name == "DATA" || name == "GLOBL" {
continue
}
items = append(items, CompletionItem{
Label: name, Kind: ciKeyword, Detail: docSummary(desc),
Documentation: desc,
})
}
for name := range textflagMacros {
items = append(items, CompletionItem{Label: name, Kind: ciKeyword, Detail: "textflag.h flag"})
}
@@ -85,7 +134,13 @@ func (s *Server) hover(p hoverParams) *Hover {
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
var md string
if in, ok := a.Lookup(word); ok {
// Directives first: several of their names (TEXT, PCALIGN, FUNCDATA,
// the BYTE family) also sit in the generated instruction tables with
// empty summaries, and the directive documentation is the answer a
// reader wants over them.
if doc, ok := directiveDocs[strings.ToUpper(word)]; ok {
md = doc
} else if in, ok := a.Lookup(word); ok {
md = "**" + in.Name + "**: " + in.Summary
} else if r, ok := a.Register(word); ok {
md = "**" + r.Name + "**: " + r.Class.String() + " register. " + r.Desc
@@ -316,10 +371,12 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
var actions []CodeAction
for _, diag := range p.Context.Diagnostics {
switch diag.Code {
case "missing-ret":
case lint.CodeMissingRet:
// Offer to add RET at the end of the flagged function only: the
// diagnostic's range covers the TEXT keyword, so a line match
// picks the function the diagnostic belongs to.
// picks the function the diagnostic belongs to. Both shapes of
// the rule are served: a function with no RET at all, and one
// whose tail can fall off the end.
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
if f == nil {
continue
@@ -359,7 +416,7 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
})
}
}
case "unused-label":
case lint.CodeUnusedLabel:
// Offer to remove the unused label.
label := strings.TrimPrefix(diag.Message, "label \"")
label = strings.TrimSuffix(label, "\" is defined but never referenced")
@@ -387,7 +444,7 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
}
}
}
case "missing-textflag-include":
case lint.CodeMissingTextflag:
// Offer to #include the header that defines the flag macros:
// after the last existing include, or at the top of the file
// when there is none.
@@ -408,7 +465,7 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
}}},
},
})
case "abi-argsize":
case lint.CodeABIArgSize:
// Offer to set the TEXT argument area to the size the // func
// signature implies. The diagnostic's range covers the TEXT
// keyword, so a line match picks the function it belongs to.
@@ -442,6 +499,43 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
},
})
}
case lint.CodeMissingArgSize:
// Offer to declare the argument area the // func signature
// implies, by appending it to the frame operand ($0 becomes
// $0-16). The diagnostic's range covers the TEXT keyword, so a
// line match picks the function it belongs to.
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
if f == nil {
continue
}
for _, d := range f.Decls {
t, ok := d.(*ast.Text)
if !ok || t.Keyword.Pos.Line-1 != int(diag.Range.Start.Line) {
continue
}
want, ok := lint.ExpectedArgSize(t.Doc)
if !ok || t.Args != nil || t.Frame == nil || !t.Frame.Imm.HasVal {
continue
}
// The frame operand as written in the document; the
// operand's Raw joins its tokens with a space and is not
// the source spelling.
spell := "$" + strconv.FormatInt(t.Frame.Imm.Val, 10)
rng := clientRange(text, Range{
Start: Position{Line: t.Frame.Pos.Line - 1, Character: t.Frame.Pos.Column - 1},
End: Position{Line: t.Frame.Pos.Line - 1, Character: t.Frame.Pos.Column - 1 + runeLen(spell)},
})
actions = append(actions, CodeAction{
Title: fmt.Sprintf("Declare argument area -%d", want),
Kind: "quickfix",
Edit: &WorkspaceEdit{
Changes: map[string][]TextEdit{p.TextDocument.URI: {{
Range: rng,
NewText: spell + "-" + strconv.FormatInt(want, 10),
}}},
},
})
}
}
}
return actions