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]
|
||||
|
||||
Reference in New Issue
Block a user