diff --git a/lsp/extensions.go b/lsp/extensions.go new file mode 100644 index 0000000..d72c3db --- /dev/null +++ b/lsp/extensions.go @@ -0,0 +1,348 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +// The language-server side of the extended-instruction layer. The registry +// in asm carries the mnemonics the generated architecture tables do not know; +// this file lets completion and hover document those mnemonics from the +// layer's own metadata, and lets diagnostics leave the layer's verdicts to +// it. Nothing the server states about a registered mnemonic is invented: +// the summaries, forms, features, encodings and references are read from the +// metadata, and the operand spellings are measured against the layer's own +// encoder, so the editor surface can never promise a spelling the assembler +// would refuse. + +package lsp + +import ( + "fmt" + "slices" + "strings" + "sync" + + "sourcedock.dev/petrbalvin/gasm-sdk/arch" + "sourcedock.dev/petrbalvin/gasm-sdk/asm" +) + +// extFormDoc is the rendering of one registered form: everything hover and +// completion state about it. +type extFormDoc struct { + summary string // the metadata's one-line description ("" when absent) + form string // the operand shape's label, from ExtForm.String + shape string // the operand spellings the layer's encoder accepts + feature string // the architecture feature, from ExtFeature + word uint32 // the fixed encoding bits + ref string // the manual entry the encoding is transcribed from +} + +// extDocsKey addresses one mnemonic's renderings for one architecture. +type extDocsKey struct { + a arch.Arch + name string +} + +// extDocsCache memoises the per-form renderings: the probe walks the layer's +// encoder once per mnemonic and architecture, and every later completion or +// hover reuses the answer. +var extDocsCache sync.Map // map[extDocsKey][]extFormDoc + +// The probe candidates. They cover every operand kind the layer defines +// today; a kind arriving later joins through its case in extBuild, and an +// unknown kind degrades to a bare label rather than a wrong spelling. +var extProbeArrangements = []arch.ExtArrangement{ + arch.ExtArrB, arch.ExtArrH, arch.ExtArrS, arch.ExtArrD, arch.ExtArrQ, arch.ExtArrNone, +} + +var extProbeQualifiers = []arch.ExtQualifier{ + arch.ExtQualMerging, arch.ExtQualZeroing, arch.ExtQualNone, +} + +var extProbeImmediates = []int64{1, 0, 255, -1} + +// extForms returns the renderings of one mnemonic's registered forms on a, +// computing and memoising them on first use. It returns nil when the layer +// registers nothing for the mnemonic. +func extForms(a arch.Arch, mnemonic string) []extFormDoc { + key := extDocsKey{a: a, name: strings.ToUpper(mnemonic)} + if v, ok := extDocsCache.Load(key); ok { + docs, _ := v.([]extFormDoc) + return docs + } + var docs []extFormDoc + if cands, ok := asm.LookupExtension(a, mnemonic); ok { + docs = make([]extFormDoc, 0, len(cands)) + for _, in := range cands { + docs = append(docs, extFormDoc{ + summary: in.Summary, + form: in.Form.String(), + shape: extFormShape(in), + feature: string(in.Feature), + word: in.Word, + ref: in.Ref, + }) + } + } + extDocsCache.Store(key, docs) + return docs +} + +// extBuild builds one candidate operand list for the form: every vector +// position at reg with the arrangement arr, every predicate position at reg +// with the qualifier qual, every immediate at imm. +func extBuild(in arch.ExtInstr, reg int, arr arch.ExtArrangement, qual arch.ExtQualifier, imm int64) []arch.ExtOperand { + kinds := in.Form.Kinds() + ops := make([]arch.ExtOperand, len(kinds)) + for i, k := range kinds { + switch k { + case arch.ExtZReg: + ops[i] = arch.ExtVector(reg, arr) + case arch.ExtPReg: + ops[i] = arch.ExtPredicate(reg, qual) + default: // arch.ExtImm and anything the probe does not model + ops[i] = arch.ExtImmediate(imm) + } + } + return ops +} + +// extEncodes reports whether the form's encoder accepts the operand list. +func extEncodes(in arch.ExtInstr, ops []arch.ExtOperand) bool { + _, err := in.Encode(ops) + return err == nil +} + +// extAnchor finds one operand list the form encodes, to anchor the probes on; +// ok is false when no probe combination encodes, and the shape then renders +// from the operand kinds alone. +func extAnchor(in arch.ExtInstr) (base []arch.ExtOperand, arr arch.ExtArrangement, qual arch.ExtQualifier, imm int64, ok bool) { + for _, a := range extProbeArrangements { + for _, q := range extProbeQualifiers { + for _, v := range extProbeImmediates { + ops := extBuild(in, 0, a, q, v) + if extEncodes(in, ops) { + return ops, a, q, v, true + } + } + } + } + return nil, arch.ExtArrNone, arch.ExtQualNone, 0, false +} + +// extAcceptedArrangements returns the arrangements the form's vector +// positions accept, probed together: the encoder requires the vector +// positions of a form to agree, so one position alone cannot carry the probe. +func extAcceptedArrangements(in arch.ExtInstr, qual arch.ExtQualifier, imm int64) []arch.ExtArrangement { + var out []arch.ExtArrangement + for _, a := range extProbeArrangements { + if extEncodes(in, extBuild(in, 0, a, qual, imm)) { + out = append(out, a) + } + } + return out +} + +// extAcceptedQualifiers returns the qualifiers the form's predicate +// positions accept, probed together for the same reason. +func extAcceptedQualifiers(in arch.ExtInstr, arr arch.ExtArrangement, imm int64) []arch.ExtQualifier { + var out []arch.ExtQualifier + for _, q := range extProbeQualifiers { + if extEncodes(in, extBuild(in, 0, arr, q, imm)) { + out = append(out, q) + } + } + return out +} + +// extRegRange walks the register numbers one operand position accepts and +// returns a "Z0-Z31" style span when the accepted set is one contiguous run +// from its low end. ok is false when no register encodes there or the set +// is not contiguous, and the caller falls back to a bare label. +func extRegRange(in arch.ExtInstr, base []arch.ExtOperand, pos int, letter string) (span string, ok bool) { + const limit = 32 // both register files the layer defines sit inside 32 + lo, hi, n := -1, -1, 0 + for r := range limit { + ops := slices.Clone(base) + ops[pos].Reg = r + if !extEncodes(in, ops) { + continue + } + n++ + if lo < 0 { + lo = r + } + hi = r + } + if n == 0 || hi-lo+1 != n { + return "", false + } + return fmt.Sprintf("%s%d-%s%d", letter, lo, letter, hi), true +} + +// extFormShape measures the operand spellings the form accepts: the register +// spans, the arrangement suffixes and the predicate qualifiers its own +// encoder takes, rendered the way the source writes them. Where a probe +// finds nothing to anchor on, the shape degrades to the operand kinds and +// states nothing the encoder has not proven. +func extFormShape(in arch.ExtInstr) string { + kinds := in.Form.Kinds() + if len(kinds) == 0 { + return "" + } + base, arr, qual, imm, anchored := extAnchor(in) + + // The arrangement suffixes and the qualifiers, rendered in enum order. + suffix := "" + quals := []arch.ExtQualifier{qual} + if anchored { + var spelled []string + for _, a := range extAcceptedArrangements(in, qual, imm) { + if a != arch.ExtArrNone { + spelled = append(spelled, a.String()) + } + } + if len(spelled) > 0 { + suffix = strings.Join(spelled, "/") + } + quals = extAcceptedQualifiers(in, arr, imm) + } + + var parts []string + for i, k := range kinds { + switch k { + case arch.ExtZReg: + label := "Z" + if anchored { + if span, ok := extRegRange(in, base, i, "Z"); ok { + label = span + } + } + parts = append(parts, label+suffix) + case arch.ExtPReg: + label := "P" + if anchored { + if span, ok := extRegRange(in, base, i, "P"); ok { + label = span + } + } + var spelled []string + for _, q := range quals { + if q != arch.ExtQualNone { + spelled = append(spelled, q.String()) + } + } + if len(spelled) > 0 { + label += strings.Join(spelled, " or ") + } + parts = append(parts, label) + default: + parts = append(parts, "$imm") + } + } + return strings.Join(parts, ", ") +} + +// extFeatureList names the architecture features the forms belong to, in +// first-occurrence order, "" when the metadata carries none. +func extFeatureList(docs []extFormDoc) string { + var out []string + for _, d := range docs { + if d.feature != "" && !slices.Contains(out, d.feature) { + out = append(out, d.feature) + } + } + return strings.Join(out, ", ") +} + +// renderExtensionHover renders the hover documentation of a registered +// extended mnemonic: the fact that it is an extension above the toolchain, +// then one entry per registered form. A form with no summary in the +// metadata states the encoding facts alone: the shape label, the feature, +// the fixed encoding bits and the manual reference, and nothing more. +func renderExtensionHover(mnemonic string, docs []extFormDoc) string { + if len(docs) == 0 { + return "" + } + var b strings.Builder + b.WriteString("**" + mnemonic + "**: extension above the Go toolchain") + if f := extFeatureList(docs); f != "" { + b.WriteString(" (" + f + ")") + } + for _, d := range docs { + b.WriteString("\n\n- ") + // The summary leads where the metadata carries one; without it the + // form's own label stands in, and the bullet states the encoding + // facts and nothing more. + lead := d.summary + if lead == "" { + lead = d.form + } + if lead != "" { + b.WriteString(lead + ": ") + } + b.WriteString(d.shape) + var clauses []string + if d.summary != "" && d.form != "" { + clauses = append(clauses, d.form) + } + if d.feature != "" { + clauses = append(clauses, d.feature) + } + if d.word != 0 { + clauses = append(clauses, fmt.Sprintf("encoding 0x%08x", d.word)) + } + if len(clauses) > 0 { + b.WriteString(" (" + strings.Join(clauses, ", ") + ")") + } + if d.ref != "" { + b.WriteString("\n " + d.ref) + } + } + return b.String() +} + +// extensionHover returns the hover documentation of a registered extended +// mnemonic, "" when the layer registers nothing for it. +func extensionHover(a arch.Arch, word string) string { + return renderExtensionHover(strings.ToUpper(word), extForms(a, word)) +} + +// applyExtensionCompletion merges the registered extended mnemonics into the +// completion list, so they are offered exactly where the toolchain +// mnemonics are: a mnemonic the base table also knows enriches its existing +// item, a registry-only one gains its own. The detail carries the operand +// shapes the layer's encoder accepts. +func applyExtensionCompletion(items []CompletionItem, a arch.Arch) []CompletionItem { + byLabel := make(map[string]int, len(items)) + for i, it := range items { + if _, ok := byLabel[it.Label]; !ok { + byLabel[it.Label] = i + } + } + for _, name := range asm.ExtensionNames(a) { + docs := extForms(a, name) + if len(docs) == 0 { + continue + } + shapes := make([]string, 0, len(docs)) + for _, d := range docs { + shapes = append(shapes, d.shape) + } + hover := extensionHover(a, name) + if i, ok := byLabel[name]; ok { + items[i].Detail += "; extension: " + strings.Join(shapes, "; ") + if items[i].Documentation == "" { + items[i].Documentation = hover + } else { + items[i].Documentation += "\n\n" + hover + } + continue + } + byLabel[name] = len(items) + items = append(items, CompletionItem{ + Label: name, + Kind: ciFunction, + Detail: "extension: " + strings.Join(shapes, "; "), + Documentation: hover, + }) + } + return items +} diff --git a/lsp/extensions_test.go b/lsp/extensions_test.go new file mode 100644 index 0000000..cb1007a --- /dev/null +++ b/lsp/extensions_test.go @@ -0,0 +1,285 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package lsp + +import ( + "encoding/json" + "strings" + "testing" + + "sourcedock.dev/petrbalvin/gasm-sdk/arch" + "sourcedock.dev/petrbalvin/gasm-sdk/asm" +) + +// sveDoc is an arm64 kernel over the registered extended-instruction layer: +// every SVE form of ADD, a registry-only mnemonic, the signed-immediate MUL, +// and one mnemonic no layer registers, which must stay reported wherever the +// registry is silent. +const sveDoc = "#include \"textflag.h\"\n" + + "TEXT \u00b7f(SB), NOSPLIT, $0\n" + + "\tADD Z0.S, Z1.S, Z2.S\n" + + "\tADD Z1.S, P0/M, Z0.S\n" + + "\tADD $255, Z0.S\n" + + "\tSQADD Z3.S, Z4.S, Z5.S\n" + + "\tMUL $-128, Z0.B\n" + + "\tNOSUCHTHING Z0.S, Z1.S\n" + + "\tRET\n" + +// completionItems runs one completion request and indexes the answers by +// label. The base surface predates this change and carries a few duplicate +// labels (a directive or pseudo-register can also sit in the generated +// tables); callers that care about a specific label count it in the slice. +func completionItems(t *testing.T, uri, text string, id int) ([]CompletionItem, map[string]CompletionItem) { + t.Helper() + in := session(uri, text) + + frame(id, "textDocument/completion", map[string]any{ + "textDocument": map[string]any{"uri": uri}, + "position": map[string]any{"line": 2, "character": 1}, + }) + frame(nil, "exit", nil) + msgs := run(t, in) + resp := findByID(msgs, id) + if resp == nil { + t.Fatal("no completion response") + } + var items []CompletionItem + if err := json.Unmarshal(mustResult(t, resp), &items); err != nil { + t.Fatal(err) + } + out := make(map[string]CompletionItem, len(items)) + for _, it := range items { + if _, dup := out[it.Label]; !dup { + out[it.Label] = it + } + } + return items, out +} + +// countLabel counts the items carrying one label. +func countLabel(items []CompletionItem, label string) int { + n := 0 + for _, it := range items { + if it.Label == label { + n++ + } + } + return n +} + +// hoverAt runs one hover request and returns the rendered markdown. +func hoverAt(t *testing.T, uri, text string, id, line, char int) *Hover { + t.Helper() + in := session(uri, text) + + frame(id, "textDocument/hover", map[string]any{ + "textDocument": map[string]any{"uri": uri}, + "position": map[string]any{"line": line, "character": char}, + }) + frame(nil, "exit", nil) + msgs := run(t, in) + resp := findByID(msgs, id) + if resp == nil { + t.Fatal("no hover response") + } + var h *Hover + if err := json.Unmarshal(mustResult(t, resp), &h); err != nil { + t.Fatal(err) + } + return h +} + +// publishedDiagnostics runs one didOpen and returns the pushed diagnostics. +func publishedDiagnostics(t *testing.T, uri, text string) []Diagnostic { + t.Helper() + msgs := run(t, session(uri, text)+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) + return p.Diagnostics +} + +// TestCompletionExtensionMnemonicsARM64 checks that the registered extended +// mnemonics are offered where the toolchain mnemonics are, with the operand +// shapes the registry accepts, and that a mnemonic both layers know merges +// into one item instead of two. +func TestCompletionExtensionMnemonicsARM64(t *testing.T) { + items, byLabel := completionItems(t, "file:///v_arm64.s", sveDoc, 40) + + sqadd, ok := byLabel["SQADD"] + if !ok { + t.Fatal("completion missing the registry mnemonic SQADD") + } + if sqadd.Kind != ciFunction { + t.Errorf("SQADD kind = %d, want the instruction kind", sqadd.Kind) + } + for _, want := range []string{"extension: ", "Z0-Z31.B/.H/.S/.D", "$imm"} { + if !strings.Contains(sqadd.Detail, want) { + t.Errorf("SQADD detail = %q, want it to carry %q", sqadd.Detail, want) + } + } + if !strings.Contains(sqadd.Documentation, "extension above the Go toolchain") { + t.Errorf("SQADD documentation = %q, want the extension notice", sqadd.Documentation) + } + + subr, ok := byLabel["SUBR"] + if !ok { + t.Fatal("completion missing the registry mnemonic SUBR") + } + if !strings.Contains(subr.Detail, "P0-P7/M") { + t.Errorf("SUBR detail = %q, want the predicate spelling P0-P7/M", subr.Detail) + } + + // ADD sits in both layers: one item, the table entry carrying the + // extension shapes beside its own summary. + if n := countLabel(items, "ADD"); n != 1 { + t.Errorf("ADD offered %d times, want one merged item", n) + } + add := byLabel["ADD"] + if !strings.Contains(add.Detail, "64-bit") || !strings.Contains(add.Detail, "extension: ") { + t.Errorf("ADD detail = %q, want the merged table and extension text", add.Detail) + } + if !strings.Contains(add.Documentation, "extension above the Go toolchain") { + t.Errorf("ADD documentation = %q, want the extension notice beside the table summary", add.Documentation) + } + + // The toolchain surface is untouched beside the additions. + for _, want := range []string{"TEXT", "MOVD", "NOSPLIT"} { + if _, ok := byLabel[want]; !ok { + t.Errorf("completion lost the toolchain entry %q", want) + } + } + // No extended mnemonic arrives as a second item beside its merge. + for _, name := range asm.ExtensionNames(arch.ARM64) { + if n := countLabel(items, name); n != 1 { + t.Errorf("extended mnemonic %q offered %d times, want once", name, n) + } + } +} + +// TestCompletionNoExtensionMnemonicsAMD64 is the per-architecture negative +// case: the extension layer registers nothing on amd64, so no extended +// mnemonic may be invented into the list and the toolchain surface stands. +func TestCompletionNoExtensionMnemonicsAMD64(t *testing.T) { + _, byLabel := completionItems(t, "file:///v_amd64.s", sveDoc, 41) + for _, banned := range []string{"SQADD", "SUBR", "UQADD", "UQSUB", "SMULH"} { + if _, ok := byLabel[banned]; ok { + t.Errorf("completion invented %q on amd64, whose layer registers nothing", banned) + } + } + if _, ok := byLabel["MOVQ"]; !ok { + t.Error("completion lost MOVQ on amd64") + } + if add, ok := byLabel["ADD"]; ok && strings.Contains(add.Detail, "extension: ") { + t.Errorf("ADD detail = %q, want no extension text where the layer is absent", add.Detail) + } +} + +// TestHoverExtensionMnemonicARM64 checks that a registered extended mnemonic +// documents itself from the layer's metadata: the extension notice, the +// family, the operand shapes, the fixed encoding and the manual reference. +func TestHoverExtensionMnemonicARM64(t *testing.T) { + h := hoverAt(t, "file:///v_arm64.s", sveDoc, 42, 5, 3) // on SQADD + if h == nil { + t.Fatal("hover over SQADD returned nothing") + } + for _, want := range []string{ + "**SQADD**: extension above the Go toolchain (sve)", + "Add signed saturating scalable vector elements, unpredicated: Z0-Z31.B/.H/.S/.D, Z0-Z31.B/.H/.S/.D, Z0-Z31.B/.H/.S/.D", + "unpredicated vectors, sve, encoding 0x04201000", + "$imm, Z0-Z31.B/.H/.S/.D", + "DDI 0487J", + } { + if !strings.Contains(h.Contents.Value, want) { + t.Errorf("hover = %q, want it to carry %q", h.Contents.Value, want) + } + } + if strings.Contains(h.Contents.Value, ".Q") { + t.Errorf("hover = %q, states an arrangement the layer's encoder refuses", h.Contents.Value) + } +} + +// TestHoverSharedMnemonicARM64 checks the merge on a mnemonic both layers +// know: the table entry keeps its summary and the registered forms attach +// beside it. +func TestHoverSharedMnemonicARM64(t *testing.T) { + h := hoverAt(t, "file:///v_arm64.s", sveDoc, 43, 2, 3) // on ADD + if h == nil { + t.Fatal("hover over ADD returned nothing") + } + if !strings.Contains(h.Contents.Value, "**ADD**: ADD (64-bit)") { + t.Errorf("hover = %q, want the generated table summary", h.Contents.Value) + } + if !strings.Contains(h.Contents.Value, "extension above the Go toolchain") { + t.Errorf("hover = %q, want the registered forms beside the summary", h.Contents.Value) + } + if !strings.Contains(h.Contents.Value, "P0-P7/M") { + t.Errorf("hover = %q, want the predicated form's spelling", h.Contents.Value) + } +} + +// TestHoverNoExtensionMnemonicAMD64 is the per-architecture negative case: +// with no registered layer on amd64, a hover over a registry-only mnemonic +// answers null exactly as before. +func TestHoverNoExtensionMnemonicAMD64(t *testing.T) { + if h := hoverAt(t, "file:///v_amd64.s", sveDoc, 44, 5, 3); h != nil { + t.Errorf("hover over SQADD on amd64 = %q, want null", h.Contents.Value) + } +} + +// TestExtensionShapesMatchRegistry pins the measuring: the rendered shapes +// are exactly the spellings the registry's encoder accepts, checked by +// round-tripping the boundary spellings through asm.EncodeExtension. +func TestExtensionShapesMatchRegistry(t *testing.T) { + docs := extForms(arch.ARM64, "ADD") + if len(docs) != 3 { + t.Fatalf("ADD renders %d forms, want 3", len(docs)) + } + wantShapes := []string{ + "Z0-Z31.B/.H/.S/.D, Z0-Z31.B/.H/.S/.D, Z0-Z31.B/.H/.S/.D", + "Z0-Z31.B/.H/.S/.D, P0-P7/M, Z0-Z31.B/.H/.S/.D", + "$imm, Z0-Z31.B/.H/.S/.D", + } + for i, want := range wantShapes { + if docs[i].shape != want { + t.Errorf("form %d shape = %q, want %q", i, docs[i].shape, want) + } + } + // The predicate span ends where the encoder's three-bit field ends. + if _, err := asm.EncodeExtension(arch.ARM64, "ADD", + arch.ExtVector(0, arch.ExtArrS), arch.ExtPredicate(7, arch.ExtQualMerging), arch.ExtVector(1, arch.ExtArrS)); err != nil { + t.Errorf("P7/M refused: %v", err) + } + if _, err := asm.EncodeExtension(arch.ARM64, "ADD", + arch.ExtVector(0, arch.ExtArrS), arch.ExtPredicate(8, arch.ExtQualMerging), arch.ExtVector(1, arch.ExtArrS)); err == nil { + t.Error("P8/M encoded, the span overstates the registry") + } + // The arrangement suffixes stop at .D: .Q refuses while the shape omits it. + if _, err := asm.EncodeExtension(arch.ARM64, "ADD", + arch.ExtVector(0, arch.ExtArrQ), arch.ExtVector(1, arch.ExtArrQ), arch.ExtVector(2, arch.ExtArrQ)); err == nil { + t.Error(".Q encoded, the suffixes overstate the registry") + } + // And .B, which the shape spells, encodes. + if _, err := asm.EncodeExtension(arch.ARM64, "ADD", + arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(1, arch.ExtArrB), arch.ExtVector(2, arch.ExtArrB)); err != nil { + t.Errorf(".B refused: %v", err) + } +} + +// TestExtensionHoverWithoutProse checks the no-prose contract: a form whose +// metadata carries no summary states the encoding facts it has and nothing +// invented in their place. +func TestExtensionHoverWithoutProse(t *testing.T) { + docs := []extFormDoc{ + {form: "unsigned immediate", shape: "$imm, Z0-Z31.B/.H/.S/.D", feature: "sve", word: 0x2520c000}, + } + md := renderExtensionHover("TESTMNEM", docs) + for _, want := range []string{ + "**TESTMNEM**: extension above the Go toolchain (sve)", + "- unsigned immediate: $imm, Z0-Z31.B/.H/.S/.D (sve, encoding 0x2520c000)", + } { + if !strings.Contains(md, want) { + t.Errorf("hover = %q, want it to carry %q", md, want) + } + } +} diff --git a/lsp/handlers.go b/lsp/handlers.go index 9c5fcda..9017f88 100644 --- a/lsp/handlers.go +++ b/lsp/handlers.go @@ -114,6 +114,9 @@ func (s *Server) completion(p completionParams) []CompletionItem { } items = append(items, CompletionItem{Label: r.Name, Kind: kind, Detail: r.Desc}) } + // The extended-instruction layer, beside the toolchain entries: the + // registered mnemonics with the operand shapes the registry accepts. + items = applyExtensionCompletion(items, a.Arch) // Local labels defined in the document. if f, _ := parser.Parse("", s.docs[p.TextDocument.URI]); f != nil { for _, name := range labelNames(f) { @@ -134,6 +137,7 @@ func (s *Server) hover(p hoverParams) *Hover { a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI))) var md string + ext := extensionHover(a.Arch, word) // 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 @@ -142,6 +146,13 @@ func (s *Server) hover(p hoverParams) *Hover { md = doc } else if in, ok := a.Lookup(word); ok { md = "**" + in.Name + "**: " + in.Summary + // A mnemonic the extension layer registers documents its registered + // forms beside the table entry. + if ext != "" { + md += "\n\n" + ext + } + } else if ext != "" { + md = ext } else if r, ok := a.Register(word); ok { md = "**" + r.Name + "**: " + r.Class.String() + " register. " + r.Desc } else if desc, ok := arch.PseudoRegDesc(word); ok {