blob: f0e5552190332369be7fac019dffcfde598a17c8 [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package template
import (
"context"
"fmt"
"regexp"
"strconv"
"time"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/protocol/semtok"
)
// line number (1-based) and message
var errRe = regexp.MustCompile(`template.*:(\d+): (.*)`)
// Diagnostics returns parse errors. There is only one per file.
// The errors are not always helpful. For instance { {end}}
// will likely point to the end of the file.
func Diagnostics(snapshot *cache.Snapshot) map[protocol.DocumentURI][]*cache.Diagnostic {
diags := make(map[protocol.DocumentURI][]*cache.Diagnostic)
for uri, fh := range snapshot.Templates() {
diags[uri] = diagnoseOne(fh)
}
return diags
}
func diagnoseOne(fh file.Handle) []*cache.Diagnostic {
// no need for skipTemplate check, as Diagnose is called on the
// snapshot's template files
buf, err := fh.Content()
if err != nil {
// TODO: Is a Diagnostic with no Range useful? event.Error also?
msg := fmt.Sprintf("failed to read %s (%v)", fh.URI().Path(), err)
return []*cache.Diagnostic{{
Message: msg,
Severity: protocol.SeverityError,
URI: fh.URI(),
Source: cache.TemplateError,
}}
}
p := parseBuffer(fh.URI(), buf)
if p.parseErr == nil {
return nil
}
errorf := func(format string, args ...any) []*cache.Diagnostic {
msg := fmt.Sprintf("malformed template error %q: %s",
p.parseErr.Error(),
fmt.Sprintf(format, args...))
rng, err := p.mapper.OffsetRange(0, 1) // first UTF-16 code
if err != nil {
rng = protocol.Range{} // start of file
}
return []*cache.Diagnostic{{
Message: msg,
Severity: protocol.SeverityError,
Range: rng,
URI: fh.URI(),
Source: cache.TemplateError,
}}
}
// errors look like `template: :40: unexpected "}" in operand`
// so the string needs to be parsed
matches := errRe.FindStringSubmatch(p.parseErr.Error())
if len(matches) != 3 {
return errorf("expected 3 matches, got %d (%v)", len(matches), matches)
}
lineno, err := strconv.Atoi(matches[1])
if err != nil {
return errorf("couldn't convert %q to int, %v", matches[1], err)
}
msg := matches[2]
// Compute the range for the whole (1-based) line.
rng, err := lineRange(p.mapper, lineno)
if err != nil {
return errorf("invalid position: %v", err)
}
return []*cache.Diagnostic{{
Message: msg,
Severity: protocol.SeverityError,
Range: rng,
Source: cache.TemplateError,
}}
}
// Definition finds the definitions of the symbol at loc. It
// does not understand scoping (if any) in templates. This code is
// for definitions, type definitions, and implementations.
// Results only for variables and templates.
func Definition(snapshot *cache.Snapshot, fh file.Handle, loc protocol.Position) ([]protocol.Location, error) {
x, _, err := symAtPosition(fh, loc)
if err != nil {
return nil, err
}
sym := x.name
ans := []protocol.Location{}
// PJW: this is probably a pattern to abstract
a := parseSet(snapshot.Templates())
for _, p := range a.files {
for _, s := range p.symbols {
if !s.vardef || s.name != sym {
continue
}
loc, err := p.mapper.OffsetLocation(s.offsets())
if err != nil {
return nil, err
}
ans = append(ans, loc)
}
}
return ans, nil
}
func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
sym, p, err := symAtPosition(fh, position)
if err != nil {
return nil, err
}
var value string
switch sym.kind {
case protocol.Function:
value = fmt.Sprintf("function: %s", sym.name)
case protocol.Variable:
value = fmt.Sprintf("variable: %s", sym.name)
case protocol.Constant:
value = fmt.Sprintf("constant %s", sym.name)
case protocol.Method: // field or method
value = fmt.Sprintf("%s: field or method", sym.name)
case protocol.Package: // template use, template def (PJW: do we want two?)
value = fmt.Sprintf("template %s\n(add definition)", sym.name)
case protocol.Namespace:
value = fmt.Sprintf("template %s defined", sym.name)
case protocol.Number:
value = "number"
case protocol.String:
value = "string"
case protocol.Boolean:
value = "boolean"
default:
value = fmt.Sprintf("oops, sym=%#v", sym)
}
rng, err := p.mapper.OffsetRange(sym.offsets())
if err != nil {
return nil, err
}
return &protocol.Hover{
Range: rng,
Contents: protocol.MarkupContent{
Kind: protocol.Markdown,
Value: value,
},
}, nil
}
func References(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, params *protocol.ReferenceParams) ([]protocol.Location, error) {
sym, _, err := symAtPosition(fh, params.Position)
if err != nil {
return nil, err
}
if sym.name == "" {
return nil, fmt.Errorf("no symbol at position")
}
ans := []protocol.Location{}
a := parseSet(snapshot.Templates())
for _, p := range a.files {
for _, s := range p.symbols {
if s.name != sym.name {
continue
}
if s.vardef && !params.Context.IncludeDeclaration {
continue
}
loc, err := p.mapper.OffsetLocation(s.offsets())
if err != nil {
return nil, err
}
ans = append(ans, loc)
}
}
// TODO: do these need to be sorted? (a.files is a map)
return ans, nil
}
func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, spn protocol.DocumentURI) (*protocol.SemanticTokens, error) {
fh, err := snapshot.ReadFile(ctx, spn)
if err != nil {
return nil, err
}
buf, err := fh.Content()
if err != nil {
return nil, err
}
p := parseBuffer(fh.URI(), buf)
var items []semtok.Token
for _, t := range p.tokens {
if t.start == t.end {
continue // vscode doesn't like 0-length tokens
}
pos, err := p.mapper.OffsetPosition(t.start)
if err != nil {
return nil, err
}
// TODO(adonovan): don't ignore the rng restriction, if any.
items = append(items, semtok.Token{
Line: pos.Line,
Start: pos.Character,
Len: uint32(protocol.UTF16Len(p.buf[t.start:t.end])),
Type: semtok.TokMacro,
})
}
return &protocol.SemanticTokens{
Data: semtok.Encode(items, nil, nil),
// for small cache, some day. for now, the LSP client ignores this
// (that is, when the LSP client starts returning these, we can cache)
ResultID: fmt.Sprintf("%v", time.Now()),
}, nil
}
// TODO: still need to do rename, etc
func symAtPosition(fh file.Handle, posn protocol.Position) (*symbol, *parsed, error) {
buf, err := fh.Content()
if err != nil {
return nil, nil, err
}
p := parseBuffer(fh.URI(), buf)
offset, err := p.mapper.PositionOffset(posn)
if err != nil {
return nil, nil, err
}
var syms []symbol
for _, s := range p.symbols {
if s.start <= offset && offset < s.start+s.len {
syms = append(syms, s)
}
}
if len(syms) == 0 {
return nil, p, fmt.Errorf("no symbol found")
}
sym := syms[0]
return &sym, p, nil
}