feat(lsp): add code actions, signature help, and document highlights
Assisted-by: MiMo V2.5 Pro
This commit is contained in:
+196
-1
@@ -256,7 +256,202 @@ func (s *Server) inlayHints(p inlayHintParams) []InlayHint {
|
||||
return out
|
||||
}
|
||||
|
||||
// formatFile produces a canonical formatting of a parsed file.
|
||||
// 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":
|
||||
// Offer to add RET at the end of the function.
|
||||
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
|
||||
}
|
||||
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
|
||||
endChar := len([]rune(lines[endLine]))
|
||||
actions = append(actions, CodeAction{
|
||||
Title: "Add RET",
|
||||
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
|
||||
endChar := len([]rune(lines[endLine]))
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
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.
|
||||
if t.Name.Name == word {
|
||||
out = append(out, DocumentHighlight{
|
||||
Range: symRange(t.Name),
|
||||
Kind: highlightWrite,
|
||||
})
|
||||
}
|
||||
for _, stmt := range t.Body {
|
||||
switch st := stmt.(type) {
|
||||
case *ast.Label:
|
||||
if st.Name.Text == word {
|
||||
out = append(out, DocumentHighlight{
|
||||
Range: tokenRange(st.Name),
|
||||
Kind: highlightWrite,
|
||||
})
|
||||
}
|
||||
case *ast.Instr:
|
||||
for _, op := range st.Operands {
|
||||
if op.Addr.Sym != nil && op.Addr.Sym.Name == word {
|
||||
out = append(out, DocumentHighlight{
|
||||
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)},
|
||||
},
|
||||
Kind: highlightRead,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// documentSymbols returns functions and their labels, plus global symbols.
|
||||
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
|
||||
text := s.docs[p.TextDocument.URI]
|
||||
|
||||
@@ -309,11 +309,75 @@ type ServerCapabilities struct {
|
||||
RenameProvider bool `json:"renameProvider,omitempty"`
|
||||
DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"`
|
||||
DocumentFormattingProvider bool `json:"documentFormattingProvider,omitempty"`
|
||||
DocumentHighlightProvider bool `json:"documentHighlightProvider,omitempty"`
|
||||
CodeActionProvider bool `json:"codeActionProvider,omitempty"`
|
||||
SignatureHelpProvider map[string]any `json:"signatureHelpProvider,omitempty"`
|
||||
SemanticTokensProvider map[string]any `json:"semanticTokensProvider,omitempty"`
|
||||
InlayHintProvider bool `json:"inlayHintProvider,omitempty"`
|
||||
DiagnosticProvider map[string]any `json:"diagnosticProvider,omitempty"`
|
||||
}
|
||||
|
||||
// --- code actions -------------------------------------------------------------
|
||||
|
||||
type codeActionParams struct {
|
||||
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||
Range Range `json:"range"`
|
||||
Context codeActionContext `json:"context"`
|
||||
}
|
||||
|
||||
type codeActionContext struct {
|
||||
Diagnostics []Diagnostic `json:"diagnostics"`
|
||||
}
|
||||
|
||||
// CodeAction represents a quick fix or refactoring.
|
||||
type CodeAction struct {
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Edit *WorkspaceEdit `json:"edit,omitempty"`
|
||||
}
|
||||
|
||||
// --- signature help -----------------------------------------------------------
|
||||
|
||||
type signatureHelpParams struct {
|
||||
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||
Position Position `json:"position"`
|
||||
}
|
||||
|
||||
// SignatureHelp represents signature information for a function call.
|
||||
type SignatureHelp struct {
|
||||
Signatures []SignatureInformation `json:"signatures"`
|
||||
}
|
||||
|
||||
// SignatureInformation represents a function signature.
|
||||
type SignatureInformation struct {
|
||||
Label string `json:"label"`
|
||||
Documentation string `json:"documentation,omitempty"`
|
||||
Parameters []ParameterInformation `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
// ParameterInformation represents a parameter in a signature.
|
||||
type ParameterInformation struct {
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// --- document highlights ------------------------------------------------------
|
||||
|
||||
type documentHighlightParams struct {
|
||||
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||
Position Position `json:"position"`
|
||||
}
|
||||
|
||||
// DocumentHighlight represents a highlight range.
|
||||
type DocumentHighlight struct {
|
||||
Range Range `json:"range"`
|
||||
Kind int `json:"kind,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
highlightRead = 1
|
||||
highlightWrite = 2
|
||||
)
|
||||
|
||||
type initializeResult struct {
|
||||
Capabilities ServerCapabilities `json:"capabilities"`
|
||||
ServerInfo map[string]string `json:"serverInfo,omitempty"`
|
||||
|
||||
+19
-1
@@ -131,7 +131,10 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
|
||||
},
|
||||
"full": true,
|
||||
},
|
||||
InlayHintProvider: true,
|
||||
InlayHintProvider: true,
|
||||
DocumentHighlightProvider: true,
|
||||
CodeActionProvider: true,
|
||||
SignatureHelpProvider: map[string]any{"triggerCharacters": []string{"("}},
|
||||
},
|
||||
ServerInfo: map[string]string{"name": "gasm", "version": "0.31.1"},
|
||||
})
|
||||
@@ -205,6 +208,21 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
|
||||
json.Unmarshal(msg.Params, &p)
|
||||
s.respond(msg.ID, s.inlayHints(p))
|
||||
|
||||
case "textDocument/codeAction":
|
||||
var p codeActionParams
|
||||
json.Unmarshal(msg.Params, &p)
|
||||
s.respond(msg.ID, s.codeActions(p))
|
||||
|
||||
case "textDocument/signatureHelp":
|
||||
var p signatureHelpParams
|
||||
json.Unmarshal(msg.Params, &p)
|
||||
s.respond(msg.ID, s.signatureHelp(p))
|
||||
|
||||
case "textDocument/documentHighlight":
|
||||
var p documentHighlightParams
|
||||
json.Unmarshal(msg.Params, &p)
|
||||
s.respond(msg.ID, s.documentHighlights(p))
|
||||
|
||||
case "textDocument/semanticTokens/full":
|
||||
var p semanticTokensParams
|
||||
json.Unmarshal(msg.Params, &p)
|
||||
|
||||
@@ -373,6 +373,83 @@ func TestInlayHints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodeActions(t *testing.T) {
|
||||
badDoc := "#include \"textflag.h\"\n" +
|
||||
"TEXT ·foo(SB), NOSPLIT, $0\n" +
|
||||
"\tMOVQ AX, CX\n"
|
||||
in := session("file:///f_amd64.s", badDoc) +
|
||||
frame(10, "textDocument/codeAction", map[string]any{
|
||||
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||
"range": map[string]any{"start": map[string]any{"line": 0, "character": 0}, "end": map[string]any{"line": 2, "character": 0}},
|
||||
"context": map[string]any{"diagnostics": []map[string]any{{"code": "missing-ret", "range": map[string]any{"start": map[string]any{"line": 0, "character": 0}, "end": map[string]any{"line": 0, "character": 4}}}}},
|
||||
}) + frame(nil, "exit", nil)
|
||||
msgs := run(t, in)
|
||||
resp := findByID(msgs, 10)
|
||||
if resp == nil {
|
||||
t.Fatal("no codeAction response")
|
||||
}
|
||||
var actions []CodeAction
|
||||
if err := json.Unmarshal(mustResult(t, resp), &actions); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(actions) == 0 {
|
||||
t.Fatal("want at least 1 code action for missing-ret")
|
||||
}
|
||||
if actions[0].Title != "Add RET" {
|
||||
t.Errorf("action title = %q, want Add RET", actions[0].Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureHelp(t *testing.T) {
|
||||
doc := "#include \"textflag.h\"\n" +
|
||||
"TEXT ·add(SB), NOSPLIT, $0-24\n" +
|
||||
"\tMOVQ AX, CX\n" +
|
||||
"\tRET\n" +
|
||||
"TEXT ·caller(SB), NOSPLIT, $0\n" +
|
||||
"\tCALL ·add(SB)\n" +
|
||||
"\tRET\n"
|
||||
in := session("file:///f_amd64.s", doc) +
|
||||
frame(11, "textDocument/signatureHelp", map[string]any{
|
||||
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||
"position": map[string]any{"line": 5, "character": 6},
|
||||
}) + frame(nil, "exit", nil)
|
||||
msgs := run(t, in)
|
||||
resp := findByID(msgs, 11)
|
||||
if resp == nil {
|
||||
t.Fatal("no signatureHelp response")
|
||||
}
|
||||
var sh SignatureHelp
|
||||
if err := json.Unmarshal(mustResult(t, resp), &sh); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sh.Signatures) == 0 {
|
||||
t.Fatal("want at least 1 signature")
|
||||
}
|
||||
if !strings.Contains(sh.Signatures[0].Label, "add") {
|
||||
t.Errorf("signature label = %q, want add", sh.Signatures[0].Label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentHighlights(t *testing.T) {
|
||||
in := session("file:///f_amd64.s", cleanDoc) +
|
||||
frame(12, "textDocument/documentHighlight", map[string]any{
|
||||
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||
"position": map[string]any{"line": 4, "character": 5},
|
||||
}) + frame(nil, "exit", nil)
|
||||
msgs := run(t, in)
|
||||
resp := findByID(msgs, 12)
|
||||
if resp == nil {
|
||||
t.Fatal("no documentHighlight response")
|
||||
}
|
||||
var highlights []DocumentHighlight
|
||||
if err := json.Unmarshal(mustResult(t, resp), &highlights); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(highlights) < 2 {
|
||||
t.Fatalf("want at least 2 highlights (definition + use), got %d", len(highlights))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMethodNotFound(t *testing.T) {
|
||||
in := frame(9, "bogus/method", map[string]any{}) + frame(nil, "exit", nil)
|
||||
msgs := run(t, in)
|
||||
|
||||
Reference in New Issue
Block a user