From 874e054ecb998cb0748017878ebf1bfb77cc7f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 20 Aug 2026 23:27:48 +0200 Subject: [PATCH] feat(lsp): add references, rename, formatting, and inlay hints Assisted-by: MiMo V2.5 Pro --- lsp/handlers.go | 217 ++++++++++++++++++++++++++++++++++++++++++++++++ lsp/protocol.go | 72 ++++++++++++++-- lsp/server.go | 33 +++++++- 3 files changed, 312 insertions(+), 10 deletions(-) diff --git a/lsp/handlers.go b/lsp/handlers.go index 8a6b72b..b1fee61 100644 --- a/lsp/handlers.go +++ b/lsp/handlers.go @@ -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] diff --git a/lsp/protocol.go b/lsp/protocol.go index 75c83b8..4983cdc 100644 --- a/lsp/protocol.go +++ b/lsp/protocol.go @@ -157,6 +157,61 @@ type definitionParams struct { 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. type Hover struct { Contents markupContent `json:"contents"` @@ -246,12 +301,17 @@ type initializeParams struct { // ServerCapabilities advertises what this server provides. type ServerCapabilities struct { - TextDocumentSync int `json:"textDocumentSync"` - CompletionProvider map[string]any `json:"completionProvider,omitempty"` - HoverProvider bool `json:"hoverProvider,omitempty"` - DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"` - SemanticTokensProvider map[string]any `json:"semanticTokensProvider,omitempty"` - DiagnosticProvider map[string]any `json:"diagnosticProvider,omitempty"` + TextDocumentSync int `json:"textDocumentSync"` + CompletionProvider map[string]any `json:"completionProvider,omitempty"` + HoverProvider bool `json:"hoverProvider,omitempty"` + DefinitionProvider bool `json:"definitionProvider,omitempty"` + ReferencesProvider bool `json:"referencesProvider,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 { diff --git a/lsp/server.go b/lsp/server.go index 55ab5e3..56e34e8 100644 --- a/lsp/server.go +++ b/lsp/server.go @@ -116,10 +116,14 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) { case "initialize": s.respond(msg.ID, initializeResult{ Capabilities: ServerCapabilities{ - TextDocumentSync: 1, // full sync - CompletionProvider: map[string]any{}, - HoverProvider: true, - DocumentSymbolProvider: true, + TextDocumentSync: 1, // full sync + CompletionProvider: map[string]any{}, + HoverProvider: true, + DefinitionProvider: true, + ReferencesProvider: true, + RenameProvider: true, + DocumentSymbolProvider: true, + DocumentFormattingProvider: true, SemanticTokensProvider: map[string]any{ "legend": SemanticTokensLegend{ TokenTypes: semanticTokenTypes, @@ -127,6 +131,7 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) { }, "full": true, }, + InlayHintProvider: true, }, 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) 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": var p semanticTokensParams json.Unmarshal(msg.Params, &p)