fix(lsp): parse-error survival, symbol ranges and UTF-16 positions
Assisted-by: GLM 5.3
This commit is contained in:
+180
-54
@@ -4,14 +4,17 @@
|
|||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
@@ -67,7 +70,7 @@ func (s *Server) completion(p completionParams) []CompletionItem {
|
|||||||
items = append(items, CompletionItem{Label: name, Kind: ciModule, Detail: "local label"})
|
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 })
|
slices.SortFunc(items, func(a, b CompletionItem) int { return cmp.Compare(a.Label, b.Label) })
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +97,7 @@ func (s *Server) hover(p hoverParams) *Hover {
|
|||||||
}
|
}
|
||||||
return &Hover{
|
return &Hover{
|
||||||
Contents: markupContent{Kind: "markdown", Value: md},
|
Contents: markupContent{Kind: "markdown", Value: md},
|
||||||
Range: rng,
|
Range: clientRange(text, rng),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +115,7 @@ func (s *Server) openASTs() []openAST {
|
|||||||
for uri := range s.docs {
|
for uri := range s.docs {
|
||||||
uris = append(uris, uri)
|
uris = append(uris, uri)
|
||||||
}
|
}
|
||||||
sort.Strings(uris)
|
slices.Sort(uris)
|
||||||
out := make([]openAST, 0, len(uris))
|
out := make([]openAST, 0, len(uris))
|
||||||
for _, uri := range uris {
|
for _, uri := range uris {
|
||||||
if f, _ := parser.Parse(uriPath(uri), s.docs[uri]); f != nil {
|
if f, _ := parser.Parse(uriPath(uri), s.docs[uri]); f != nil {
|
||||||
@@ -143,11 +146,8 @@ func (s *Server) definition(p definitionParams) []Location {
|
|||||||
if lbl, ok := stmt.(*ast.Label); ok {
|
if lbl, ok := stmt.(*ast.Label); ok {
|
||||||
if lbl.Name.Text == name || lbl.Name.Text == word {
|
if lbl.Name.Text == name || lbl.Name.Text == word {
|
||||||
return []Location{{
|
return []Location{{
|
||||||
URI: p.TextDocument.URI,
|
URI: p.TextDocument.URI,
|
||||||
Range: Range{
|
Range: clientRange(text, tokenRange(lbl.Name)),
|
||||||
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)},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,11 +160,11 @@ func (s *Server) definition(p definitionParams) []Location {
|
|||||||
for _, of := range s.openASTs() {
|
for _, of := range s.openASTs() {
|
||||||
for _, d := range of.file.Decls {
|
for _, d := range of.file.Decls {
|
||||||
t, ok := d.(*ast.Text)
|
t, ok := d.(*ast.Text)
|
||||||
if !ok || t.Name == nil {
|
if !ok || !realSymbol(t.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if t.Name.Name == name {
|
if t.Name.Name == name {
|
||||||
return []Location{{URI: of.uri, Range: symRange(t.Name)}}
|
return []Location{{URI: of.uri, Range: clientRange(s.docs[of.uri], symRange(t.Name))}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,18 +190,18 @@ func (s *Server) references(p referenceParams) []Location {
|
|||||||
sameDoc := of.uri == uri
|
sameDoc := of.uri == uri
|
||||||
for _, d := range of.file.Decls {
|
for _, d := range of.file.Decls {
|
||||||
t, ok := d.(*ast.Text)
|
t, ok := d.(*ast.Text)
|
||||||
if !ok || t.Name == nil {
|
if !ok || !realSymbol(t.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Include the definition if requested.
|
// Include the definition if requested.
|
||||||
if p.Context.IncludeDeclaration && t.Name.Name == name {
|
if p.Context.IncludeDeclaration && t.Name.Name == name {
|
||||||
out = append(out, Location{URI: of.uri, Range: symRange(t.Name)})
|
out = append(out, Location{URI: of.uri, Range: clientRange(s.docs[of.uri], symRange(t.Name))})
|
||||||
}
|
}
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
switch st := stmt.(type) {
|
switch st := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
if sameDoc && st.Name.Text == name {
|
if sameDoc && st.Name.Text == name {
|
||||||
out = append(out, Location{URI: of.uri, Range: tokenRange(st.Name)})
|
out = append(out, Location{URI: of.uri, Range: clientRange(s.docs[of.uri], tokenRange(st.Name))})
|
||||||
}
|
}
|
||||||
case *ast.Instr:
|
case *ast.Instr:
|
||||||
for _, op := range st.Operands {
|
for _, op := range st.Operands {
|
||||||
@@ -211,12 +211,15 @@ func (s *Server) references(p referenceParams) []Location {
|
|||||||
if !sameDoc && op.Addr.Sym.Pseudo != "SB" {
|
if !sameDoc && op.Addr.Sym.Pseudo != "SB" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// The range covers the operand's verbatim
|
||||||
|
// identifier, `·`/package prefix included, so a
|
||||||
|
// rename replaces the whole spelling.
|
||||||
out = append(out, Location{
|
out = append(out, Location{
|
||||||
URI: of.uri,
|
URI: of.uri,
|
||||||
Range: Range{
|
Range: clientRange(s.docs[of.uri], Range{
|
||||||
Start: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1},
|
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(name)},
|
End: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1 + symIdentLen(op.Addr.Sym)},
|
||||||
},
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,7 +268,7 @@ func (s *Server) documentFormatting(p documentFormattingParams) []TextEdit {
|
|||||||
endLine := len(lines) - 1
|
endLine := len(lines) - 1
|
||||||
endChar := 0
|
endChar := 0
|
||||||
if endLine >= 0 {
|
if endLine >= 0 {
|
||||||
endChar = len([]rune(lines[endLine]))
|
endChar = utf16Len(lines[endLine])
|
||||||
}
|
}
|
||||||
return []TextEdit{{
|
return []TextEdit{{
|
||||||
Range: Range{Start: Position{Line: 0, Character: 0}, End: Position{Line: endLine, Character: endChar}},
|
Range: Range{Start: Position{Line: 0, Character: 0}, End: Position{Line: endLine, Character: endChar}},
|
||||||
@@ -289,9 +292,10 @@ func (s *Server) inlayHints(p inlayHintParams) []InlayHint {
|
|||||||
// Hint after the args size: show frame size.
|
// Hint after the args size: show frame size.
|
||||||
if t.Frame != nil && t.Frame.Imm.HasVal && t.Args != nil && t.Args.Imm.HasVal {
|
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.
|
// Place hint after the args operand using its raw text length.
|
||||||
col := t.Args.Pos.Column - 1 + len(t.Args.Raw)
|
line := t.Args.Pos.Line - 1
|
||||||
|
col := t.Args.Pos.Column - 1 + runeLen(t.Args.Raw)
|
||||||
out = append(out, InlayHint{
|
out = append(out, InlayHint{
|
||||||
Position: Position{Line: t.Args.Pos.Line - 1, Character: col},
|
Position: Position{Line: line, Character: utf16Column(lineAt(text, line), col)},
|
||||||
Label: fmt.Sprintf(" frame=%d", t.Frame.Imm.Val),
|
Label: fmt.Sprintf(" frame=%d", t.Frame.Imm.Val),
|
||||||
Kind: inlayHintTypeParameter,
|
Kind: inlayHintTypeParameter,
|
||||||
Tooltip: fmt.Sprintf("local frame size: %d bytes", t.Frame.Imm.Val),
|
Tooltip: fmt.Sprintf("local frame size: %d bytes", t.Frame.Imm.Val),
|
||||||
@@ -341,7 +345,7 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
|
|||||||
newLines = append(newLines, lines[insertLine:]...)
|
newLines = append(newLines, lines[insertLine:]...)
|
||||||
newText := strings.Join(newLines, "\n")
|
newText := strings.Join(newLines, "\n")
|
||||||
endLine := len(lines) - 1
|
endLine := len(lines) - 1
|
||||||
endChar := len([]rune(lines[endLine]))
|
endChar := utf16Len(lines[endLine])
|
||||||
actions = append(actions, CodeAction{
|
actions = append(actions, CodeAction{
|
||||||
Title: "Add RET to " + t.Name.Name,
|
Title: "Add RET to " + t.Name.Name,
|
||||||
Kind: "quickfix",
|
Kind: "quickfix",
|
||||||
@@ -367,7 +371,7 @@ func (s *Server) codeActions(p codeActionParams) []CodeAction {
|
|||||||
newLines = append(newLines, lines[i+1:]...)
|
newLines = append(newLines, lines[i+1:]...)
|
||||||
newText := strings.Join(newLines, "\n")
|
newText := strings.Join(newLines, "\n")
|
||||||
endLine := len(lines) - 1
|
endLine := len(lines) - 1
|
||||||
endChar := len([]rune(lines[endLine]))
|
endChar := utf16Len(lines[endLine])
|
||||||
actions = append(actions, CodeAction{
|
actions = append(actions, CodeAction{
|
||||||
Title: "Remove unused label \"" + label + "\"",
|
Title: "Remove unused label \"" + label + "\"",
|
||||||
Kind: "quickfix",
|
Kind: "quickfix",
|
||||||
@@ -458,6 +462,10 @@ func (s *Server) documentHighlights(p documentHighlightParams) []DocumentHighlig
|
|||||||
if word == "" {
|
if word == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// Symbol names are stored without the middle dot, while the word under
|
||||||
|
// the cursor keeps it; trim once and compare the trimmed form, the way
|
||||||
|
// references and definition do.
|
||||||
|
name := strings.TrimPrefix(word, "\u00B7")
|
||||||
f, errs := parser.Parse(uriPath(p.TextDocument.URI), text)
|
f, errs := parser.Parse(uriPath(p.TextDocument.URI), text)
|
||||||
if f == nil || len(errs) > 0 {
|
if f == nil || len(errs) > 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -469,29 +477,29 @@ func (s *Server) documentHighlights(p documentHighlightParams) []DocumentHighlig
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Highlight the definition.
|
// Highlight the definition.
|
||||||
if t.Name.Name == word {
|
if t.Name != nil && t.Name.Name == name {
|
||||||
out = append(out, DocumentHighlight{
|
out = append(out, DocumentHighlight{
|
||||||
Range: symRange(t.Name),
|
Range: clientRange(text, symRange(t.Name)),
|
||||||
Kind: highlightWrite,
|
Kind: highlightWrite,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
switch st := stmt.(type) {
|
switch st := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
if st.Name.Text == word {
|
if st.Name.Text == name {
|
||||||
out = append(out, DocumentHighlight{
|
out = append(out, DocumentHighlight{
|
||||||
Range: tokenRange(st.Name),
|
Range: clientRange(text, tokenRange(st.Name)),
|
||||||
Kind: highlightWrite,
|
Kind: highlightWrite,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case *ast.Instr:
|
case *ast.Instr:
|
||||||
for _, op := range st.Operands {
|
for _, op := range st.Operands {
|
||||||
if op.Addr.Sym != nil && op.Addr.Sym.Name == word {
|
if op.Addr.Sym != nil && op.Addr.Sym.Name == name {
|
||||||
out = append(out, DocumentHighlight{
|
out = append(out, DocumentHighlight{
|
||||||
Range: Range{
|
Range: clientRange(text, Range{
|
||||||
Start: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1},
|
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)},
|
End: Position{Line: op.Pos.Line - 1, Character: op.Pos.Column - 1 + symIdentLen(op.Addr.Sym)},
|
||||||
},
|
}),
|
||||||
Kind: highlightRead,
|
Kind: highlightRead,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -517,11 +525,17 @@ func (s *Server) workspaceSymbols(p workspaceSymbolParams) []WorkspaceSymbol {
|
|||||||
for _, d := range f.Decls {
|
for _, d := range f.Decls {
|
||||||
switch dd := d.(type) {
|
switch dd := d.(type) {
|
||||||
case *ast.Text:
|
case *ast.Text:
|
||||||
|
// A malformed TEXT line is kept in the tree under a "?"
|
||||||
|
// placeholder; it is not a symbol, and skipping it keeps
|
||||||
|
// the answer serving the rest of a mid-edit buffer.
|
||||||
|
if !realSymbol(dd.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if strings.Contains(strings.ToLower(dd.Name.Name), query) {
|
if strings.Contains(strings.ToLower(dd.Name.Name), query) {
|
||||||
out = append(out, WorkspaceSymbol{
|
out = append(out, WorkspaceSymbol{
|
||||||
Name: dd.Name.Name,
|
Name: dd.Name.Name,
|
||||||
Kind: symFunction,
|
Kind: symFunction,
|
||||||
Location: Location{URI: uri, Range: symRange(dd.Name)},
|
Location: Location{URI: uri, Range: clientRange(text, symRange(dd.Name))},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case *ast.Globl:
|
case *ast.Globl:
|
||||||
@@ -529,7 +543,7 @@ func (s *Server) workspaceSymbols(p workspaceSymbolParams) []WorkspaceSymbol {
|
|||||||
out = append(out, WorkspaceSymbol{
|
out = append(out, WorkspaceSymbol{
|
||||||
Name: dd.Name.Name,
|
Name: dd.Name.Name,
|
||||||
Kind: symConstant,
|
Kind: symConstant,
|
||||||
Location: Location{URI: uri, Range: symRange(dd.Name)},
|
Location: Location{URI: uri, Range: clientRange(text, symRange(dd.Name))},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case *ast.Data:
|
case *ast.Data:
|
||||||
@@ -537,7 +551,7 @@ func (s *Server) workspaceSymbols(p workspaceSymbolParams) []WorkspaceSymbol {
|
|||||||
out = append(out, WorkspaceSymbol{
|
out = append(out, WorkspaceSymbol{
|
||||||
Name: dd.Name.Name,
|
Name: dd.Name.Name,
|
||||||
Kind: symConstant,
|
Kind: symConstant,
|
||||||
Location: Location{URI: uri, Range: symRange(dd.Name)},
|
Location: Location{URI: uri, Range: clientRange(text, symRange(dd.Name))},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,33 +571,44 @@ func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
|
|||||||
for _, d := range f.Decls {
|
for _, d := range f.Decls {
|
||||||
switch dd := d.(type) {
|
switch dd := d.(type) {
|
||||||
case *ast.Text:
|
case *ast.Text:
|
||||||
|
// As in workspaceSymbols: a "?" placeholder is not a symbol,
|
||||||
|
// and the remaining declarations are still listed.
|
||||||
|
if !realSymbol(dd.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
sym := DocumentSymbol{
|
sym := DocumentSymbol{
|
||||||
Name: dd.Name.Name,
|
Name: dd.Name.Name,
|
||||||
Detail: "TEXT " + strings.Join(dd.Flags, " "),
|
Detail: "TEXT " + strings.Join(dd.Flags, " "),
|
||||||
Kind: symFunction,
|
Kind: symFunction,
|
||||||
Range: textRange(dd),
|
Range: clientRange(text, textRange(dd)),
|
||||||
SelectionRange: symRange(dd.Name),
|
SelectionRange: clientRange(text, symRange(dd.Name)),
|
||||||
}
|
}
|
||||||
for _, st := range dd.Body {
|
for _, st := range dd.Body {
|
||||||
if l, ok := st.(*ast.Label); ok {
|
if l, ok := st.(*ast.Label); ok {
|
||||||
sym.Children = append(sym.Children, DocumentSymbol{
|
sym.Children = append(sym.Children, DocumentSymbol{
|
||||||
Name: l.Name.Text,
|
Name: l.Name.Text,
|
||||||
Kind: symVariable,
|
Kind: symVariable,
|
||||||
Range: tokenRange(l.Name),
|
Range: clientRange(text, tokenRange(l.Name)),
|
||||||
SelectionRange: tokenRange(l.Name),
|
SelectionRange: clientRange(text, tokenRange(l.Name)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out = append(out, sym)
|
out = append(out, sym)
|
||||||
case *ast.Globl:
|
case *ast.Globl:
|
||||||
|
if dd.Name == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
out = append(out, DocumentSymbol{
|
out = append(out, DocumentSymbol{
|
||||||
Name: dd.Name.Name, Detail: "GLOBL", Kind: symConstant,
|
Name: dd.Name.Name, Detail: "GLOBL", Kind: symConstant,
|
||||||
Range: symRange(dd.Name), SelectionRange: symRange(dd.Name),
|
Range: clientRange(text, symRange(dd.Name)), SelectionRange: clientRange(text, symRange(dd.Name)),
|
||||||
})
|
})
|
||||||
case *ast.Data:
|
case *ast.Data:
|
||||||
|
if dd.Name == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
out = append(out, DocumentSymbol{
|
out = append(out, DocumentSymbol{
|
||||||
Name: dd.Name.Name, Detail: "DATA", Kind: symConstant,
|
Name: dd.Name.Name, Detail: "DATA", Kind: symConstant,
|
||||||
Range: symRange(dd.Name), SelectionRange: symRange(dd.Name),
|
Range: clientRange(text, symRange(dd.Name)), SelectionRange: clientRange(text, symRange(dd.Name)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,10 +632,17 @@ func (s *Server) semanticTokens(p semanticTokensParams) SemanticTokens {
|
|||||||
|
|
||||||
toks := lexer.Tokenize(text)
|
toks := lexer.Tokenize(text)
|
||||||
lines := groupLines(toks)
|
lines := groupLines(toks)
|
||||||
|
srcLines := strings.Split(text, "\n")
|
||||||
|
|
||||||
var encoded []semTok
|
var encoded []semTok
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
encoded = append(encoded, classifyLine(line, a, labels)...)
|
for _, st := range classifyLine(line, a, labels) {
|
||||||
|
// Columns cross the protocol boundary in UTF-16 code units.
|
||||||
|
if st.line >= 0 && st.line < len(srcLines) {
|
||||||
|
st.char = utf16Column(srcLines[st.line], st.char)
|
||||||
|
}
|
||||||
|
encoded = append(encoded, st)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SemanticTokens{Data: deltaEncode(encoded)}
|
return SemanticTokens{Data: deltaEncode(encoded)}
|
||||||
@@ -649,14 +681,15 @@ func classifyLine(line []token.Token, a *arch.Table, labels map[string]bool) []s
|
|||||||
typ = classifyIdent(i, first, t.Text, a, labels, isDirective, isLabel, isInstr, &mnemonicDone)
|
typ = classifyIdent(i, first, t.Text, a, labels, isDirective, isLabel, isInstr, &mnemonicDone)
|
||||||
case token.Colon, token.Comma, token.LParen, token.RParen,
|
case token.Colon, token.Comma, token.LParen, token.RParen,
|
||||||
token.Plus, token.Minus, token.Star, token.Slash, token.Dollar,
|
token.Plus, token.Minus, token.Star, token.Slash, token.Dollar,
|
||||||
token.LAngle, token.RAngle, token.LShift, token.RShift, token.Arrow, token.At:
|
token.LAngle, token.RAngle, token.LShift, token.RShift, token.Arrow,
|
||||||
|
token.At, token.Pipe:
|
||||||
typ = stOperator
|
typ = stOperator
|
||||||
}
|
}
|
||||||
if typ >= 0 {
|
if typ >= 0 {
|
||||||
out = append(out, semTok{
|
out = append(out, semTok{
|
||||||
line: t.Pos.Line - 1,
|
line: t.Pos.Line - 1,
|
||||||
char: t.Pos.Column - 1,
|
char: t.Pos.Column - 1,
|
||||||
length: runeLen(t.Text),
|
length: utf16Len(t.Text),
|
||||||
typ: typ,
|
typ: typ,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -714,14 +747,16 @@ func deltaEncode(toks []semTok) []int {
|
|||||||
|
|
||||||
// --- shared helpers ---------------------------------------------------------
|
// --- shared helpers ---------------------------------------------------------
|
||||||
|
|
||||||
// wordAt extracts the identifier surrounding pos and its range.
|
// wordAt extracts the identifier surrounding pos and its range. The
|
||||||
|
// incoming character offset is UTF-16 code units, as LSP defines it, and is
|
||||||
|
// converted to the rune index the scanning works in.
|
||||||
func wordAt(text string, pos Position) (string, Range) {
|
func wordAt(text string, pos Position) (string, Range) {
|
||||||
lines := strings.Split(text, "\n")
|
lines := strings.Split(text, "\n")
|
||||||
if pos.Line < 0 || pos.Line >= len(lines) {
|
if pos.Line < 0 || pos.Line >= len(lines) {
|
||||||
return "", Range{}
|
return "", Range{}
|
||||||
}
|
}
|
||||||
runes := []rune(lines[pos.Line])
|
runes := []rune(lines[pos.Line])
|
||||||
col := pos.Character
|
col := runeColumn(lines[pos.Line], pos.Character)
|
||||||
if col < 0 || col > len(runes) {
|
if col < 0 || col > len(runes) {
|
||||||
return "", Range{}
|
return "", Range{}
|
||||||
}
|
}
|
||||||
@@ -802,14 +837,102 @@ func firstSignificant(line []token.Token) int {
|
|||||||
|
|
||||||
func runeLen(s string) int { return len([]rune(s)) }
|
func runeLen(s string) int { return len([]rune(s)) }
|
||||||
|
|
||||||
// symRange builds a range covering a symbol from its position and raw text.
|
// utf16Len returns the length of s in UTF-16 code units, the unit LSP
|
||||||
|
// positions count: an astral rune (an emoji in a comment) is two of them.
|
||||||
|
func utf16Len(s string) int {
|
||||||
|
n := 0
|
||||||
|
for _, r := range s {
|
||||||
|
n += utf16.RuneLen(r)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// utf16Column converts a rune-based column on line into a UTF-16 code-unit
|
||||||
|
// offset. Rune columns are the lexer's convention; code units are the
|
||||||
|
// protocol's, and the two diverge once an astral rune precedes the column.
|
||||||
|
func utf16Column(line string, col int) int {
|
||||||
|
if col <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
units := 0
|
||||||
|
seen := 0
|
||||||
|
for _, r := range line {
|
||||||
|
if seen >= col {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
units += utf16.RuneLen(r)
|
||||||
|
seen++
|
||||||
|
}
|
||||||
|
return units
|
||||||
|
}
|
||||||
|
|
||||||
|
// runeColumn converts a UTF-16 code-unit offset on line into a rune column,
|
||||||
|
// the inverse of utf16Column, applied to positions arriving from the client.
|
||||||
|
func runeColumn(line string, units int) int {
|
||||||
|
if units <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
col := 0
|
||||||
|
u := 0
|
||||||
|
for _, r := range line {
|
||||||
|
if u >= units {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
u += utf16.RuneLen(r)
|
||||||
|
col++
|
||||||
|
}
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
// lineAt returns the n-th zero-based line of text, or "" when out of range.
|
||||||
|
func lineAt(text string, n int) string {
|
||||||
|
if n < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
if n >= len(lines) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return lines[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientRange re-encodes a rune-based range (the columns the lexer, parser
|
||||||
|
// and the helpers above produce) in the UTF-16 code units LSP mandates.
|
||||||
|
// Every range leaving the server passes through here.
|
||||||
|
func clientRange(text string, r Range) Range {
|
||||||
|
return Range{
|
||||||
|
Start: Position{Line: r.Start.Line, Character: utf16Column(lineAt(text, r.Start.Line), r.Start.Character)},
|
||||||
|
End: Position{Line: r.End.Line, Character: utf16Column(lineAt(text, r.End.Line), r.End.Character)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// symIdentLen returns the rune length of a symbol's identifier as written:
|
||||||
|
// the verbatim spelling up to the ABI marker, offset or pseudo-register
|
||||||
|
// group, so `pkg·name<ABIInternal>(SB)` counts the package prefix and the
|
||||||
|
// middle dot. A range built from it covers the whole token a rename
|
||||||
|
// replaces; the stripped Name alone would stop one character short.
|
||||||
|
func symIdentLen(sym *ast.Symbol) int {
|
||||||
|
if i := strings.IndexAny(sym.Raw, "<+-("); i >= 0 {
|
||||||
|
return runeLen(sym.Raw[:i])
|
||||||
|
}
|
||||||
|
return runeLen(sym.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// symRange builds a range covering a symbol as written, prefix included.
|
||||||
func symRange(sym *ast.Symbol) Range {
|
func symRange(sym *ast.Symbol) Range {
|
||||||
start := Position{Line: sym.Pos.Line - 1, Character: sym.Pos.Column - 1}
|
start := Position{Line: sym.Pos.Line - 1, Character: sym.Pos.Column - 1}
|
||||||
end := start
|
end := start
|
||||||
end.Character += runeLen(sym.Name)
|
end.Character += symIdentLen(sym)
|
||||||
return Range{Start: start, End: end}
|
return Range{Start: start, End: end}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// realSymbol reports whether sym names something a client can act on. The
|
||||||
|
// parser keeps a malformed TEXT in the tree under a "?" placeholder so the
|
||||||
|
// rest of the buffer stays servable; that placeholder is not a symbol.
|
||||||
|
func realSymbol(sym *ast.Symbol) bool {
|
||||||
|
return sym != nil && sym.Name != "?"
|
||||||
|
}
|
||||||
|
|
||||||
// tokenRange builds a range covering one token.
|
// tokenRange builds a range covering one token.
|
||||||
func tokenRange(t token.Token) Range {
|
func tokenRange(t token.Token) Range {
|
||||||
return Range{
|
return Range{
|
||||||
@@ -852,7 +975,7 @@ func (s *Server) diagnosticsFor(uri string) []Diagnostic {
|
|||||||
pos = pe.Pos
|
pos = pe.Pos
|
||||||
}
|
}
|
||||||
out = append(out, Diagnostic{
|
out = append(out, Diagnostic{
|
||||||
Range: toRange(pos.Line, pos.Column, token.Position{}),
|
Range: clientRange(text, toRange(pos.Line, pos.Column, token.Position{})),
|
||||||
Severity: sevError,
|
Severity: sevError,
|
||||||
Code: "syntax",
|
Code: "syntax",
|
||||||
Source: "gasm",
|
Source: "gasm",
|
||||||
@@ -861,7 +984,7 @@ func (s *Server) diagnosticsFor(uri string) []Diagnostic {
|
|||||||
}
|
}
|
||||||
for _, d := range diags {
|
for _, d := range diags {
|
||||||
out = append(out, Diagnostic{
|
out = append(out, Diagnostic{
|
||||||
Range: toRange(d.Pos.Line, d.Pos.Column, d.End),
|
Range: clientRange(text, toRange(d.Pos.Line, d.Pos.Column, d.End)),
|
||||||
Severity: lintSeverity(d.Severity),
|
Severity: lintSeverity(d.Severity),
|
||||||
Code: d.Code,
|
Code: d.Code,
|
||||||
Source: "gasm",
|
Source: "gasm",
|
||||||
@@ -886,7 +1009,9 @@ func (s *Server) documentLinks(uri string) []DocumentLink {
|
|||||||
goroot := runtime.GOROOT()
|
goroot := runtime.GOROOT()
|
||||||
|
|
||||||
var out []DocumentLink
|
var out []DocumentLink
|
||||||
for i, line := range strings.Split(text, "\n") {
|
lineNo := -1
|
||||||
|
for line := range strings.SplitSeq(text, "\n") {
|
||||||
|
lineNo++
|
||||||
m := includeRe.FindStringSubmatch(line)
|
m := includeRe.FindStringSubmatch(line)
|
||||||
if m == nil {
|
if m == nil {
|
||||||
continue
|
continue
|
||||||
@@ -895,12 +1020,13 @@ func (s *Server) documentLinks(uri string) []DocumentLink {
|
|||||||
if target == "" {
|
if target == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
start := strings.Index(line, "\"")
|
// strings.Index is a byte offset; columns are runes.
|
||||||
|
start := utf8.RuneCountInString(line[:strings.Index(line, "\"")])
|
||||||
out = append(out, DocumentLink{
|
out = append(out, DocumentLink{
|
||||||
Range: Range{
|
Range: clientRange(text, Range{
|
||||||
Start: Position{Line: i, Character: start},
|
Start: Position{Line: lineNo, Character: start},
|
||||||
End: Position{Line: i, Character: start + len(m[1]) + 2},
|
End: Position{Line: lineNo, Character: start + runeLen(m[1]) + 2},
|
||||||
},
|
}),
|
||||||
Target: "file://" + target,
|
Target: "file://" + target,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type rpcError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
errParse = -32700
|
||||||
errMethodNotFound = -32601
|
errMethodNotFound = -32601
|
||||||
errInternalError = -32603
|
errInternalError = -32603
|
||||||
)
|
)
|
||||||
|
|||||||
+44
-3
@@ -6,6 +6,7 @@ package lsp
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -45,6 +46,11 @@ func (s *Server) Run() error {
|
|||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, errMalformed) {
|
||||||
|
// The message was already answered with a -32700 response;
|
||||||
|
// one malformed frame must not end the session.
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -54,9 +60,20 @@ func (s *Server) Run() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// read parses one Content-Length framed JSON-RPC message.
|
// errMalformed marks a message that could not be framed or decoded. The
|
||||||
|
// server answers it with a -32700 parse error before returning it, so the
|
||||||
|
// read loop continues instead of propagating a process exit.
|
||||||
|
var errMalformed = errors.New("malformed JSON-RPC message")
|
||||||
|
|
||||||
|
// read parses one Content-Length framed JSON-RPC message. A Content-Length
|
||||||
|
// value that does not parse, or a body that is not valid JSON, is answered
|
||||||
|
// with a -32700 response against the null id (as the JSON-RPC specification
|
||||||
|
// requires when the request itself cannot be decoded) and reported as
|
||||||
|
// errMalformed; the header block is consumed first so the next frame is
|
||||||
|
// read from its true start.
|
||||||
func (s *Server) read() (*rpcMessage, error) {
|
func (s *Server) read() (*rpcMessage, error) {
|
||||||
length := -1
|
length := -1
|
||||||
|
var headerErr string
|
||||||
for {
|
for {
|
||||||
line, err := s.in.ReadString('\n')
|
line, err := s.in.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -67,9 +84,18 @@ func (s *Server) read() (*rpcMessage, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if k, v, ok := strings.Cut(line, ":"); ok && strings.EqualFold(strings.TrimSpace(k), "Content-Length") {
|
if k, v, ok := strings.Cut(line, ":"); ok && strings.EqualFold(strings.TrimSpace(k), "Content-Length") {
|
||||||
length, _ = strconv.Atoi(strings.TrimSpace(v))
|
n, err := strconv.Atoi(strings.TrimSpace(v))
|
||||||
|
if err != nil || n < 0 {
|
||||||
|
headerErr = "invalid Content-Length header: " + strconv.Quote(strings.TrimSpace(v))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
length = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if headerErr != "" {
|
||||||
|
s.parseError(headerErr)
|
||||||
|
return nil, errMalformed
|
||||||
|
}
|
||||||
if length < 0 {
|
if length < 0 {
|
||||||
return nil, fmt.Errorf("missing Content-Length header")
|
return nil, fmt.Errorf("missing Content-Length header")
|
||||||
}
|
}
|
||||||
@@ -79,7 +105,8 @@ func (s *Server) read() (*rpcMessage, error) {
|
|||||||
}
|
}
|
||||||
var msg rpcMessage
|
var msg rpcMessage
|
||||||
if err := json.Unmarshal(body, &msg); err != nil {
|
if err := json.Unmarshal(body, &msg); err != nil {
|
||||||
return nil, err
|
s.parseError("invalid JSON body: " + err.Error())
|
||||||
|
return nil, errMalformed
|
||||||
}
|
}
|
||||||
return &msg, nil
|
return &msg, nil
|
||||||
}
|
}
|
||||||
@@ -101,6 +128,13 @@ func (s *Server) send(msg *rpcMessage) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) respond(id *json.RawMessage, result any) {
|
func (s *Server) respond(id *json.RawMessage, result any) {
|
||||||
|
// JSON-RPC 2.0 requires the result member to be present even when its
|
||||||
|
// value is null (shutdown, a hover over nothing); a nil result is
|
||||||
|
// marshalled as an explicit null rather than dropped by omitempty.
|
||||||
|
// Notifications never take this path, so they grow no member.
|
||||||
|
if result == nil {
|
||||||
|
result = json.RawMessage("null")
|
||||||
|
}
|
||||||
_ = s.send(&rpcMessage{ID: id, Result: result})
|
_ = s.send(&rpcMessage{ID: id, Result: result})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +142,13 @@ func (s *Server) respondError(id *json.RawMessage, code int, msg string) {
|
|||||||
_ = s.send(&rpcMessage{ID: id, Error: &rpcError{Code: code, Message: msg}})
|
_ = s.send(&rpcMessage{ID: id, Error: &rpcError{Code: code, Message: msg}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseError answers a -32700 parse error against the null id: when the
|
||||||
|
// request could not be decoded there is no id to echo back.
|
||||||
|
func (s *Server) parseError(msg string) {
|
||||||
|
id := json.RawMessage("null")
|
||||||
|
s.respondError(&id, errParse, msg)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) notify(method string, params any) {
|
func (s *Server) notify(method string, params any) {
|
||||||
raw, _ := json.Marshal(params)
|
raw, _ := json.Marshal(params)
|
||||||
_ = s.send(&rpcMessage{Method: method, Params: raw})
|
_ = s.send(&rpcMessage{Method: method, Params: raw})
|
||||||
|
|||||||
@@ -484,6 +484,328 @@ func TestMethodNotFound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMalformedContentLengthAnsweredAndRecovered pins the framing contract:
|
||||||
|
// an unparseable Content-Length is answered with a -32700 parse error against
|
||||||
|
// the null id, and the server keeps serving the next well-formed request
|
||||||
|
// instead of exiting the process.
|
||||||
|
func TestMalformedContentLengthAnsweredAndRecovered(t *testing.T) {
|
||||||
|
in := "Content-Length: 12x\r\n\r\n" +
|
||||||
|
frame(1, "initialize", map[string]any{}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
if n := countParseErrors(msgs); n != 1 {
|
||||||
|
t.Fatalf("want exactly one -32700 response, got %d", n)
|
||||||
|
}
|
||||||
|
if findByID(msgs, 1) == nil {
|
||||||
|
t.Fatal("server did not serve the request after the malformed header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMalformedJSONBodyAnsweredAndRecovered pins the same contract for a
|
||||||
|
// body that does not decode as JSON-RPC.
|
||||||
|
func TestMalformedJSONBodyAnsweredAndRecovered(t *testing.T) {
|
||||||
|
body := "{not json"
|
||||||
|
in := fmt.Sprintf("Content-Length: %d\r\n\r\n%s", len(body), body) +
|
||||||
|
frame(1, "initialize", map[string]any{}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
if n := countParseErrors(msgs); n != 1 {
|
||||||
|
t.Fatalf("want exactly one -32700 response, got %d", n)
|
||||||
|
}
|
||||||
|
if findByID(msgs, 1) == nil {
|
||||||
|
t.Fatal("server did not serve the request after the malformed body")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countParseErrors(msgs []rpcMessage) int {
|
||||||
|
n := 0
|
||||||
|
for i := range msgs {
|
||||||
|
if msgs[i].Error != nil && msgs[i].Error.Code == errParse {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNullResultPresent pins the response envelope: JSON-RPC 2.0 requires a
|
||||||
|
// result member even when its value is null (shutdown, hover over nothing),
|
||||||
|
// while notifications must not grow one.
|
||||||
|
func TestNullResultPresent(t *testing.T) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
in := frame(1, "shutdown", nil) + frame(nil, "exit", nil)
|
||||||
|
srv := New(strings.NewReader(in), &out)
|
||||||
|
if err := srv.Run(); err != nil {
|
||||||
|
t.Fatalf("server run: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.String(), `"result":null`) {
|
||||||
|
t.Fatalf("shutdown response must serialise an explicit null result, got %q", out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHoverNullResultPresent checks the same envelope on a handler whose
|
||||||
|
// result is genuinely null: hover over a position with no symbol.
|
||||||
|
func TestHoverNullResultPresent(t *testing.T) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
in := session("file:///f_amd64.s", cleanDoc) +
|
||||||
|
frame(3, "textDocument/hover", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
"position": map[string]any{"line": 0, "character": 0},
|
||||||
|
}) + frame(nil, "exit", nil)
|
||||||
|
srv := New(strings.NewReader(in), &out)
|
||||||
|
if err := srv.Run(); err != nil {
|
||||||
|
t.Fatalf("server run: %v", err)
|
||||||
|
}
|
||||||
|
raw := out.String()
|
||||||
|
if !strings.Contains(raw, `"result":null`) {
|
||||||
|
t.Fatalf("null hover result must serialise as an explicit null, got %q", raw)
|
||||||
|
}
|
||||||
|
// The publishDiagnostics notification must not carry a result member.
|
||||||
|
for _, m := range readFrames(t, strings.NewReader(raw)) {
|
||||||
|
if m.Method == "textDocument/publishDiagnostics" && m.Result != nil {
|
||||||
|
t.Fatalf("notification grew a result member: %+v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyEdits applies text edits whose character columns are rune columns
|
||||||
|
// (the middle dot makes byte and rune offsets diverge).
|
||||||
|
func applyEdits(text string, edits []TextEdit) string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
for _, e := range edits {
|
||||||
|
s, en := e.Range.Start, e.Range.End
|
||||||
|
if s.Line != en.Line || s.Line >= len(lines) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
runes := []rune(lines[s.Line])
|
||||||
|
if s.Character > len(runes) || en.Character > len(runes) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lines[s.Line] = string(runes[:s.Character]) + e.NewText + string(runes[en.Character:])
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRenameMiddleDotCoversFullToken pins the reference ranges: a symbol
|
||||||
|
// reference carries the `·` prefix, so every edit must span the whole raw
|
||||||
|
// token or the rename corrupts the source (`·helpe` + `r` shapes).
|
||||||
|
func TestRenameMiddleDotCoversFullToken(t *testing.T) {
|
||||||
|
doc := "#include \"textflag.h\"\n" +
|
||||||
|
"TEXT ·helper(SB), NOSPLIT, $0\n" +
|
||||||
|
"\tRET\n" +
|
||||||
|
"TEXT ·caller(SB), NOSPLIT, $0\n" +
|
||||||
|
"\tCALL ·helper(SB)\n" +
|
||||||
|
"\tRET\n"
|
||||||
|
in := session("file:///f_amd64.s", doc) +
|
||||||
|
frame(7, "textDocument/rename", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
"position": map[string]any{"line": 4, "character": 7}, // on ·helper in CALL
|
||||||
|
"newName": "renamed",
|
||||||
|
}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
resp := findByID(msgs, 7)
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("no rename response")
|
||||||
|
}
|
||||||
|
var edit WorkspaceEdit
|
||||||
|
if err := json.Unmarshal(mustResult(t, resp), &edit); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
edits := edit.Changes["file:///f_amd64.s"]
|
||||||
|
if len(edits) != 2 {
|
||||||
|
t.Fatalf("want 2 edits (definition + call), got %d", len(edits))
|
||||||
|
}
|
||||||
|
for _, e := range edits {
|
||||||
|
if got := e.Range.End.Character - e.Range.Start.Character; got != runeLen("·helper") {
|
||||||
|
t.Errorf("edit range covers %d characters, want the full %d of ·helper", got, runeLen("·helper"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
got := applyEdits(doc, edits)
|
||||||
|
if strings.Contains(got, "·helper") || strings.Contains(got, "·renamed") {
|
||||||
|
t.Fatalf("rename left a partial spelling behind: %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "TEXT renamed(SB)") || !strings.Contains(got, "CALL renamed(SB)") {
|
||||||
|
t.Fatalf("rename did not rewrite both sites: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDocumentHighlightsMiddleDot pins the trim the handler was missing:
|
||||||
|
// the word under the cursor keeps its `·` prefix while symbol names are
|
||||||
|
// stored without it, so the untrimmed comparison returned nothing.
|
||||||
|
func TestDocumentHighlightsMiddleDot(t *testing.T) {
|
||||||
|
doc := "#include \"textflag.h\"\n" +
|
||||||
|
"TEXT ·helper(SB), NOSPLIT, $0\n" +
|
||||||
|
"\tRET\n" +
|
||||||
|
"TEXT ·caller(SB), NOSPLIT, $0\n" +
|
||||||
|
"\tCALL ·helper(SB)\n" +
|
||||||
|
"\tRET\n"
|
||||||
|
in := session("file:///f_amd64.s", doc) +
|
||||||
|
frame(12, "textDocument/documentHighlight", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
"position": map[string]any{"line": 4, "character": 7},
|
||||||
|
}) + 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 the definition and the call highlighted, got %d", len(highlights))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSymbolsSkipMalformedTextPlaceholder checks both symbol listings
|
||||||
|
// against a mid-edit buffer whose first TEXT line has no symbol yet: the
|
||||||
|
// placeholder is skipped and the remaining symbols are still served.
|
||||||
|
func TestSymbolsSkipMalformedTextPlaceholder(t *testing.T) {
|
||||||
|
doc := "TEXT $0\n" +
|
||||||
|
"\tMOVQ AX, BX\n" +
|
||||||
|
"TEXT ·ok(SB), NOSPLIT, $0\n" +
|
||||||
|
"\tRET\n"
|
||||||
|
in := session("file:///f_amd64.s", doc) +
|
||||||
|
frame(4, "textDocument/documentSymbol", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
}) +
|
||||||
|
frame(13, "workspace/symbol", map[string]any{"query": "ok"}) +
|
||||||
|
frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
|
||||||
|
dresp := findByID(msgs, 4)
|
||||||
|
if dresp == nil || dresp.Error != nil {
|
||||||
|
t.Fatalf("documentSymbol failed on a mid-edit buffer: %+v", dresp)
|
||||||
|
}
|
||||||
|
var syms []DocumentSymbol
|
||||||
|
if err := json.Unmarshal(mustResult(t, dresp), &syms); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(syms) != 1 || syms[0].Name != "ok" {
|
||||||
|
t.Fatalf("want only the ·ok function, got %+v", syms)
|
||||||
|
}
|
||||||
|
|
||||||
|
wresp := findByID(msgs, 13)
|
||||||
|
if wresp == nil || wresp.Error != nil {
|
||||||
|
t.Fatalf("workspace/symbol failed on a mid-edit buffer: %+v", wresp)
|
||||||
|
}
|
||||||
|
var wsyms []WorkspaceSymbol
|
||||||
|
if err := json.Unmarshal(mustResult(t, wresp), &wsyms); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(wsyms) != 1 || wsyms[0].Name != "ok" {
|
||||||
|
t.Fatalf("want only the ·ok symbol, got %+v", wsyms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUTF16PositionEncoding exercises the boundary conversions directly:
|
||||||
|
// one astral rune is one rune but two UTF-16 code units.
|
||||||
|
func TestUTF16PositionEncoding(t *testing.T) {
|
||||||
|
const line = "\U0001F389ab" // emoji, then two ASCII letters
|
||||||
|
if got := utf16Len(line); got != 4 {
|
||||||
|
t.Errorf("utf16Len = %d, want 4", got)
|
||||||
|
}
|
||||||
|
if got := utf16Column(line, 3); got != 4 {
|
||||||
|
t.Errorf("utf16Column(col 3) = %d, want 4", got)
|
||||||
|
}
|
||||||
|
if got := runeColumn(line, 4); got != 3 {
|
||||||
|
t.Errorf("runeColumn(units 4) = %d, want 3", got)
|
||||||
|
}
|
||||||
|
rng := clientRange("x\n"+line, Range{
|
||||||
|
Start: Position{Line: 1, Character: 1},
|
||||||
|
End: Position{Line: 1, Character: 3},
|
||||||
|
})
|
||||||
|
if rng.Start.Character != 2 || rng.End.Character != 4 {
|
||||||
|
t.Errorf("clientRange = %+v, want 2..4 UTF-16 units", rng)
|
||||||
|
}
|
||||||
|
// wordAt takes UTF-16 units from the client and lands on the right rune.
|
||||||
|
w, wr := wordAt("loop: // \U0001F389 x\n", Position{Line: 0, Character: 12})
|
||||||
|
if w != "x" {
|
||||||
|
t.Fatalf("wordAt = %q, want x", w)
|
||||||
|
}
|
||||||
|
if wr.Start.Character != 11 || wr.End.Character != 12 {
|
||||||
|
t.Errorf("wordAt range = %+v, want 11..12 (rune columns are returned unconverted)", wr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSemanticTokensAstralCommentLength checks that an astral rune in a
|
||||||
|
// comment is counted as two UTF-16 code units in the token length.
|
||||||
|
func TestSemanticTokensAstralCommentLength(t *testing.T) {
|
||||||
|
comment := "// \U0001F389 ok"
|
||||||
|
doc := "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET " + comment + "\n"
|
||||||
|
in := session("file:///f_amd64.s", doc) +
|
||||||
|
frame(5, "textDocument/semanticTokens/full", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
resp := findByID(msgs, 5)
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("no semanticTokens response")
|
||||||
|
}
|
||||||
|
var st SemanticTokens
|
||||||
|
if err := json.Unmarshal(mustResult(t, resp), &st); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Decode the delta stream: the RET line carries the comment token.
|
||||||
|
line, char := 0, 0
|
||||||
|
found := false
|
||||||
|
for i := 0; i+4 < len(st.Data); i += 5 {
|
||||||
|
if st.Data[i] != 0 {
|
||||||
|
line += st.Data[i]
|
||||||
|
char = st.Data[i+1]
|
||||||
|
} else {
|
||||||
|
char += st.Data[i+1]
|
||||||
|
}
|
||||||
|
if line == 2 && st.Data[i+3] == stComment {
|
||||||
|
found = true
|
||||||
|
if st.Data[i+2] != utf16Len(comment) {
|
||||||
|
t.Errorf("comment token length = %d, want %d UTF-16 code units", st.Data[i+2], utf16Len(comment))
|
||||||
|
}
|
||||||
|
if want := utf16Len("\tRET "); char != want {
|
||||||
|
t.Errorf("comment token char = %d, want %d UTF-16 code units", char, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("no comment token on the RET line: %v", st.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSemanticTokensPipeOperator checks that the `|` joining TEXT flags is
|
||||||
|
// classified as an operator instead of falling out of the switch unstyled.
|
||||||
|
func TestSemanticTokensPipeOperator(t *testing.T) {
|
||||||
|
doc := "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT|DUPOK, $0\n\tRET\n"
|
||||||
|
in := session("file:///f_amd64.s", doc) +
|
||||||
|
frame(5, "textDocument/semanticTokens/full", map[string]any{
|
||||||
|
"textDocument": map[string]any{"uri": "file:///f_amd64.s"},
|
||||||
|
}) + frame(nil, "exit", nil)
|
||||||
|
msgs := run(t, in)
|
||||||
|
resp := findByID(msgs, 5)
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("no semanticTokens response")
|
||||||
|
}
|
||||||
|
var st SemanticTokens
|
||||||
|
if err := json.Unmarshal(mustResult(t, resp), &st); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Decode the delta stream and look for a one-character operator token
|
||||||
|
// on the TEXT line.
|
||||||
|
line, char := 0, 0
|
||||||
|
found := false
|
||||||
|
for i := 0; i+4 < len(st.Data); i += 5 {
|
||||||
|
if st.Data[i] != 0 {
|
||||||
|
line += st.Data[i]
|
||||||
|
char = st.Data[i+1]
|
||||||
|
} else {
|
||||||
|
char += st.Data[i+1]
|
||||||
|
}
|
||||||
|
if line == 1 && st.Data[i+2] == 1 && st.Data[i+3] == stOperator {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("no operator token found for the | in the flag list: %v", st.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mustResult re-marshals a response result into raw JSON for typed decoding.
|
// mustResult re-marshals a response result into raw JSON for typed decoding.
|
||||||
func mustResult(t *testing.T, m *rpcMessage) []byte {
|
func mustResult(t *testing.T, m *rpcMessage) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
Reference in New Issue
Block a user