feat(lsp): add references, rename, formatting, and inlay hints
Assisted-by: MiMo V2.5 Pro
This commit is contained in:
+217
@@ -4,6 +4,7 @@
|
|||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -125,6 +126,222 @@ func (s *Server) definition(p definitionParams) []Location {
|
|||||||
return nil
|
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.
|
// documentSymbols returns functions and their labels, plus global symbols.
|
||||||
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
|
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
|
||||||
text := s.docs[p.TextDocument.URI]
|
text := s.docs[p.TextDocument.URI]
|
||||||
|
|||||||
+66
-6
@@ -157,6 +157,61 @@ type definitionParams struct {
|
|||||||
Position Position `json:"position"`
|
Position Position `json:"position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type referenceParams struct {
|
||||||
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
Context referenceContext `json:"context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type referenceContext struct {
|
||||||
|
IncludeDeclaration bool `json:"includeDeclaration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type renameParams struct {
|
||||||
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
NewName string `json:"newName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkspaceEdit holds changes across multiple documents.
|
||||||
|
type WorkspaceEdit struct {
|
||||||
|
Changes map[string][]TextEdit `json:"changes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextEdit replaces a range with new text.
|
||||||
|
type TextEdit struct {
|
||||||
|
Range Range `json:"range"`
|
||||||
|
NewText string `json:"newText"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type documentFormattingParams struct {
|
||||||
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||||
|
Options formattingOptions `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type formattingOptions struct {
|
||||||
|
TabSize int `json:"tabSize"`
|
||||||
|
InsertSpaces bool `json:"insertSpaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type inlayHintParams struct {
|
||||||
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||||
|
Range Range `json:"range"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InlayHint is a hint displayed inline in the editor.
|
||||||
|
type InlayHint struct {
|
||||||
|
Position Position `json:"position"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Kind int `json:"kind,omitempty"`
|
||||||
|
Padding bool `json:"paddingLeft,omitempty"`
|
||||||
|
Tooltip string `json:"tooltip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
inlayHintTypeParameter = 2
|
||||||
|
)
|
||||||
|
|
||||||
// Hover is the hover response.
|
// Hover is the hover response.
|
||||||
type Hover struct {
|
type Hover struct {
|
||||||
Contents markupContent `json:"contents"`
|
Contents markupContent `json:"contents"`
|
||||||
@@ -246,12 +301,17 @@ type initializeParams struct {
|
|||||||
|
|
||||||
// ServerCapabilities advertises what this server provides.
|
// ServerCapabilities advertises what this server provides.
|
||||||
type ServerCapabilities struct {
|
type ServerCapabilities struct {
|
||||||
TextDocumentSync int `json:"textDocumentSync"`
|
TextDocumentSync int `json:"textDocumentSync"`
|
||||||
CompletionProvider map[string]any `json:"completionProvider,omitempty"`
|
CompletionProvider map[string]any `json:"completionProvider,omitempty"`
|
||||||
HoverProvider bool `json:"hoverProvider,omitempty"`
|
HoverProvider bool `json:"hoverProvider,omitempty"`
|
||||||
DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"`
|
DefinitionProvider bool `json:"definitionProvider,omitempty"`
|
||||||
SemanticTokensProvider map[string]any `json:"semanticTokensProvider,omitempty"`
|
ReferencesProvider bool `json:"referencesProvider,omitempty"`
|
||||||
DiagnosticProvider map[string]any `json:"diagnosticProvider,omitempty"`
|
RenameProvider bool `json:"renameProvider,omitempty"`
|
||||||
|
DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"`
|
||||||
|
DocumentFormattingProvider bool `json:"documentFormattingProvider,omitempty"`
|
||||||
|
SemanticTokensProvider map[string]any `json:"semanticTokensProvider,omitempty"`
|
||||||
|
InlayHintProvider bool `json:"inlayHintProvider,omitempty"`
|
||||||
|
DiagnosticProvider map[string]any `json:"diagnosticProvider,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type initializeResult struct {
|
type initializeResult struct {
|
||||||
|
|||||||
+29
-4
@@ -116,10 +116,14 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
|
|||||||
case "initialize":
|
case "initialize":
|
||||||
s.respond(msg.ID, initializeResult{
|
s.respond(msg.ID, initializeResult{
|
||||||
Capabilities: ServerCapabilities{
|
Capabilities: ServerCapabilities{
|
||||||
TextDocumentSync: 1, // full sync
|
TextDocumentSync: 1, // full sync
|
||||||
CompletionProvider: map[string]any{},
|
CompletionProvider: map[string]any{},
|
||||||
HoverProvider: true,
|
HoverProvider: true,
|
||||||
DocumentSymbolProvider: true,
|
DefinitionProvider: true,
|
||||||
|
ReferencesProvider: true,
|
||||||
|
RenameProvider: true,
|
||||||
|
DocumentSymbolProvider: true,
|
||||||
|
DocumentFormattingProvider: true,
|
||||||
SemanticTokensProvider: map[string]any{
|
SemanticTokensProvider: map[string]any{
|
||||||
"legend": SemanticTokensLegend{
|
"legend": SemanticTokensLegend{
|
||||||
TokenTypes: semanticTokenTypes,
|
TokenTypes: semanticTokenTypes,
|
||||||
@@ -127,6 +131,7 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
|
|||||||
},
|
},
|
||||||
"full": true,
|
"full": true,
|
||||||
},
|
},
|
||||||
|
InlayHintProvider: true,
|
||||||
},
|
},
|
||||||
ServerInfo: map[string]string{"name": "gasm", "version": "0.1.0"},
|
ServerInfo: map[string]string{"name": "gasm", "version": "0.1.0"},
|
||||||
})
|
})
|
||||||
@@ -180,6 +185,26 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
|
|||||||
json.Unmarshal(msg.Params, &p)
|
json.Unmarshal(msg.Params, &p)
|
||||||
s.respond(msg.ID, s.documentSymbols(p))
|
s.respond(msg.ID, s.documentSymbols(p))
|
||||||
|
|
||||||
|
case "textDocument/references":
|
||||||
|
var p referenceParams
|
||||||
|
json.Unmarshal(msg.Params, &p)
|
||||||
|
s.respond(msg.ID, s.references(p))
|
||||||
|
|
||||||
|
case "textDocument/rename":
|
||||||
|
var p renameParams
|
||||||
|
json.Unmarshal(msg.Params, &p)
|
||||||
|
s.respond(msg.ID, s.rename(p))
|
||||||
|
|
||||||
|
case "textDocument/formatting":
|
||||||
|
var p documentFormattingParams
|
||||||
|
json.Unmarshal(msg.Params, &p)
|
||||||
|
s.respond(msg.ID, s.documentFormatting(p))
|
||||||
|
|
||||||
|
case "textDocument/inlayHint":
|
||||||
|
var p inlayHintParams
|
||||||
|
json.Unmarshal(msg.Params, &p)
|
||||||
|
s.respond(msg.ID, s.inlayHints(p))
|
||||||
|
|
||||||
case "textDocument/semanticTokens/full":
|
case "textDocument/semanticTokens/full":
|
||||||
var p semanticTokensParams
|
var p semanticTokensParams
|
||||||
json.Unmarshal(msg.Params, &p)
|
json.Unmarshal(msg.Params, &p)
|
||||||
|
|||||||
Reference in New Issue
Block a user