feat(lsp): add code actions, signature help, and document highlights

Assisted-by: MiMo V2.5 Pro
This commit is contained in:
2026-08-21 01:20:14 +02:00
parent 1bf95a169e
commit f153be8158
4 changed files with 356 additions and 2 deletions
+196 -1
View File
@@ -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]