feat(lsp): add references, rename, formatting, and inlay hints

Assisted-by: MiMo V2.5 Pro
This commit is contained in:
2026-08-21 00:35:21 +02:00
parent f1960febdc
commit 874e054ecb
3 changed files with 312 additions and 10 deletions
+217
View File
@@ -4,6 +4,7 @@
package lsp
import (
"fmt"
"sort"
"strings"
"unicode"
@@ -125,6 +126,222 @@ func (s *Server) definition(p definitionParams) []Location {
return nil
}
// references returns all locations where the symbol under the cursor appears.
func (s *Server) references(p referenceParams) []Location {
text := s.docs[p.TextDocument.URI]
word, _ := wordAt(text, p.Position)
if word == "" {
return nil
}
f, errs := parser.Parse(uriPath(p.TextDocument.URI), text)
if f == nil || len(errs) > 0 {
return nil
}
uri := p.TextDocument.URI
var out []Location
for _, d := range f.Decls {
t, ok := d.(*ast.Text)
if !ok {
continue
}
// Include the definition if requested.
if p.Context.IncludeDeclaration {
if t.Name.Name == word {
out = append(out, Location{
URI: uri,
Range: symRange(t.Name),
})
}
}
for _, stmt := range t.Body {
switch st := stmt.(type) {
case *ast.Label:
if st.Name.Text == word {
out = append(out, Location{
URI: uri,
Range: tokenRange(st.Name),
})
}
case *ast.Instr:
for _, op := range st.Operands {
if op.Addr.Sym != nil && op.Addr.Sym.Name == word {
out = append(out, Location{
URI: uri,
Range: Range{
Start: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1},
End: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1 + runeLen(word)},
},
})
}
}
}
}
}
return out
}
// rename renames a label across the document.
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
}
edits := make([]TextEdit, 0, len(refs))
for _, loc := range refs {
edits = append(edits, TextEdit{Range: loc.Range, NewText: p.NewName})
}
return &WorkspaceEdit{
Changes: map[string][]TextEdit{p.TextDocument.URI: edits},
}
}
// 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
}
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
if f == nil {
return nil
}
formatted := formatFile(f)
if formatted == text {
return nil
}
lines := strings.Split(text, "\n")
endLine := len(lines) - 1
endChar := 0
if endLine >= 0 {
endChar = len([]rune(lines[endLine]))
}
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.
col := t.Args.Pos.Column - 1 + len(t.Args.Raw)
out = append(out, InlayHint{
Position: Position{Line: t.Args.Pos.Line - 1, Character: col},
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
}
// formatFile produces a canonical formatting of a parsed file.
func formatFile(f *ast.File) string {
var b strings.Builder
for i, d := range f.Decls {
if i > 0 {
b.WriteByte('\n')
}
switch dd := d.(type) {
case *ast.Include:
fmt.Fprintf(&b, "#include %s\n", dd.Header.Text)
case *ast.Preproc:
b.WriteString("#")
b.WriteString(dd.Raw)
b.WriteByte('\n')
case *ast.Text:
fmt.Fprintf(&b, "TEXT %s(SB), %s, $%d-%d\n",
symString(dd.Name), strings.Join(dd.Flags, "|"), frameVal(dd.Frame), argsVal(dd.Args))
for _, stmt := range dd.Body {
switch st := stmt.(type) {
case *ast.Label:
fmt.Fprintf(&b, "%s:\n", st.Name.Text)
case *ast.Instr:
b.WriteByte('\t')
b.WriteString(st.Mnemonic.Text)
for j, op := range st.Operands {
if j > 0 {
b.WriteString(", ")
}
b.WriteString(op.Raw)
}
if st.Comment != "" {
b.WriteString(st.Comment)
}
b.WriteByte('\n')
}
}
case *ast.Globl:
fmt.Fprintf(&b, "GLOBL %s(SB), %s, $%d\n",
symString(dd.Name), strings.Join(dd.Flags, "|"), globlSize(dd.Size))
case *ast.Data:
fmt.Fprintf(&b, "DATA %s+%d(SB)/%d, %s\n",
symString(dd.Name), dd.Name.Offset, dd.Width, dd.Value.Raw)
}
}
return b.String()
}
func symString(sym *ast.Symbol) string {
if sym == nil {
return "<?>"
}
s := ""
if sym.Pkg != "" {
s = sym.Pkg + "·"
}
s += sym.Name
if sym.Static {
s += "<>"
}
return s
}
func frameVal(op *ast.Operand) int64 {
if op != nil && op.Imm.HasVal {
return op.Imm.Val
}
return 0
}
func argsVal(op *ast.Operand) int64 {
if op != nil && op.Imm.HasVal {
return op.Imm.Val
}
return 0
}
func globlSize(op *ast.Operand) int64 {
if op != nil && op.Imm.HasVal {
return op.Imm.Val
}
return 0
}
// documentSymbols returns functions and their labels, plus global symbols.
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
text := s.docs[p.TextDocument.URI]