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
+165
View File
@@ -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.
func mustResult(t *testing.T, m *rpcMessage) []byte {
t.Helper()