Files
gasm-sdk/lsp/handlers.go
T
2026-08-30 21:42:12 +02:00

883 lines
25 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package lsp
import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"unicode"
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/format"
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
"sourcedock.dev/petrbalvin/gasm-devkit/token"
)
// textflagMacros are the flag names defined by textflag.h; they are highlighted
// as macros and offered as completions after a TEXT/GLOBL directive.
var textflagMacros = map[string]bool{
"NOPROFILE": true, "DUPOK": true, "NOSPLIT": true, "RODATA": true,
"NOPTR": true, "WRAPPER": true, "NEEDCTXT": true, "TOPFRAME": true,
"LEAF": true, "ABI0": true, "REFLECTDATA": true,
}
// completion builds the completion list for a document.
func (s *Server) completion(p completionParams) []CompletionItem {
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
tab := a
items := []CompletionItem{
{Label: "TEXT", Kind: ciKeyword, Detail: "define a function"},
{Label: "DATA", Kind: ciKeyword, Detail: "initialise a data symbol"},
{Label: "GLOBL", Kind: ciKeyword, Detail: "declare a global symbol"},
}
for name := range textflagMacros {
items = append(items, CompletionItem{Label: name, Kind: ciKeyword, Detail: "textflag.h flag"})
}
for name, desc := range map[string]string{
"FP": "frame pointer (arguments/results)", "SP": "stack pointer",
"SB": "static base (globals)", "PC": "program counter",
} {
items = append(items, CompletionItem{Label: name, Kind: ciConstant, Detail: desc})
}
for _, in := range tab.Instructions() {
items = append(items, CompletionItem{
Label: in.Name, Kind: ciFunction, Detail: in.Summary, Documentation: in.Summary,
})
}
for _, r := range tab.Registers() {
kind := ciVariable
if r.Class == arch.Vector || r.Class == arch.Mask || r.Class == arch.Float || r.Class == arch.VecARM {
kind = ciClass
}
items = append(items, CompletionItem{Label: r.Name, Kind: kind, Detail: r.Desc})
}
// Local labels defined in the document.
if f, _ := parser.Parse("", s.docs[p.TextDocument.URI]); f != nil {
for _, name := range labelNames(f) {
items = append(items, CompletionItem{Label: name, Kind: ciModule, Detail: "local label"})
}
}
sort.Slice(items, func(i, j int) bool { return items[i].Label < items[j].Label })
return items
}
// hover returns documentation for the symbol under the cursor.
func (s *Server) hover(p hoverParams) *Hover {
text := s.docs[p.TextDocument.URI]
word, rng := wordAt(text, p.Position)
if word == "" {
return nil
}
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
var md string
if in, ok := a.Lookup(word); ok {
md = "**" + in.Name + "** — " + in.Summary
} else if r, ok := a.Register(word); ok {
md = "**" + r.Name + "** — " + r.Class.String() + " register. " + r.Desc
} else if desc, ok := arch.PseudoRegDesc(word); ok {
md = "**" + strings.ToUpper(word) + "** — pseudo-register. " + desc
} else if textflagMacros[strings.ToUpper(word)] {
md = "**" + strings.ToUpper(word) + "** — textflag.h flag"
} else {
return nil
}
return &Hover{
Contents: markupContent{Kind: "markdown", Value: md},
Range: rng,
}
}
// definition returns the location of the label definition for a label reference.
func (s *Server) definition(p definitionParams) []Location {
text := s.docs[p.TextDocument.URI]
word, _ := wordAt(text, p.Position)
if word == "" {
return nil
}
// Parse the document to find label definitions.
f, errs := parser.Parse(uriPath(p.TextDocument.URI), text)
if f == nil || len(errs) > 0 {
return nil
}
// Find the label definition.
for _, d := range f.Decls {
if t, ok := d.(*ast.Text); ok {
for _, stmt := range t.Body {
if lbl, ok := stmt.(*ast.Label); ok {
if lbl.Name.Text == word {
return []Location{{
URI: p.TextDocument.URI,
Range: Range{
Start: Position{Line: lbl.Name.Pos.Line - 1, Character: lbl.Name.Pos.Column - 1},
End: Position{Line: lbl.Name.Pos.Line - 1, Character: lbl.Name.Pos.Column - 1 + len(word)},
},
}}
}
}
}
}
}
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
}
formatted := format.Source(text)
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
}
// 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
}
// workspaceSymbols searches all open documents for symbols matching the query.
func (s *Server) workspaceSymbols(p workspaceSymbolParams) []WorkspaceSymbol {
if p.Query == "" {
return nil
}
query := strings.ToLower(p.Query)
var out []WorkspaceSymbol
for uri, text := range s.docs {
f, _ := parser.Parse(uriPath(uri), text)
if f == nil {
continue
}
for _, d := range f.Decls {
switch dd := d.(type) {
case *ast.Text:
if strings.Contains(strings.ToLower(dd.Name.Name), query) {
out = append(out, WorkspaceSymbol{
Name: dd.Name.Name,
Kind: symFunction,
Location: Location{URI: uri, Range: symRange(dd.Name)},
})
}
case *ast.Globl:
if dd.Name != nil && strings.Contains(strings.ToLower(dd.Name.Name), query) {
out = append(out, WorkspaceSymbol{
Name: dd.Name.Name,
Kind: symConstant,
Location: Location{URI: uri, Range: symRange(dd.Name)},
})
}
case *ast.Data:
if dd.Name != nil && strings.Contains(strings.ToLower(dd.Name.Name), query) {
out = append(out, WorkspaceSymbol{
Name: dd.Name.Name,
Kind: symConstant,
Location: Location{URI: uri, Range: symRange(dd.Name)},
})
}
}
}
}
return out
}
// documentSymbols returns functions and their labels, plus global symbols.
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
text := s.docs[p.TextDocument.URI]
f, _ := parser.Parse(uriPath(p.TextDocument.URI), text)
if f == nil {
return nil
}
var out []DocumentSymbol
for _, d := range f.Decls {
switch dd := d.(type) {
case *ast.Text:
sym := DocumentSymbol{
Name: dd.Name.Name,
Detail: "TEXT " + strings.Join(dd.Flags, " "),
Kind: symFunction,
Range: textRange(dd),
SelectionRange: symRange(dd.Name),
}
for _, st := range dd.Body {
if l, ok := st.(*ast.Label); ok {
sym.Children = append(sym.Children, DocumentSymbol{
Name: l.Name.Text,
Kind: symVariable,
Range: tokenRange(l.Name),
SelectionRange: tokenRange(l.Name),
})
}
}
out = append(out, sym)
case *ast.Globl:
out = append(out, DocumentSymbol{
Name: dd.Name.Name, Detail: "GLOBL", Kind: symConstant,
Range: symRange(dd.Name), SelectionRange: symRange(dd.Name),
})
case *ast.Data:
out = append(out, DocumentSymbol{
Name: dd.Name.Name, Detail: "DATA", Kind: symConstant,
Range: symRange(dd.Name), SelectionRange: symRange(dd.Name),
})
}
}
return out
}
// semTok is one classified token before delta encoding.
type semTok struct {
line, char, length, typ int
}
// semanticTokens encodes syntax highlighting as LSP semantic tokens.
func (s *Server) semanticTokens(p semanticTokensParams) SemanticTokens {
text := s.docs[p.TextDocument.URI]
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
f, _ := parser.Parse("", text)
labels := map[string]bool{}
for _, name := range labelNames(f) {
labels[name] = true
}
toks := lexer.Tokenize(text)
lines := groupLines(toks)
var encoded []semTok
for _, line := range lines {
encoded = append(encoded, classifyLine(line, a, labels)...)
}
return SemanticTokens{Data: deltaEncode(encoded)}
}
// classifyLine assigns a semantic token type to each significant token on a line.
func classifyLine(line []token.Token, a *arch.Table, labels map[string]bool) []semTok {
if len(line) == 0 {
return nil
}
var out []semTok
first := firstSignificant(line)
if first < 0 {
return nil
}
isDirective := line[first].Kind == token.Ident &&
(line[first].Text == "TEXT" || line[first].Text == "DATA" || line[first].Text == "GLOBL")
isLabel := line[first].Kind == token.Ident && first+1 < len(line) &&
line[first+1].Kind == token.Colon
isInstr := !isDirective && !isLabel && line[first].Kind == token.Ident
mnemonicDone := false
for i, t := range line {
typ := -1
switch t.Kind {
case token.Comment:
typ = stComment
case token.Number:
typ = stNumber
case token.String, token.Rune:
typ = stString
case token.Hash:
typ = stMacro
case token.Ident:
typ = classifyIdent(i, first, t.Text, a, labels, isDirective, isLabel, isInstr, &mnemonicDone)
case token.Colon, token.Comma, token.LParen, token.RParen,
token.Plus, token.Minus, token.Star, token.Slash, token.Dollar,
token.LAngle, token.RAngle, token.LShift, token.RShift, token.Arrow, token.At:
typ = stOperator
}
if typ >= 0 {
out = append(out, semTok{
line: t.Pos.Line - 1,
char: t.Pos.Column - 1,
length: runeLen(t.Text),
typ: typ,
})
}
}
return out
}
// classifyIdent decides the semantic type of an identifier token.
func classifyIdent(i, first int, text string, a *arch.Table, labels map[string]bool, isDirective, isLabel, isInstr bool, mnemonicDone *bool) int {
upper := strings.ToUpper(text)
switch {
case isDirective && i == first:
return stKeyword
case isDirective && textflagMacros[upper]:
return stMacro
case isLabel && i == first:
return stNamespace
case arch.IsPseudoReg(text):
return stProperty
case labels[text]:
return stNamespace
}
if r, ok := a.Register(text); ok {
switch r.Class {
case arch.Vector, arch.Mask, arch.Float, arch.VecARM:
return stType
default:
return stVariable
}
}
if isInstr && i == first && !*mnemonicDone {
*mnemonicDone = true
return stFunction
}
// Argument/symbol names and anything else.
return stVariable
}
// deltaEncode converts absolute token positions to the LSP relative encoding.
func deltaEncode(toks []semTok) []int {
data := make([]int, 0, len(toks)*5)
prevLine, prevChar := 0, 0
for _, t := range toks {
dLine := t.line - prevLine
dChar := t.char
if dLine == 0 {
dChar = t.char - prevChar
}
data = append(data, dLine, dChar, t.length, t.typ, 0)
prevLine, prevChar = t.line, t.char
}
return data
}
// --- shared helpers ---------------------------------------------------------
// wordAt extracts the identifier surrounding pos and its range.
func wordAt(text string, pos Position) (string, Range) {
lines := strings.Split(text, "\n")
if pos.Line < 0 || pos.Line >= len(lines) {
return "", Range{}
}
runes := []rune(lines[pos.Line])
col := pos.Character
if col < 0 || col > len(runes) {
return "", Range{}
}
isWord := func(r rune) bool {
return r == '_' || r == '\u00B7' || unicode.IsLetter(r) || unicode.IsDigit(r)
}
start, end := col, col
for start > 0 && isWord(runes[start-1]) {
start--
}
for end < len(runes) && isWord(runes[end]) {
end++
}
if start == end {
return "", Range{}
}
rng := Range{
Start: Position{Line: pos.Line, Character: start},
End: Position{Line: pos.Line, Character: end},
}
return string(runes[start:end]), rng
}
// labelNames collects every label defined in a file.
func labelNames(f *ast.File) []string {
if f == nil {
return nil
}
seen := map[string]bool{}
var out []string
collect := func(body []ast.Stmt) {
for _, st := range body {
if l, ok := st.(*ast.Label); ok && !seen[l.Name.Text] {
seen[l.Name.Text] = true
out = append(out, l.Name.Text)
}
}
}
for _, d := range f.Decls {
if t, ok := d.(*ast.Text); ok {
collect(t.Body)
}
}
collect(f.Orphans)
return out
}
// groupLines splits a token stream into lines, keeping Newline boundaries but
// dropping the Newline and EOF tokens themselves.
func groupLines(toks []token.Token) [][]token.Token {
var lines [][]token.Token
var cur []token.Token
for _, t := range toks {
if t.Kind == token.EOF {
break
}
if t.Kind == token.Newline {
lines = append(lines, cur)
cur = nil
continue
}
cur = append(cur, t)
}
if len(cur) > 0 {
lines = append(lines, cur)
}
return lines
}
func firstSignificant(line []token.Token) int {
for i, t := range line {
if t.Kind != token.Comment {
return i
}
}
return -1
}
func runeLen(s string) int { return len([]rune(s)) }
// symRange builds a range covering a symbol from its position and raw text.
func symRange(sym *ast.Symbol) Range {
start := Position{Line: sym.Pos.Line - 1, Character: sym.Pos.Column - 1}
end := start
end.Character += runeLen(sym.Name)
return Range{Start: start, End: end}
}
// tokenRange builds a range covering one token.
func tokenRange(t token.Token) Range {
return Range{
Start: Position{Line: t.Pos.Line - 1, Character: t.Pos.Column - 1},
End: Position{Line: t.End.Line - 1, Character: t.End.Column - 1},
}
}
// textRange spans a TEXT function from its keyword to the end of its body.
func textRange(t *ast.Text) Range {
start := Position{Line: t.Keyword.Pos.Line - 1, Character: t.Keyword.Pos.Column - 1}
end := start
end.Character += runeLen(t.Keyword.Text)
if n := len(t.Body); n > 0 {
last := t.Body[n-1]
if in, ok := last.(*ast.Instr); ok {
end = Position{Line: in.Mnemonic.End.Line - 1, Character: in.Mnemonic.End.Column - 1}
} else {
lp := last.Pos()
end = Position{Line: lp.Line - 1, Character: lp.Column - 1}
}
}
return Range{Start: start, End: end}
}
// diagnosticsFor computes the LSP diagnostics of one document; the push
// (publishDiagnostics) and pull (textDocument/diagnostic) paths share it.
func (s *Server) diagnosticsFor(uri string) []Diagnostic {
text := s.docs[uri]
f, _ := parser.Parse(uriPath(uri), text)
cfg := lint.Config{Arch: arch.FromFilename(uriPath(uri))}
diags := lint.File(f, cfg)
out := make([]Diagnostic, 0, len(diags))
for _, d := range diags {
out = append(out, Diagnostic{
Range: toRange(d.Pos.Line, d.Pos.Column, d.End),
Severity: lintSeverity(d.Severity),
Code: d.Code,
Source: "gasm",
Message: d.Message,
})
}
return out
}
// includeRe matches a #include directive's quoted path.
var includeRe = regexp.MustCompile(`^\s*#include\s+"([^"]+)"`)
// documentLinks links each #include path to the header file it resolves to:
// first relative to the document's directory, then in $GOROOT/pkg/include
// (where textflag.h and friends live). Unresolvable paths get no link.
func (s *Server) documentLinks(uri string) []DocumentLink {
text := s.docs[uri]
if text == "" {
return nil
}
docDir := filepath.Dir(uriPath(uri))
goroot := runtime.GOROOT()
var out []DocumentLink
for i, line := range strings.Split(text, "\n") {
m := includeRe.FindStringSubmatch(line)
if m == nil {
continue
}
target := resolveInclude(docDir, goroot, m[1])
if target == "" {
continue
}
start := strings.Index(line, "\"")
out = append(out, DocumentLink{
Range: Range{
Start: Position{Line: i, Character: start},
End: Position{Line: i, Character: start + len(m[1]) + 2},
},
Target: "file://" + target,
})
}
return out
}
// resolveInclude returns the existing file a #include path refers to, or "".
func resolveInclude(docDir, goroot, path string) string {
for _, dir := range []string{docDir, filepath.Join(goroot, "pkg", "include")} {
cand := filepath.Join(dir, path)
if _, err := os.Stat(cand); err == nil {
return cand
}
}
return ""
}
// foldingRanges marks each TEXT function's body as a collapsible region.
func (s *Server) foldingRanges(uri string) []FoldingRange {
text := s.docs[uri]
f, _ := parser.Parse(uriPath(uri), text)
if f == nil {
return nil
}
var out []FoldingRange
for _, d := range f.Decls {
t, ok := d.(*ast.Text)
if !ok || len(t.Body) == 0 {
continue
}
start := t.Keyword.Pos.Line - 1
end := t.Body[len(t.Body)-1].Pos().Line - 1
if end <= start {
continue
}
out = append(out, FoldingRange{StartLine: start, EndLine: end, Kind: "region"})
}
return out
}