2026-07-06 09:49:50 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package lsp
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-19 23:49:19 +02:00
|
|
|
"cmp"
|
2026-08-20 23:27:48 +02:00
|
|
|
"fmt"
|
2026-08-30 21:30:22 +02:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"regexp"
|
|
|
|
|
"runtime"
|
2026-09-19 23:49:19 +02:00
|
|
|
"slices"
|
2026-09-23 20:23:35 +02:00
|
|
|
"strconv"
|
2026-07-06 09:49:50 +02:00
|
|
|
"strings"
|
|
|
|
|
"unicode"
|
2026-09-19 23:49:19 +02:00
|
|
|
"unicode/utf16"
|
|
|
|
|
"unicode/utf8"
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
2026-08-21 00:20:37 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/format"
|
2026-07-06 09:49:50 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
|
2026-08-30 21:30:22 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
|
2026-07-06 09:49:50 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-14 18:22:00 +02:00
|
|
|
// textflagMacros are the flag names defined by runtime/textflag.h; they are
|
|
|
|
|
// highlighted as macros and offered as completions after a TEXT/GLOBL
|
|
|
|
|
// directive.
|
2026-07-06 09:49:50 +02:00
|
|
|
var textflagMacros = map[string]bool{
|
2026-09-14 18:22:00 +02:00
|
|
|
"NOPROF": true, "DUPOK": true, "NOSPLIT": true, "RODATA": true,
|
|
|
|
|
"NOPTR": true, "WRAPPER": true, "NEEDCTXT": true, "TLSBSS": true,
|
|
|
|
|
"NOFRAME": true, "REFLECTMETHOD": true, "TOPFRAME": true, "ABIWRAPPER": true,
|
2026-07-06 09:49:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// completion builds the completion list for a document.
|
|
|
|
|
func (s *Server) completion(p completionParams) []CompletionItem {
|
|
|
|
|
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
|
|
|
|
|
tab := a
|
|
|
|
|
items := []CompletionItem{
|
|
|
|
|
{Label: "TEXT", Kind: ciKeyword, Detail: "define a function"},
|
|
|
|
|
{Label: "DATA", Kind: ciKeyword, Detail: "initialise a data symbol"},
|
|
|
|
|
{Label: "GLOBL", Kind: ciKeyword, Detail: "declare a global symbol"},
|
|
|
|
|
}
|
|
|
|
|
for name := range textflagMacros {
|
|
|
|
|
items = append(items, CompletionItem{Label: name, Kind: ciKeyword, Detail: "textflag.h flag"})
|
|
|
|
|
}
|
|
|
|
|
for name, desc := range map[string]string{
|
|
|
|
|
"FP": "frame pointer (arguments/results)", "SP": "stack pointer",
|
|
|
|
|
"SB": "static base (globals)", "PC": "program counter",
|
|
|
|
|
} {
|
|
|
|
|
items = append(items, CompletionItem{Label: name, Kind: ciConstant, Detail: desc})
|
|
|
|
|
}
|
|
|
|
|
for _, in := range tab.Instructions() {
|
|
|
|
|
items = append(items, CompletionItem{
|
|
|
|
|
Label: in.Name, Kind: ciFunction, Detail: in.Summary, Documentation: in.Summary,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
for _, r := range tab.Registers() {
|
|
|
|
|
kind := ciVariable
|
|
|
|
|
if r.Class == arch.Vector || r.Class == arch.Mask || r.Class == arch.Float || r.Class == arch.VecARM {
|
|
|
|
|
kind = ciClass
|
|
|
|
|
}
|
|
|
|
|
items = append(items, CompletionItem{Label: r.Name, Kind: kind, Detail: r.Desc})
|
|
|
|
|
}
|
|
|
|
|
// Local labels defined in the document.
|
|
|
|
|
if f, _ := parser.Parse("", s.docs[p.TextDocument.URI]); f != nil {
|
|
|
|
|
for _, name := range labelNames(f) {
|
|
|
|
|
items = append(items, CompletionItem{Label: name, Kind: ciModule, Detail: "local label"})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 23:49:19 +02:00
|
|
|
slices.SortFunc(items, func(a, b CompletionItem) int { return cmp.Compare(a.Label, b.Label) })
|
2026-07-06 09:49:50 +02:00
|
|
|
return items
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hover returns documentation for the symbol under the cursor.
|
|
|
|
|
func (s *Server) hover(p hoverParams) *Hover {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
word, rng := wordAt(text, p.Position)
|
|
|
|
|
if word == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
|
|
|
|
|
|
|
|
|
|
var md string
|
|
|
|
|
if in, ok := a.Lookup(word); ok {
|
2026-09-14 18:22:00 +02:00
|
|
|
md = "**" + in.Name + "**: " + in.Summary
|
2026-07-06 09:49:50 +02:00
|
|
|
} else if r, ok := a.Register(word); ok {
|
2026-09-14 18:22:00 +02:00
|
|
|
md = "**" + r.Name + "**: " + r.Class.String() + " register. " + r.Desc
|
2026-07-06 09:49:50 +02:00
|
|
|
} else if desc, ok := arch.PseudoRegDesc(word); ok {
|
2026-09-14 18:22:00 +02:00
|
|
|
md = "**" + strings.ToUpper(word) + "**: pseudo-register. " + desc
|
2026-07-06 09:49:50 +02:00
|
|
|
} else if textflagMacros[strings.ToUpper(word)] {
|
2026-09-14 18:22:00 +02:00
|
|
|
md = "**" + strings.ToUpper(word) + "**: textflag.h flag"
|
2026-07-06 09:49:50 +02:00
|
|
|
} else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return &Hover{
|
|
|
|
|
Contents: markupContent{Kind: "markdown", Value: md},
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, rng),
|
2026-07-06 09:49:50 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:50:55 +02:00
|
|
|
// openAST is one open document with its parsed file.
|
|
|
|
|
type openAST struct {
|
|
|
|
|
uri string
|
|
|
|
|
file *ast.File
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// openASTs parses every open document, in URI order for deterministic
|
|
|
|
|
// results. Parsing is tolerant: a buffer with syntax errors still
|
|
|
|
|
// contributes its usable declarations to the workspace scans.
|
|
|
|
|
func (s *Server) openASTs() []openAST {
|
|
|
|
|
uris := make([]string, 0, len(s.docs))
|
|
|
|
|
for uri := range s.docs {
|
|
|
|
|
uris = append(uris, uri)
|
|
|
|
|
}
|
2026-09-19 23:49:19 +02:00
|
|
|
slices.Sort(uris)
|
2026-09-14 18:50:55 +02:00
|
|
|
out := make([]openAST, 0, len(uris))
|
|
|
|
|
for _, uri := range uris {
|
|
|
|
|
if f, _ := parser.Parse(uriPath(uri), s.docs[uri]); f != nil {
|
|
|
|
|
out = append(out, openAST{uri: uri, file: f})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// definition returns the location of the named label or function: a local
|
|
|
|
|
// label in the current document wins, then the TEXT functions of every open
|
2026-09-23 20:21:58 +02:00
|
|
|
// document and indexed workspace file are searched, so a `CALL ·helper(SB)`
|
|
|
|
|
// jumps to its definition even when the defining file was never opened.
|
2026-08-05 11:08:00 +02:00
|
|
|
func (s *Server) definition(p definitionParams) []Location {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
word, _ := wordAt(text, p.Position)
|
|
|
|
|
if word == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-09-14 18:50:55 +02:00
|
|
|
name := strings.TrimPrefix(word, "\u00B7")
|
2026-08-05 11:08:00 +02:00
|
|
|
|
2026-09-14 18:50:55 +02:00
|
|
|
// The local label definition.
|
|
|
|
|
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
|
|
|
|
|
if f != nil {
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
if t, ok := d.(*ast.Text); ok {
|
|
|
|
|
for _, stmt := range t.Body {
|
|
|
|
|
if lbl, ok := stmt.(*ast.Label); ok {
|
|
|
|
|
if lbl.Name.Text == name || lbl.Name.Text == word {
|
|
|
|
|
return []Location{{
|
2026-09-19 23:49:19 +02:00
|
|
|
URI: p.TextDocument.URI,
|
|
|
|
|
Range: clientRange(text, tokenRange(lbl.Name)),
|
2026-09-14 18:50:55 +02:00
|
|
|
}}
|
|
|
|
|
}
|
2026-08-05 11:08:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-14 18:50:55 +02:00
|
|
|
|
2026-09-23 20:21:58 +02:00
|
|
|
// Function definitions across the workspace.
|
|
|
|
|
for _, u := range s.scanUnits() {
|
|
|
|
|
for _, d := range u.file.Decls {
|
2026-09-14 18:50:55 +02:00
|
|
|
t, ok := d.(*ast.Text)
|
2026-09-19 23:49:19 +02:00
|
|
|
if !ok || !realSymbol(t.Name) {
|
2026-09-14 18:50:55 +02:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if t.Name.Name == name {
|
2026-09-23 20:21:58 +02:00
|
|
|
return []Location{{URI: u.uri, Range: clientRange(u.text, symRange(t.Name))}}
|
2026-09-14 18:50:55 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-05 11:08:00 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:50:55 +02:00
|
|
|
// references returns all locations where the symbol under the cursor appears
|
2026-09-23 20:21:58 +02:00
|
|
|
// across every open document and indexed workspace file. The current
|
|
|
|
|
// document matches labels and any operand name, as before; every other
|
|
|
|
|
// source only matches SB-qualified operand references and the definition
|
|
|
|
|
// itself, because a bare name is a function-local label whose repeats in
|
|
|
|
|
// other files are unrelated.
|
2026-08-20 23:27:48 +02:00
|
|
|
func (s *Server) references(p referenceParams) []Location {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
word, _ := wordAt(text, p.Position)
|
|
|
|
|
if word == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-09-14 18:50:55 +02:00
|
|
|
name := strings.TrimPrefix(word, "\u00B7")
|
2026-08-20 23:27:48 +02:00
|
|
|
uri := p.TextDocument.URI
|
2026-09-14 18:50:55 +02:00
|
|
|
|
2026-08-20 23:27:48 +02:00
|
|
|
var out []Location
|
2026-09-23 20:21:58 +02:00
|
|
|
for _, u := range s.scanUnits() {
|
|
|
|
|
sameDoc := u.uri == uri
|
|
|
|
|
for _, d := range u.file.Decls {
|
2026-09-14 18:50:55 +02:00
|
|
|
t, ok := d.(*ast.Text)
|
2026-09-19 23:49:19 +02:00
|
|
|
if !ok || !realSymbol(t.Name) {
|
2026-09-14 18:50:55 +02:00
|
|
|
continue
|
2026-08-20 23:27:48 +02:00
|
|
|
}
|
2026-09-14 18:50:55 +02:00
|
|
|
// Include the definition if requested.
|
|
|
|
|
if p.Context.IncludeDeclaration && t.Name.Name == name {
|
2026-09-23 20:21:58 +02:00
|
|
|
out = append(out, Location{URI: u.uri, Range: clientRange(u.text, symRange(t.Name))})
|
2026-09-14 18:50:55 +02:00
|
|
|
}
|
|
|
|
|
for _, stmt := range t.Body {
|
|
|
|
|
switch st := stmt.(type) {
|
|
|
|
|
case *ast.Label:
|
|
|
|
|
if sameDoc && st.Name.Text == name {
|
2026-09-23 20:21:58 +02:00
|
|
|
out = append(out, Location{URI: u.uri, Range: clientRange(u.text, tokenRange(st.Name))})
|
2026-09-14 18:50:55 +02:00
|
|
|
}
|
|
|
|
|
case *ast.Instr:
|
|
|
|
|
for _, op := range st.Operands {
|
|
|
|
|
if op.Addr.Sym == nil || op.Addr.Sym.Name != name {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !sameDoc && op.Addr.Sym.Pseudo != "SB" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-19 23:49:19 +02:00
|
|
|
// The range covers the operand's verbatim
|
|
|
|
|
// identifier, `·`/package prefix included, so a
|
|
|
|
|
// rename replaces the whole spelling.
|
2026-08-20 23:27:48 +02:00
|
|
|
out = append(out, Location{
|
2026-09-23 20:21:58 +02:00
|
|
|
URI: u.uri,
|
|
|
|
|
Range: clientRange(u.text, Range{
|
2026-08-20 23:27:48 +02:00
|
|
|
Start: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1},
|
2026-09-19 23:49:19 +02:00
|
|
|
End: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1 + symIdentLen(op.Addr.Sym)},
|
|
|
|
|
}),
|
2026-08-20 23:27:48 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 20:19:21 +02:00
|
|
|
// rename renames a label across every document the references reach. Each
|
|
|
|
|
// edit is grouped under the URI it belongs to: a range collected in another
|
|
|
|
|
// file must never be applied to the document that started the rename.
|
2026-08-20 23:27:48 +02:00
|
|
|
func (s *Server) rename(p renameParams) *WorkspaceEdit {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
word, _ := wordAt(text, p.Position)
|
|
|
|
|
if word == "" || word == p.NewName {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
refs := s.references(referenceParams{
|
|
|
|
|
TextDocument: p.TextDocument,
|
|
|
|
|
Position: p.Position,
|
|
|
|
|
Context: referenceContext{IncludeDeclaration: true},
|
|
|
|
|
})
|
|
|
|
|
if len(refs) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-09-23 20:19:21 +02:00
|
|
|
edits := make(map[string][]TextEdit, len(refs))
|
2026-08-20 23:27:48 +02:00
|
|
|
for _, loc := range refs {
|
2026-09-23 20:19:21 +02:00
|
|
|
edits[loc.URI] = append(edits[loc.URI], TextEdit{Range: loc.Range, NewText: p.NewName})
|
2026-08-20 23:27:48 +02:00
|
|
|
}
|
2026-09-23 20:19:21 +02:00
|
|
|
return &WorkspaceEdit{Changes: edits}
|
2026-08-20 23:27:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// documentFormatting returns a single edit that replaces the whole document
|
|
|
|
|
// with its formatted version (canonical indentation and spacing).
|
|
|
|
|
func (s *Server) documentFormatting(p documentFormattingParams) []TextEdit {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
if text == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-08-29 17:12:53 +02:00
|
|
|
formatted := format.Source(text)
|
2026-08-20 23:27:48 +02:00
|
|
|
if formatted == text {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
|
endLine := len(lines) - 1
|
|
|
|
|
endChar := 0
|
|
|
|
|
if endLine >= 0 {
|
2026-09-19 23:49:19 +02:00
|
|
|
endChar = utf16Len(lines[endLine])
|
2026-08-20 23:27:48 +02:00
|
|
|
}
|
|
|
|
|
return []TextEdit{{
|
|
|
|
|
Range: Range{Start: Position{Line: 0, Character: 0}, End: Position{Line: endLine, Character: endChar}},
|
|
|
|
|
NewText: formatted,
|
|
|
|
|
}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// inlayHints returns hints for frame sizes and argument areas.
|
|
|
|
|
func (s *Server) inlayHints(p inlayHintParams) []InlayHint {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
|
|
|
|
|
if f == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var out []InlayHint
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
t, ok := d.(*ast.Text)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Hint after the args size: show frame size.
|
|
|
|
|
if t.Frame != nil && t.Frame.Imm.HasVal && t.Args != nil && t.Args.Imm.HasVal {
|
|
|
|
|
// Place hint after the args operand using its raw text length.
|
2026-09-19 23:49:19 +02:00
|
|
|
line := t.Args.Pos.Line - 1
|
|
|
|
|
col := t.Args.Pos.Column - 1 + runeLen(t.Args.Raw)
|
2026-08-20 23:27:48 +02:00
|
|
|
out = append(out, InlayHint{
|
2026-09-19 23:49:19 +02:00
|
|
|
Position: Position{Line: line, Character: utf16Column(lineAt(text, line), col)},
|
2026-08-20 23:27:48 +02:00
|
|
|
Label: fmt.Sprintf(" frame=%d", t.Frame.Imm.Val),
|
|
|
|
|
Kind: inlayHintTypeParameter,
|
|
|
|
|
Tooltip: fmt.Sprintf("local frame size: %d bytes", t.Frame.Imm.Val),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 01:01:58 +02:00
|
|
|
// codeActions returns quick fixes for diagnostics in the selected range.
|
|
|
|
|
func (s *Server) codeActions(p codeActionParams) []CodeAction {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
if text == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var actions []CodeAction
|
|
|
|
|
for _, diag := range p.Context.Diagnostics {
|
|
|
|
|
switch diag.Code {
|
|
|
|
|
case "missing-ret":
|
2026-09-14 18:22:00 +02:00
|
|
|
// 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.
|
2026-08-21 01:01:58 +02:00
|
|
|
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
|
|
|
|
|
if f == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
t, ok := d.(*ast.Text)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-14 18:22:00 +02:00
|
|
|
if t.Keyword.Pos.Line-1 != int(diag.Range.Start.Line) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-08-21 01:01:58 +02:00
|
|
|
if len(t.Body) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
last := t.Body[len(t.Body)-1]
|
|
|
|
|
line := last.Pos().Line
|
|
|
|
|
// Insert RET after the last statement.
|
|
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
|
if line-1 < len(lines) {
|
|
|
|
|
insertLine := line // zero-based index for insertion after last stmt
|
|
|
|
|
newLines := make([]string, 0, len(lines)+1)
|
|
|
|
|
newLines = append(newLines, lines[:insertLine]...)
|
|
|
|
|
newLines = append(newLines, "\tRET")
|
|
|
|
|
newLines = append(newLines, lines[insertLine:]...)
|
|
|
|
|
newText := strings.Join(newLines, "\n")
|
|
|
|
|
endLine := len(lines) - 1
|
2026-09-19 23:49:19 +02:00
|
|
|
endChar := utf16Len(lines[endLine])
|
2026-08-21 01:01:58 +02:00
|
|
|
actions = append(actions, CodeAction{
|
2026-09-14 18:22:00 +02:00
|
|
|
Title: "Add RET to " + t.Name.Name,
|
2026-08-21 01:01:58 +02:00
|
|
|
Kind: "quickfix",
|
|
|
|
|
Edit: &WorkspaceEdit{
|
|
|
|
|
Changes: map[string][]TextEdit{p.TextDocument.URI: {
|
|
|
|
|
{Range: Range{Start: Position{Line: 0, Character: 0}, End: Position{Line: endLine, Character: endChar}}, NewText: newText},
|
|
|
|
|
}},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case "unused-label":
|
|
|
|
|
// Offer to remove the unused label.
|
|
|
|
|
label := strings.TrimPrefix(diag.Message, "label \"")
|
|
|
|
|
label = strings.TrimSuffix(label, "\" is defined but never referenced")
|
|
|
|
|
if label != "" {
|
|
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
|
for i, line := range lines {
|
|
|
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
|
if trimmed == label+":" {
|
|
|
|
|
newLines := make([]string, 0, len(lines)-1)
|
|
|
|
|
newLines = append(newLines, lines[:i]...)
|
|
|
|
|
newLines = append(newLines, lines[i+1:]...)
|
|
|
|
|
newText := strings.Join(newLines, "\n")
|
|
|
|
|
endLine := len(lines) - 1
|
2026-09-19 23:49:19 +02:00
|
|
|
endChar := utf16Len(lines[endLine])
|
2026-08-21 01:01:58 +02:00
|
|
|
actions = append(actions, CodeAction{
|
|
|
|
|
Title: "Remove unused label \"" + label + "\"",
|
|
|
|
|
Kind: "quickfix",
|
|
|
|
|
Edit: &WorkspaceEdit{
|
|
|
|
|
Changes: map[string][]TextEdit{p.TextDocument.URI: {
|
|
|
|
|
{Range: Range{Start: Position{Line: 0, Character: 0}, End: Position{Line: endLine, Character: endChar}}, NewText: newText},
|
|
|
|
|
}},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-23 20:23:35 +02:00
|
|
|
case "missing-textflag-include":
|
|
|
|
|
// 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.
|
|
|
|
|
insertLine := 0
|
|
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
|
for i, line := range lines {
|
|
|
|
|
if includeRe.MatchString(line) {
|
|
|
|
|
insertLine = i + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
actions = append(actions, CodeAction{
|
|
|
|
|
Title: `Add #include "textflag.h"`,
|
|
|
|
|
Kind: "quickfix",
|
|
|
|
|
Edit: &WorkspaceEdit{
|
|
|
|
|
Changes: map[string][]TextEdit{p.TextDocument.URI: {{
|
|
|
|
|
Range: Range{Start: Position{Line: insertLine, Character: 0}, End: Position{Line: insertLine, Character: 0}},
|
|
|
|
|
NewText: "#include \"textflag.h\"\n",
|
|
|
|
|
}}},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
case "abi-argsize":
|
|
|
|
|
// 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.
|
|
|
|
|
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.Args.Imm.HasVal || t.Args.Imm.Val == want {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// The parser records the argument area with its leading
|
|
|
|
|
// minus, so the replacement spans the whole `-16` shape.
|
|
|
|
|
rng := clientRange(text, Range{
|
|
|
|
|
Start: Position{Line: t.Args.Pos.Line - 1, Character: t.Args.Pos.Column - 1},
|
|
|
|
|
End: Position{Line: t.Args.Pos.Line - 1, Character: t.Args.Pos.Column - 1 + runeLen(t.Args.Raw)},
|
|
|
|
|
})
|
|
|
|
|
actions = append(actions, CodeAction{
|
|
|
|
|
Title: fmt.Sprintf("Set arg size to %d", want),
|
|
|
|
|
Kind: "quickfix",
|
|
|
|
|
Edit: &WorkspaceEdit{
|
|
|
|
|
Changes: map[string][]TextEdit{p.TextDocument.URI: {{
|
|
|
|
|
Range: rng,
|
|
|
|
|
NewText: "-" + strconv.FormatInt(want, 10),
|
|
|
|
|
}}},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-08-21 01:01:58 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return actions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// signatureHelp returns signature information for a CALL instruction.
|
|
|
|
|
func (s *Server) signatureHelp(p signatureHelpParams) *SignatureHelp {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
if text == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// Check if the cursor is on a CALL/BL line.
|
|
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
|
if p.Position.Line >= len(lines) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
line := lines[p.Position.Line]
|
|
|
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
|
upper := strings.ToUpper(trimmed)
|
|
|
|
|
if !strings.HasPrefix(upper, "CALL ") && !strings.HasPrefix(upper, "BL ") {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// Extract the function name from the CALL operand.
|
|
|
|
|
parts := strings.Fields(trimmed)
|
|
|
|
|
if len(parts) < 2 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
funcName := parts[1]
|
|
|
|
|
// Strip (SB) suffix.
|
|
|
|
|
funcName = strings.TrimSuffix(funcName, "(SB)")
|
|
|
|
|
|
|
|
|
|
// Look up the function in the document.
|
|
|
|
|
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
|
|
|
|
|
if f == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// Strip middle-dot prefix if present (Plan 9 package separator).
|
|
|
|
|
bareName := strings.TrimPrefix(funcName, "·")
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
t, ok := d.(*ast.Text)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if t.Name.Name == bareName || t.Name.Name == funcName || strings.HasSuffix(t.Name.Name, "·"+bareName) {
|
|
|
|
|
label := "TEXT " + t.Name.Name + "(SB)"
|
|
|
|
|
sig := SignatureInformation{
|
|
|
|
|
Label: label,
|
|
|
|
|
}
|
|
|
|
|
if t.Doc != "" {
|
|
|
|
|
sig.Documentation = t.Doc
|
|
|
|
|
}
|
|
|
|
|
// Add frame and args as parameters.
|
|
|
|
|
frame := int64(0)
|
|
|
|
|
if t.Frame != nil && t.Frame.Imm.HasVal {
|
|
|
|
|
frame = t.Frame.Imm.Val
|
|
|
|
|
}
|
|
|
|
|
args := int64(0)
|
|
|
|
|
if t.Args != nil && t.Args.Imm.HasVal {
|
|
|
|
|
args = t.Args.Imm.Val
|
|
|
|
|
}
|
|
|
|
|
sig.Parameters = []ParameterInformation{
|
|
|
|
|
{Label: fmt.Sprintf("$%d", frame)},
|
|
|
|
|
{Label: fmt.Sprintf("-%d", args)},
|
|
|
|
|
}
|
|
|
|
|
return &SignatureHelp{Signatures: []SignatureInformation{sig}}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// documentHighlights returns all occurrences of the symbol under the cursor.
|
|
|
|
|
func (s *Server) documentHighlights(p documentHighlightParams) []DocumentHighlight {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
word, _ := wordAt(text, p.Position)
|
|
|
|
|
if word == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-09-19 23:49:19 +02:00
|
|
|
// Symbol names are stored without the middle dot, while the word under
|
|
|
|
|
// the cursor keeps it; trim once and compare the trimmed form, the way
|
|
|
|
|
// references and definition do.
|
|
|
|
|
name := strings.TrimPrefix(word, "\u00B7")
|
2026-08-21 01:01:58 +02:00
|
|
|
f, errs := parser.Parse(uriPath(p.TextDocument.URI), text)
|
|
|
|
|
if f == nil || len(errs) > 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var out []DocumentHighlight
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
t, ok := d.(*ast.Text)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Highlight the definition.
|
2026-09-19 23:49:19 +02:00
|
|
|
if t.Name != nil && t.Name.Name == name {
|
2026-08-21 01:01:58 +02:00
|
|
|
out = append(out, DocumentHighlight{
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, symRange(t.Name)),
|
2026-08-21 01:01:58 +02:00
|
|
|
Kind: highlightWrite,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
for _, stmt := range t.Body {
|
|
|
|
|
switch st := stmt.(type) {
|
|
|
|
|
case *ast.Label:
|
2026-09-19 23:49:19 +02:00
|
|
|
if st.Name.Text == name {
|
2026-08-21 01:01:58 +02:00
|
|
|
out = append(out, DocumentHighlight{
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, tokenRange(st.Name)),
|
2026-08-21 01:01:58 +02:00
|
|
|
Kind: highlightWrite,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
case *ast.Instr:
|
|
|
|
|
for _, op := range st.Operands {
|
2026-09-19 23:49:19 +02:00
|
|
|
if op.Addr.Sym != nil && op.Addr.Sym.Name == name {
|
2026-08-21 01:01:58 +02:00
|
|
|
out = append(out, DocumentHighlight{
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, Range{
|
2026-08-21 01:01:58 +02:00
|
|
|
Start: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1},
|
2026-09-19 23:49:19 +02:00
|
|
|
End: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1 + symIdentLen(op.Addr.Sym)},
|
|
|
|
|
}),
|
2026-08-21 01:01:58 +02:00
|
|
|
Kind: highlightRead,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 20:21:58 +02:00
|
|
|
// workspaceSymbols searches all open documents and indexed workspace files
|
|
|
|
|
// for symbols matching the query.
|
2026-08-21 01:15:05 +02:00
|
|
|
func (s *Server) workspaceSymbols(p workspaceSymbolParams) []WorkspaceSymbol {
|
|
|
|
|
if p.Query == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
query := strings.ToLower(p.Query)
|
|
|
|
|
var out []WorkspaceSymbol
|
2026-09-23 20:21:58 +02:00
|
|
|
for _, u := range s.scanUnits() {
|
|
|
|
|
for _, d := range u.file.Decls {
|
2026-08-21 01:15:05 +02:00
|
|
|
switch dd := d.(type) {
|
|
|
|
|
case *ast.Text:
|
2026-09-19 23:49:19 +02:00
|
|
|
// A malformed TEXT line is kept in the tree under a "?"
|
|
|
|
|
// placeholder; it is not a symbol, and skipping it keeps
|
|
|
|
|
// the answer serving the rest of a mid-edit buffer.
|
|
|
|
|
if !realSymbol(dd.Name) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-08-21 01:15:05 +02:00
|
|
|
if strings.Contains(strings.ToLower(dd.Name.Name), query) {
|
|
|
|
|
out = append(out, WorkspaceSymbol{
|
|
|
|
|
Name: dd.Name.Name,
|
|
|
|
|
Kind: symFunction,
|
2026-09-23 20:21:58 +02:00
|
|
|
Location: Location{URI: u.uri, Range: clientRange(u.text, symRange(dd.Name))},
|
2026-08-21 01:15:05 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
case *ast.Globl:
|
|
|
|
|
if dd.Name != nil && strings.Contains(strings.ToLower(dd.Name.Name), query) {
|
|
|
|
|
out = append(out, WorkspaceSymbol{
|
|
|
|
|
Name: dd.Name.Name,
|
|
|
|
|
Kind: symConstant,
|
2026-09-23 20:21:58 +02:00
|
|
|
Location: Location{URI: u.uri, Range: clientRange(u.text, symRange(dd.Name))},
|
2026-08-21 01:15:05 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
case *ast.Data:
|
|
|
|
|
if dd.Name != nil && strings.Contains(strings.ToLower(dd.Name.Name), query) {
|
|
|
|
|
out = append(out, WorkspaceSymbol{
|
|
|
|
|
Name: dd.Name.Name,
|
|
|
|
|
Kind: symConstant,
|
2026-09-23 20:21:58 +02:00
|
|
|
Location: Location{URI: u.uri, Range: clientRange(u.text, symRange(dd.Name))},
|
2026-08-21 01:15:05 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
// documentSymbols returns functions and their labels, plus global symbols.
|
|
|
|
|
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
|
|
|
|
|
if f == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var out []DocumentSymbol
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
switch dd := d.(type) {
|
|
|
|
|
case *ast.Text:
|
2026-09-19 23:49:19 +02:00
|
|
|
// As in workspaceSymbols: a "?" placeholder is not a symbol,
|
|
|
|
|
// and the remaining declarations are still listed.
|
|
|
|
|
if !realSymbol(dd.Name) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-07-06 09:49:50 +02:00
|
|
|
sym := DocumentSymbol{
|
|
|
|
|
Name: dd.Name.Name,
|
|
|
|
|
Detail: "TEXT " + strings.Join(dd.Flags, " "),
|
|
|
|
|
Kind: symFunction,
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, textRange(dd)),
|
|
|
|
|
SelectionRange: clientRange(text, symRange(dd.Name)),
|
2026-07-06 09:49:50 +02:00
|
|
|
}
|
|
|
|
|
for _, st := range dd.Body {
|
|
|
|
|
if l, ok := st.(*ast.Label); ok {
|
|
|
|
|
sym.Children = append(sym.Children, DocumentSymbol{
|
|
|
|
|
Name: l.Name.Text,
|
|
|
|
|
Kind: symVariable,
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, tokenRange(l.Name)),
|
|
|
|
|
SelectionRange: clientRange(text, tokenRange(l.Name)),
|
2026-07-06 09:49:50 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out = append(out, sym)
|
|
|
|
|
case *ast.Globl:
|
2026-09-19 23:49:19 +02:00
|
|
|
if dd.Name == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-07-06 09:49:50 +02:00
|
|
|
out = append(out, DocumentSymbol{
|
|
|
|
|
Name: dd.Name.Name, Detail: "GLOBL", Kind: symConstant,
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, symRange(dd.Name)), SelectionRange: clientRange(text, symRange(dd.Name)),
|
2026-07-06 09:49:50 +02:00
|
|
|
})
|
|
|
|
|
case *ast.Data:
|
2026-09-19 23:49:19 +02:00
|
|
|
if dd.Name == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-07-06 09:49:50 +02:00
|
|
|
out = append(out, DocumentSymbol{
|
|
|
|
|
Name: dd.Name.Name, Detail: "DATA", Kind: symConstant,
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, symRange(dd.Name)), SelectionRange: clientRange(text, symRange(dd.Name)),
|
2026-07-06 09:49:50 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// semTok is one classified token before delta encoding.
|
|
|
|
|
type semTok struct {
|
|
|
|
|
line, char, length, typ int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// semanticTokens encodes syntax highlighting as LSP semantic tokens.
|
|
|
|
|
func (s *Server) semanticTokens(p semanticTokensParams) SemanticTokens {
|
|
|
|
|
text := s.docs[p.TextDocument.URI]
|
|
|
|
|
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
|
|
|
|
|
f, _ := parser.Parse("", text)
|
|
|
|
|
labels := map[string]bool{}
|
|
|
|
|
for _, name := range labelNames(f) {
|
|
|
|
|
labels[name] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toks := lexer.Tokenize(text)
|
|
|
|
|
lines := groupLines(toks)
|
2026-09-19 23:49:19 +02:00
|
|
|
srcLines := strings.Split(text, "\n")
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
var encoded []semTok
|
|
|
|
|
for _, line := range lines {
|
2026-09-19 23:49:19 +02:00
|
|
|
for _, st := range classifyLine(line, a, labels) {
|
|
|
|
|
// Columns cross the protocol boundary in UTF-16 code units.
|
|
|
|
|
if st.line >= 0 && st.line < len(srcLines) {
|
|
|
|
|
st.char = utf16Column(srcLines[st.line], st.char)
|
|
|
|
|
}
|
|
|
|
|
encoded = append(encoded, st)
|
|
|
|
|
}
|
2026-07-06 09:49:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return SemanticTokens{Data: deltaEncode(encoded)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// classifyLine assigns a semantic token type to each significant token on a line.
|
|
|
|
|
func classifyLine(line []token.Token, a *arch.Table, labels map[string]bool) []semTok {
|
|
|
|
|
if len(line) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var out []semTok
|
|
|
|
|
first := firstSignificant(line)
|
|
|
|
|
if first < 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isDirective := line[first].Kind == token.Ident &&
|
|
|
|
|
(line[first].Text == "TEXT" || line[first].Text == "DATA" || line[first].Text == "GLOBL")
|
|
|
|
|
isLabel := line[first].Kind == token.Ident && first+1 < len(line) &&
|
|
|
|
|
line[first+1].Kind == token.Colon
|
|
|
|
|
isInstr := !isDirective && !isLabel && line[first].Kind == token.Ident
|
|
|
|
|
|
|
|
|
|
mnemonicDone := false
|
|
|
|
|
for i, t := range line {
|
|
|
|
|
typ := -1
|
|
|
|
|
switch t.Kind {
|
|
|
|
|
case token.Comment:
|
|
|
|
|
typ = stComment
|
|
|
|
|
case token.Number:
|
|
|
|
|
typ = stNumber
|
|
|
|
|
case token.String, token.Rune:
|
|
|
|
|
typ = stString
|
|
|
|
|
case token.Hash:
|
|
|
|
|
typ = stMacro
|
|
|
|
|
case token.Ident:
|
2026-08-29 17:12:53 +02:00
|
|
|
typ = classifyIdent(i, first, t.Text, a, labels, isDirective, isLabel, isInstr, &mnemonicDone)
|
2026-07-06 09:49:50 +02:00
|
|
|
case token.Colon, token.Comma, token.LParen, token.RParen,
|
|
|
|
|
token.Plus, token.Minus, token.Star, token.Slash, token.Dollar,
|
2026-09-19 23:49:19 +02:00
|
|
|
token.LAngle, token.RAngle, token.LShift, token.RShift, token.Arrow,
|
|
|
|
|
token.At, token.Pipe:
|
2026-07-06 09:49:50 +02:00
|
|
|
typ = stOperator
|
|
|
|
|
}
|
|
|
|
|
if typ >= 0 {
|
|
|
|
|
out = append(out, semTok{
|
|
|
|
|
line: t.Pos.Line - 1,
|
|
|
|
|
char: t.Pos.Column - 1,
|
2026-09-19 23:49:19 +02:00
|
|
|
length: utf16Len(t.Text),
|
2026-07-06 09:49:50 +02:00
|
|
|
typ: typ,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// classifyIdent decides the semantic type of an identifier token.
|
2026-08-29 17:12:53 +02:00
|
|
|
func classifyIdent(i, first int, text string, a *arch.Table, labels map[string]bool, isDirective, isLabel, isInstr bool, mnemonicDone *bool) int {
|
2026-07-06 09:49:50 +02:00
|
|
|
|
|
|
|
|
upper := strings.ToUpper(text)
|
|
|
|
|
switch {
|
|
|
|
|
case isDirective && i == first:
|
|
|
|
|
return stKeyword
|
|
|
|
|
case isDirective && textflagMacros[upper]:
|
|
|
|
|
return stMacro
|
|
|
|
|
case isLabel && i == first:
|
|
|
|
|
return stNamespace
|
|
|
|
|
case arch.IsPseudoReg(text):
|
|
|
|
|
return stProperty
|
|
|
|
|
case labels[text]:
|
|
|
|
|
return stNamespace
|
|
|
|
|
}
|
|
|
|
|
if r, ok := a.Register(text); ok {
|
|
|
|
|
switch r.Class {
|
|
|
|
|
case arch.Vector, arch.Mask, arch.Float, arch.VecARM:
|
|
|
|
|
return stType
|
|
|
|
|
default:
|
|
|
|
|
return stVariable
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if isInstr && i == first && !*mnemonicDone {
|
|
|
|
|
*mnemonicDone = true
|
|
|
|
|
return stFunction
|
|
|
|
|
}
|
|
|
|
|
// Argument/symbol names and anything else.
|
|
|
|
|
return stVariable
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// deltaEncode converts absolute token positions to the LSP relative encoding.
|
|
|
|
|
func deltaEncode(toks []semTok) []int {
|
|
|
|
|
data := make([]int, 0, len(toks)*5)
|
|
|
|
|
prevLine, prevChar := 0, 0
|
|
|
|
|
for _, t := range toks {
|
|
|
|
|
dLine := t.line - prevLine
|
|
|
|
|
dChar := t.char
|
|
|
|
|
if dLine == 0 {
|
|
|
|
|
dChar = t.char - prevChar
|
|
|
|
|
}
|
|
|
|
|
data = append(data, dLine, dChar, t.length, t.typ, 0)
|
|
|
|
|
prevLine, prevChar = t.line, t.char
|
|
|
|
|
}
|
|
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- shared helpers ---------------------------------------------------------
|
|
|
|
|
|
2026-09-19 23:49:19 +02:00
|
|
|
// wordAt extracts the identifier surrounding pos and its range. The
|
|
|
|
|
// incoming character offset is UTF-16 code units, as LSP defines it, and is
|
|
|
|
|
// converted to the rune index the scanning works in.
|
2026-07-06 09:49:50 +02:00
|
|
|
func wordAt(text string, pos Position) (string, Range) {
|
|
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
|
if pos.Line < 0 || pos.Line >= len(lines) {
|
|
|
|
|
return "", Range{}
|
|
|
|
|
}
|
|
|
|
|
runes := []rune(lines[pos.Line])
|
2026-09-19 23:49:19 +02:00
|
|
|
col := runeColumn(lines[pos.Line], pos.Character)
|
2026-07-06 09:49:50 +02:00
|
|
|
if col < 0 || col > len(runes) {
|
|
|
|
|
return "", Range{}
|
|
|
|
|
}
|
|
|
|
|
isWord := func(r rune) bool {
|
|
|
|
|
return r == '_' || r == '\u00B7' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
|
|
|
|
}
|
|
|
|
|
start, end := col, col
|
|
|
|
|
for start > 0 && isWord(runes[start-1]) {
|
|
|
|
|
start--
|
|
|
|
|
}
|
|
|
|
|
for end < len(runes) && isWord(runes[end]) {
|
|
|
|
|
end++
|
|
|
|
|
}
|
|
|
|
|
if start == end {
|
|
|
|
|
return "", Range{}
|
|
|
|
|
}
|
|
|
|
|
rng := Range{
|
|
|
|
|
Start: Position{Line: pos.Line, Character: start},
|
|
|
|
|
End: Position{Line: pos.Line, Character: end},
|
|
|
|
|
}
|
|
|
|
|
return string(runes[start:end]), rng
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// labelNames collects every label defined in a file.
|
|
|
|
|
func labelNames(f *ast.File) []string {
|
|
|
|
|
if f == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
var out []string
|
|
|
|
|
collect := func(body []ast.Stmt) {
|
|
|
|
|
for _, st := range body {
|
|
|
|
|
if l, ok := st.(*ast.Label); ok && !seen[l.Name.Text] {
|
|
|
|
|
seen[l.Name.Text] = true
|
|
|
|
|
out = append(out, l.Name.Text)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
if t, ok := d.(*ast.Text); ok {
|
|
|
|
|
collect(t.Body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
collect(f.Orphans)
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// groupLines splits a token stream into lines, keeping Newline boundaries but
|
|
|
|
|
// dropping the Newline and EOF tokens themselves.
|
|
|
|
|
func groupLines(toks []token.Token) [][]token.Token {
|
|
|
|
|
var lines [][]token.Token
|
|
|
|
|
var cur []token.Token
|
|
|
|
|
for _, t := range toks {
|
|
|
|
|
if t.Kind == token.EOF {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if t.Kind == token.Newline {
|
|
|
|
|
lines = append(lines, cur)
|
|
|
|
|
cur = nil
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
cur = append(cur, t)
|
|
|
|
|
}
|
|
|
|
|
if len(cur) > 0 {
|
|
|
|
|
lines = append(lines, cur)
|
|
|
|
|
}
|
|
|
|
|
return lines
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func firstSignificant(line []token.Token) int {
|
|
|
|
|
for i, t := range line {
|
|
|
|
|
if t.Kind != token.Comment {
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runeLen(s string) int { return len([]rune(s)) }
|
|
|
|
|
|
2026-09-19 23:49:19 +02:00
|
|
|
// utf16Len returns the length of s in UTF-16 code units, the unit LSP
|
|
|
|
|
// positions count: an astral rune (an emoji in a comment) is two of them.
|
|
|
|
|
func utf16Len(s string) int {
|
|
|
|
|
n := 0
|
|
|
|
|
for _, r := range s {
|
|
|
|
|
n += utf16.RuneLen(r)
|
|
|
|
|
}
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// utf16Column converts a rune-based column on line into a UTF-16 code-unit
|
|
|
|
|
// offset. Rune columns are the lexer's convention; code units are the
|
|
|
|
|
// protocol's, and the two diverge once an astral rune precedes the column.
|
|
|
|
|
func utf16Column(line string, col int) int {
|
|
|
|
|
if col <= 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
units := 0
|
|
|
|
|
seen := 0
|
|
|
|
|
for _, r := range line {
|
|
|
|
|
if seen >= col {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
units += utf16.RuneLen(r)
|
|
|
|
|
seen++
|
|
|
|
|
}
|
|
|
|
|
return units
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// runeColumn converts a UTF-16 code-unit offset on line into a rune column,
|
|
|
|
|
// the inverse of utf16Column, applied to positions arriving from the client.
|
|
|
|
|
func runeColumn(line string, units int) int {
|
|
|
|
|
if units <= 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
col := 0
|
|
|
|
|
u := 0
|
|
|
|
|
for _, r := range line {
|
|
|
|
|
if u >= units {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
u += utf16.RuneLen(r)
|
|
|
|
|
col++
|
|
|
|
|
}
|
|
|
|
|
return col
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// lineAt returns the n-th zero-based line of text, or "" when out of range.
|
|
|
|
|
func lineAt(text string, n int) string {
|
|
|
|
|
if n < 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
|
if n >= len(lines) {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return lines[n]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clientRange re-encodes a rune-based range (the columns the lexer, parser
|
|
|
|
|
// and the helpers above produce) in the UTF-16 code units LSP mandates.
|
|
|
|
|
// Every range leaving the server passes through here.
|
|
|
|
|
func clientRange(text string, r Range) Range {
|
|
|
|
|
return Range{
|
|
|
|
|
Start: Position{Line: r.Start.Line, Character: utf16Column(lineAt(text, r.Start.Line), r.Start.Character)},
|
|
|
|
|
End: Position{Line: r.End.Line, Character: utf16Column(lineAt(text, r.End.Line), r.End.Character)},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// symIdentLen returns the rune length of a symbol's identifier as written:
|
|
|
|
|
// the verbatim spelling up to the ABI marker, offset or pseudo-register
|
|
|
|
|
// group, so `pkg·name<ABIInternal>(SB)` counts the package prefix and the
|
|
|
|
|
// middle dot. A range built from it covers the whole token a rename
|
|
|
|
|
// replaces; the stripped Name alone would stop one character short.
|
|
|
|
|
func symIdentLen(sym *ast.Symbol) int {
|
|
|
|
|
if i := strings.IndexAny(sym.Raw, "<+-("); i >= 0 {
|
|
|
|
|
return runeLen(sym.Raw[:i])
|
|
|
|
|
}
|
|
|
|
|
return runeLen(sym.Raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// symRange builds a range covering a symbol as written, prefix included.
|
2026-07-06 09:49:50 +02:00
|
|
|
func symRange(sym *ast.Symbol) Range {
|
|
|
|
|
start := Position{Line: sym.Pos.Line - 1, Character: sym.Pos.Column - 1}
|
|
|
|
|
end := start
|
2026-09-19 23:49:19 +02:00
|
|
|
end.Character += symIdentLen(sym)
|
2026-07-06 09:49:50 +02:00
|
|
|
return Range{Start: start, End: end}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:19 +02:00
|
|
|
// realSymbol reports whether sym names something a client can act on. The
|
|
|
|
|
// parser keeps a malformed TEXT in the tree under a "?" placeholder so the
|
|
|
|
|
// rest of the buffer stays servable; that placeholder is not a symbol.
|
|
|
|
|
func realSymbol(sym *ast.Symbol) bool {
|
|
|
|
|
return sym != nil && sym.Name != "?"
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
// tokenRange builds a range covering one token.
|
|
|
|
|
func tokenRange(t token.Token) Range {
|
|
|
|
|
return Range{
|
|
|
|
|
Start: Position{Line: t.Pos.Line - 1, Character: t.Pos.Column - 1},
|
|
|
|
|
End: Position{Line: t.End.Line - 1, Character: t.End.Column - 1},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// textRange spans a TEXT function from its keyword to the end of its body.
|
|
|
|
|
func textRange(t *ast.Text) Range {
|
|
|
|
|
start := Position{Line: t.Keyword.Pos.Line - 1, Character: t.Keyword.Pos.Column - 1}
|
|
|
|
|
end := start
|
|
|
|
|
end.Character += runeLen(t.Keyword.Text)
|
|
|
|
|
if n := len(t.Body); n > 0 {
|
|
|
|
|
last := t.Body[n-1]
|
|
|
|
|
if in, ok := last.(*ast.Instr); ok {
|
|
|
|
|
end = Position{Line: in.Mnemonic.End.Line - 1, Character: in.Mnemonic.End.Column - 1}
|
|
|
|
|
} else {
|
|
|
|
|
lp := last.Pos()
|
|
|
|
|
end = Position{Line: lp.Line - 1, Character: lp.Column - 1}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Range{Start: start, End: end}
|
|
|
|
|
}
|
2026-08-30 21:30:22 +02:00
|
|
|
|
|
|
|
|
// diagnosticsFor computes the LSP diagnostics of one document; the push
|
|
|
|
|
// (publishDiagnostics) and pull (textDocument/diagnostic) paths share it.
|
2026-09-14 18:22:00 +02:00
|
|
|
// Parse errors surface as error-severity diagnostics so a malformed line is
|
|
|
|
|
// visible in the editor instead of only breaking derived features.
|
2026-08-30 21:30:22 +02:00
|
|
|
func (s *Server) diagnosticsFor(uri string) []Diagnostic {
|
|
|
|
|
text := s.docs[uri]
|
2026-09-14 18:22:00 +02:00
|
|
|
f, errs := parser.Parse(uriPath(uri), text)
|
2026-08-30 21:30:22 +02:00
|
|
|
cfg := lint.Config{Arch: arch.FromFilename(uriPath(uri))}
|
|
|
|
|
diags := lint.File(f, cfg)
|
|
|
|
|
|
2026-09-14 18:22:00 +02:00
|
|
|
out := make([]Diagnostic, 0, len(diags)+len(errs))
|
|
|
|
|
for _, e := range errs {
|
|
|
|
|
pos := token.Position{Line: 1, Column: 1}
|
|
|
|
|
if pe, ok := e.(parser.Error); ok && pe.Pos.IsValid() {
|
|
|
|
|
pos = pe.Pos
|
|
|
|
|
}
|
|
|
|
|
out = append(out, Diagnostic{
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, toRange(pos.Line, pos.Column, token.Position{})),
|
2026-09-14 18:22:00 +02:00
|
|
|
Severity: sevError,
|
|
|
|
|
Code: "syntax",
|
|
|
|
|
Source: "gasm",
|
|
|
|
|
Message: e.Error(),
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-08-30 21:30:22 +02:00
|
|
|
for _, d := range diags {
|
|
|
|
|
out = append(out, Diagnostic{
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, toRange(d.Pos.Line, d.Pos.Column, d.End)),
|
2026-08-30 21:30:22 +02:00
|
|
|
Severity: lintSeverity(d.Severity),
|
|
|
|
|
Code: d.Code,
|
|
|
|
|
Source: "gasm",
|
|
|
|
|
Message: d.Message,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// includeRe matches a #include directive's quoted path.
|
|
|
|
|
var includeRe = regexp.MustCompile(`^\s*#include\s+"([^"]+)"`)
|
|
|
|
|
|
|
|
|
|
// documentLinks links each #include path to the header file it resolves to:
|
|
|
|
|
// first relative to the document's directory, then in $GOROOT/pkg/include
|
|
|
|
|
// (where textflag.h and friends live). Unresolvable paths get no link.
|
|
|
|
|
func (s *Server) documentLinks(uri string) []DocumentLink {
|
|
|
|
|
text := s.docs[uri]
|
|
|
|
|
if text == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
docDir := filepath.Dir(uriPath(uri))
|
|
|
|
|
goroot := runtime.GOROOT()
|
|
|
|
|
|
|
|
|
|
var out []DocumentLink
|
2026-09-19 23:49:19 +02:00
|
|
|
lineNo := -1
|
|
|
|
|
for line := range strings.SplitSeq(text, "\n") {
|
|
|
|
|
lineNo++
|
2026-08-30 21:30:22 +02:00
|
|
|
m := includeRe.FindStringSubmatch(line)
|
|
|
|
|
if m == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
target := resolveInclude(docDir, goroot, m[1])
|
|
|
|
|
if target == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-19 23:49:19 +02:00
|
|
|
// strings.Index is a byte offset; columns are runes.
|
|
|
|
|
start := utf8.RuneCountInString(line[:strings.Index(line, "\"")])
|
2026-08-30 21:30:22 +02:00
|
|
|
out = append(out, DocumentLink{
|
2026-09-19 23:49:19 +02:00
|
|
|
Range: clientRange(text, Range{
|
|
|
|
|
Start: Position{Line: lineNo, Character: start},
|
|
|
|
|
End: Position{Line: lineNo, Character: start + runeLen(m[1]) + 2},
|
|
|
|
|
}),
|
2026-08-30 21:30:22 +02:00
|
|
|
Target: "file://" + target,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolveInclude returns the existing file a #include path refers to, or "".
|
|
|
|
|
func resolveInclude(docDir, goroot, path string) string {
|
|
|
|
|
for _, dir := range []string{docDir, filepath.Join(goroot, "pkg", "include")} {
|
|
|
|
|
cand := filepath.Join(dir, path)
|
|
|
|
|
if _, err := os.Stat(cand); err == nil {
|
|
|
|
|
return cand
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// foldingRanges marks each TEXT function's body as a collapsible region.
|
|
|
|
|
func (s *Server) foldingRanges(uri string) []FoldingRange {
|
|
|
|
|
text := s.docs[uri]
|
|
|
|
|
f, _ := parser.Parse(uriPath(uri), text)
|
|
|
|
|
if f == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var out []FoldingRange
|
|
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
t, ok := d.(*ast.Text)
|
|
|
|
|
if !ok || len(t.Body) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
start := t.Keyword.Pos.Line - 1
|
|
|
|
|
end := t.Body[len(t.Body)-1].Pos().Line - 1
|
|
|
|
|
if end <= start {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
out = append(out, FoldingRange{StartLine: start, EndLine: end, Kind: "region"})
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|