feat(lsp): document directives on hover and widen completion
Assisted-by: GLM 5.3
This commit is contained in:
+100
-6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user