feat(lsp): document directives on hover and widen completion
Assisted-by: GLM 5.3
This commit is contained in:
+25
-9
@@ -1,4 +1,4 @@
|
|||||||
.TH GASM-LSP 1 "2026-09-19" "gasm" "User Commands"
|
.TH GASM-LSP 1 "2026-10-01" "gasm" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
gasm-lsp \- run the Plan 9 assembly language server
|
gasm-lsp \- run the Plan 9 assembly language server
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -11,14 +11,30 @@ associate it with
|
|||||||
files; the target architecture is inferred from the file suffix
|
files; the target architecture is inferred from the file suffix
|
||||||
(_amd64.s, _arm64.s, _riscv64.s, _loong64.s).
|
(_amd64.s, _arm64.s, _riscv64.s, _loong64.s).
|
||||||
.PP
|
.PP
|
||||||
Provides completion, hover, document symbols, push and pull
|
Provides completion, hover, document symbols, push and pull diagnostics,
|
||||||
diagnostics, semantic-token highlighting, go-to-definition, find
|
semantic-token highlighting, go-to-definition, find references, rename,
|
||||||
references, rename, formatting, inlay hints, code actions, signature
|
formatting, inlay hints, code actions, signature help, document
|
||||||
help, document highlights, workspace symbol search, include document
|
highlights, workspace symbol search, include document links and folding
|
||||||
links and folding ranges; definition, references and rename work across
|
ranges. Completion offers the architecture's registers and
|
||||||
every open document. Syntax highlighting is delivered as LSP semantic
|
instructions, the TEXT, DATA and GLOBL directives, the annotation and
|
||||||
tokens, so no editor-specific grammar is required.
|
padding pseudo-ops (PCALIGN, FUNCDATA, PCDATA, the BYTE family) and the
|
||||||
|
textflag constants; hover documents mnemonics, registers,
|
||||||
|
pseudo-registers and the directives. Syntax highlighting is delivered
|
||||||
|
as LSP semantic tokens, so no editor-specific grammar is required.
|
||||||
|
.PP
|
||||||
|
Definition, references and rename work across every open document and
|
||||||
|
the wider workspace on disk: the server indexes the .s files under the
|
||||||
|
workspace root that the editor has never opened, an open buffer always
|
||||||
|
shadows its disk copy, and watched-file events together with a per-query
|
||||||
|
freshness check keep the index current.
|
||||||
|
.PP
|
||||||
|
The quick fixes add the missing textflag.h include, set the TEXT
|
||||||
|
argument area to the size the
|
||||||
|
.B //\ func
|
||||||
|
signature implies, declare that area when it is omitted entirely, add a
|
||||||
|
missing RET, and remove an unused label.
|
||||||
.SH EXIT STATUS
|
.SH EXIT STATUS
|
||||||
Runs until the client closes the session; exits 0 on a clean shutdown.
|
Runs until the client closes the session; exits 0 on a clean shutdown.
|
||||||
.SH SEE ALSO
|
.SH SEE ALSO
|
||||||
.BR gasm (1)
|
.BR gasm (1),
|
||||||
|
.BR gasm\-lint (1)
|
||||||
|
|||||||
+100
-6
@@ -35,6 +35,46 @@ var textflagMacros = map[string]bool{
|
|||||||
"NOFRAME": true, "REFLECTMETHOD": true, "TOPFRAME": true, "ABIWRAPPER": true,
|
"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.
|
// completion builds the completion list for a document.
|
||||||
func (s *Server) completion(p completionParams) []CompletionItem {
|
func (s *Server) completion(p completionParams) []CompletionItem {
|
||||||
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
|
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: "DATA", Kind: ciKeyword, Detail: "initialise a data symbol"},
|
||||||
{Label: "GLOBL", Kind: ciKeyword, Detail: "declare a global 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 {
|
for name := range textflagMacros {
|
||||||
items = append(items, CompletionItem{Label: name, Kind: ciKeyword, Detail: "textflag.h flag"})
|
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)))
|
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
|
||||||
|
|
||||||
var md string
|
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
|
md = "**" + in.Name + "**: " + in.Summary
|
||||||
} else if r, ok := a.Register(word); ok {
|
} else if r, ok := a.Register(word); ok {
|
||||||
md = "**" + r.Name + "**: " + r.Class.String() + " register. " + r.Desc
|
md = "**" + r.Name + "**: " + r.Class.String() + " register. " + r.Desc
|
||||||
@@ -316,10 +371,12 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
|
|||||||
var actions []CodeAction
|
var actions []CodeAction
|
||||||
for _, diag := range p.Context.Diagnostics {
|
for _, diag := range p.Context.Diagnostics {
|
||||||
switch diag.Code {
|
switch diag.Code {
|
||||||
case "missing-ret":
|
case lint.CodeMissingRet:
|
||||||
// Offer to add RET at the end of the flagged function only: the
|
// Offer to add RET at the end of the flagged function only: the
|
||||||
// diagnostic's range covers the TEXT keyword, so a line match
|
// 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)
|
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
|
||||||
if f == nil {
|
if f == nil {
|
||||||
continue
|
continue
|
||||||
@@ -359,7 +416,7 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "unused-label":
|
case lint.CodeUnusedLabel:
|
||||||
// Offer to remove the unused label.
|
// Offer to remove the unused label.
|
||||||
label := strings.TrimPrefix(diag.Message, "label \"")
|
label := strings.TrimPrefix(diag.Message, "label \"")
|
||||||
label = strings.TrimSuffix(label, "\" is defined but never referenced")
|
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:
|
// Offer to #include the header that defines the flag macros:
|
||||||
// after the last existing include, or at the top of the file
|
// after the last existing include, or at the top of the file
|
||||||
// when there is none.
|
// 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
|
// Offer to set the TEXT argument area to the size the // func
|
||||||
// signature implies. The diagnostic's range covers the TEXT
|
// signature implies. The diagnostic's range covers the TEXT
|
||||||
// keyword, so a line match picks the function it belongs to.
|
// 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
|
return actions
|
||||||
|
|||||||
@@ -806,6 +806,171 @@ func TestSemanticTokensPipeOperator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCodeActionDeclaresArgSize pins the missing-argsize quick fix: the
|
||||||
|
// frame operand $0 grows to $0-16, the area the // func signature implies.
|
||||||
|
func TestCodeActionDeclaresArgSize(t *testing.T) {
|
||||||
|
doc := "// func f(a int) int\n" +
|
||||||
|
"TEXT \u00b7f(SB), $0\n" +
|
||||||
|
"\tMOVQ a+0(FP), AX\n" +
|
||||||
|
"\tMOVQ AX, ret+8(FP)\n" +
|
||||||
|
"\tRET\n"
|
||||||
|
in := session("file:///f_amd64.s", doc) +
|
||||||
|
frame(34, "textDocument/codeAction", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
"range": map[string]any{"start": map[string]any{"line": 1, "character": 0}, "end": map[string]any{"line": 1, "character": 4}},
|
||||||
|
"context": map[string]any{"diagnostics": []map[string]any{
|
||||||
|
{"code": "missing-argsize", "range": map[string]any{"start": map[string]any{"line": 1, "character": 0}, "end": map[string]any{"line": 1, "character": 4}}},
|
||||||
|
}},
|
||||||
|
}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
resp := findByID(msgs, 34)
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("no codeAction response")
|
||||||
|
}
|
||||||
|
var actions []CodeAction
|
||||||
|
if err := json.Unmarshal(mustResult(t, resp), &actions); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(actions) != 1 {
|
||||||
|
t.Fatalf("actions = %d, want 1", len(actions))
|
||||||
|
}
|
||||||
|
if actions[0].Title != "Declare argument area -16" {
|
||||||
|
t.Errorf("action title = %q, want Declare argument area -16", actions[0].Title)
|
||||||
|
}
|
||||||
|
edits := actions[0].Edit.Changes["file:///f_amd64.s"]
|
||||||
|
if len(edits) != 1 || edits[0].NewText != "$0-16" {
|
||||||
|
t.Fatalf("edit = %+v, want the frame operand rewritten to $0-16", edits)
|
||||||
|
}
|
||||||
|
if got := applyEdits(doc, edits); !strings.Contains(got, "TEXT \u00b7f(SB), $0-16") {
|
||||||
|
t.Errorf("edited document = %q, want $0-16 on the TEXT line", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewLintRulesPublished checks that the new rule families reach the
|
||||||
|
// editor through publishDiagnostics: the DATA structure errors and the
|
||||||
|
// addressing edges.
|
||||||
|
func TestNewLintRulesPublished(t *testing.T) {
|
||||||
|
doc := "#include \"textflag.h\"\n" +
|
||||||
|
"DATA ·tab+0(SB)/3, $7\n" +
|
||||||
|
"GLOBL ·tab(SB), RODATA, $8\n" +
|
||||||
|
"TEXT ·f(SB), NOSPLIT, $0-8\n" +
|
||||||
|
"\tMOVQ 0(FP), AX\n" +
|
||||||
|
"\tRET\n"
|
||||||
|
msgs := run(t, session("file:///f_amd64.s", doc)+frame(nil, "exit", nil))
|
||||||
|
pub := findMethod(msgs, "textDocument/publishDiagnostics")
|
||||||
|
if pub == nil {
|
||||||
|
t.Fatal("no publishDiagnostics notification")
|
||||||
|
}
|
||||||
|
var p publishDiagnosticsParams
|
||||||
|
json.Unmarshal(pub.Params, &p)
|
||||||
|
found := map[string]bool{}
|
||||||
|
for _, d := range p.Diagnostics {
|
||||||
|
found[d.Code] = true
|
||||||
|
}
|
||||||
|
for _, want := range []string{"data-width", "unnamed-fp-reference"} {
|
||||||
|
if !found[want] {
|
||||||
|
t.Errorf("want a %s diagnostic, got %+v", want, p.Diagnostics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCompletionDirectives checks the directive completions: the annotation
|
||||||
|
// and padding pseudo-ops appear beside TEXT, DATA and GLOBL.
|
||||||
|
func TestCompletionDirectives(t *testing.T) {
|
||||||
|
in := session("file:///f_amd64.s", cleanDoc) +
|
||||||
|
frame(35, "textDocument/completion", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
"position": map[string]any{"line": 2, "character": 1},
|
||||||
|
}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
resp := findByID(msgs, 35)
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("no completion response")
|
||||||
|
}
|
||||||
|
var items []CompletionItem
|
||||||
|
if err := json.Unmarshal(mustResult(t, resp), &items); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
labels := map[string]bool{}
|
||||||
|
for _, it := range items {
|
||||||
|
labels[it.Label] = true
|
||||||
|
}
|
||||||
|
for _, want := range []string{"PCALIGN", "FUNCDATA", "PCDATA", "BYTE", "QUAD"} {
|
||||||
|
if !labels[want] {
|
||||||
|
t.Errorf("completion missing %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHoverDirectives checks hover over the directives the language defines.
|
||||||
|
func TestHoverDirectives(t *testing.T) {
|
||||||
|
doc := "#include \"textflag.h\"\n" +
|
||||||
|
"TEXT ·f(SB), NOSPLIT, $8\n" +
|
||||||
|
"\tPCALIGN $32\n" +
|
||||||
|
"\tMOVQ $1, x-8(SP)\n" +
|
||||||
|
"\tRET\n"
|
||||||
|
in := session("file:///f_amd64.s", doc) +
|
||||||
|
frame(36, "textDocument/hover", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
"position": map[string]any{"line": 2, "character": 2}, // on PCALIGN
|
||||||
|
}) +
|
||||||
|
frame(37, "textDocument/hover", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
"position": map[string]any{"line": 1, "character": 2}, // on TEXT
|
||||||
|
}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
for id, want := range map[int]string{36: "boundary", 37: "framesize-argsize"} {
|
||||||
|
resp := findByID(msgs, id)
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatalf("no hover response %d", id)
|
||||||
|
}
|
||||||
|
var h Hover
|
||||||
|
if err := json.Unmarshal(mustResult(t, resp), &h); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(h.Contents.Value, want) {
|
||||||
|
t.Errorf("hover %d = %q, want it to mention %q", id, h.Contents.Value, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCodeActionFallthroughAddsRet pins the quick fix for the second
|
||||||
|
// missing-ret shape: a function that has a RET but can fall off its end
|
||||||
|
// gets the RET appended after its last statement.
|
||||||
|
func TestCodeActionFallthroughAddsRet(t *testing.T) {
|
||||||
|
doc := "#include \"textflag.h\"\n" +
|
||||||
|
"TEXT \u00b7f(SB), NOSPLIT, $0\n" +
|
||||||
|
"\tCMPQ AX, $0\n" +
|
||||||
|
"\tJNE done\n" +
|
||||||
|
"\tRET\n" +
|
||||||
|
"done:\n" +
|
||||||
|
"\tMOVQ $2, BX\n"
|
||||||
|
in := session("file:///f_amd64.s", doc) +
|
||||||
|
frame(38, "textDocument/codeAction", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
"range": map[string]any{"start": map[string]any{"line": 1, "character": 0}, "end": map[string]any{"line": 6, "character": 0}},
|
||||||
|
"context": map[string]any{"diagnostics": []map[string]any{
|
||||||
|
{"code": "missing-ret", "range": map[string]any{"start": map[string]any{"line": 1, "character": 0}, "end": map[string]any{"line": 1, "character": 4}}},
|
||||||
|
}},
|
||||||
|
}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
resp := findByID(msgs, 38)
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("no codeAction response")
|
||||||
|
}
|
||||||
|
var actions []CodeAction
|
||||||
|
if err := json.Unmarshal(mustResult(t, resp), &actions); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(actions) != 1 {
|
||||||
|
t.Fatalf("actions = %d, want 1", len(actions))
|
||||||
|
}
|
||||||
|
newText := actions[0].Edit.Changes["file:///f_amd64.s"][0].NewText
|
||||||
|
if !strings.HasSuffix(newText, "MOVQ $2, BX\n\tRET\n") {
|
||||||
|
t.Errorf("edit does not append RET after the last statement:\n%s", newText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mustResult re-marshals a response result into raw JSON for typed decoding.
|
// mustResult re-marshals a response result into raw JSON for typed decoding.
|
||||||
func mustResult(t *testing.T, m *rpcMessage) []byte {
|
func mustResult(t *testing.T, m *rpcMessage) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
Reference in New Issue
Block a user