blob: 6c57a6875b954c99aaffff0dc9b115f84289277a [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/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
// line number (1-based) and message
var errRe = regexp.MustCompile(`template.*:(\d+): (.*)`)
// Diagnose returns parse errors. There is only one.
// The errors are not always helpful. For instance { {end}}
// will likely point to the end of the file.
func Diagnose(f source.VersionedFileHandle) []*source.Diagnostic {
// no need for skipTemplate check, as Diagnose is called on the
// snapshot's template files
buf, err := f.Read()
if err != nil {
// Is a Diagnostic with no Range useful? event.Error also?
msg := fmt.Sprintf("failed to read %s (%v)", f.URI().Filename(), err)
d := source.Diagnostic{Message: msg, Severity: protocol.SeverityError, URI: f.URI()}
return []*source.Diagnostic{&d}
}
p := parseBuffer(buf)
if p.ParseErr == nil {
return nil
}
unknownError := func(msg string) []*source.Diagnostic {
s := fmt.Sprintf("malformed template error %q: %s", p.ParseErr.Error(), msg)
d := source.Diagnostic{Message: s, Severity: protocol.SeverityError, Range: p.Range(p.nls[0], 1), URI: f.URI()}
return []*source.Diagnostic{&d}
}
// 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 {
msg := fmt.Sprintf("expected 3 matches, got %d (%v)", len(matches), matches)
return unknownError(msg)
}
lineno, err := strconv.Atoi(matches[1])
if err != nil {
msg := fmt.Sprintf("couldn't convert %q to int, %v", matches[1], err)
return unknownError(msg)
}
msg := matches[2]
d := source.Diagnostic{Message: msg, Severity: protocol.SeverityError}
start := p.nls[lineno-1]
if lineno < len(p.nls) {
size := p.nls[lineno] - start
d.Range = p.Range(start, size)
} else {
d.Range = p.Range(start, 1)
}
return []*source.Diagnostic{&d}
}
func skipTemplates(s source.Snapshot) bool {
return !s.View().Options().ExperimentalTemplateSupport
}
// Definition finds the definitions of the symbol at loc. It
// does not understand scoping (if any) in templates. This code is
// for defintions, type definitions, and implementations.
// Results only for variables and templates.
func Definition(snapshot source.Snapshot, fh source.VersionedFileHandle, loc protocol.Position) ([]protocol.Location, error) {
if skipTemplates(snapshot) {
return nil, nil
}
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 := New(snapshot.Templates())
for k, p := range a.files {
for _, s := range p.symbols {
if !s.vardef || s.name != sym {
continue
}
ans = append(ans, protocol.Location{URI: protocol.DocumentURI(k), Range: p.Range(s.start, s.length)})
}
}
return ans, nil
}
func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
if skipTemplates(snapshot) {
return nil, nil
}
sym, p, err := symAtPosition(fh, position)
if sym == nil || err != nil {
return nil, err
}
ans := protocol.Hover{Range: p.Range(sym.start, sym.length), Contents: protocol.MarkupContent{Kind: protocol.Markdown}}
switch sym.kind {
case protocol.Function:
ans.Contents.Value = fmt.Sprintf("function: %s", sym.name)
case protocol.Variable:
ans.Contents.Value = fmt.Sprintf("variable: %s", sym.name)
case protocol.Constant:
ans.Contents.Value = fmt.Sprintf("constant %s", sym.name)
case protocol.Method: // field or method
ans.Contents.Value = fmt.Sprintf("%s: field or method", sym.name)
case protocol.Package: // template use, template def (PJW: do we want two?)
ans.Contents.Value = fmt.Sprintf("template %s\n(add definition)", sym.name)
case protocol.Namespace:
ans.Contents.Value = fmt.Sprintf("template %s defined", sym.name)
default:
ans.Contents.Value = fmt.Sprintf("oops, sym=%#v", sym)
}
return &ans, nil
}
func References(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, params *protocol.ReferenceParams) ([]protocol.Location, error) {
if skipTemplates(snapshot) {
return nil, nil
}
sym, _, err := symAtPosition(fh, params.Position)
if sym == nil || err != nil || sym.name == "" {
return nil, err
}
ans := []protocol.Location{}
a := New(snapshot.Templates())
for k, p := range a.files {
for _, s := range p.symbols {
if s.name != sym.name {
continue
}
if s.vardef && !params.Context.IncludeDeclaration {
continue
}
ans = append(ans, protocol.Location{URI: protocol.DocumentURI(k), Range: p.Range(s.start, s.length)})
}
}
// do these need to be sorted? (a.files is a map)
return ans, nil
}
func SemanticTokens(ctx context.Context, snapshot source.Snapshot, spn span.URI, add func(line, start, len uint32), d func() []uint32) (*protocol.SemanticTokens, error) {
if skipTemplates(snapshot) {
return nil, nil
}
fh, err := snapshot.GetFile(ctx, spn)
if err != nil {
return nil, err
}
buf, err := fh.Read()
if err != nil {
return nil, err
}
p := parseBuffer(buf)
if p.ParseErr != nil {
return nil, p.ParseErr
}
for _, t := range p.Tokens() {
if t.Multiline {
la, ca := p.LineCol(t.Start)
lb, cb := p.LineCol(t.End)
add(la, ca, p.RuneCount(la, ca, 0))
for l := la + 1; l < lb; l++ {
add(l, 0, p.RuneCount(l, 0, 0))
}
add(lb, 0, p.RuneCount(lb, 0, cb))
continue
}
sz, err := p.TokenSize(t)
if err != nil {
return nil, err
}
line, col := p.LineCol(t.Start)
add(line, col, uint32(sz))
}
data := d()
ans := &protocol.SemanticTokens{
Data: data,
// 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()),
}
return ans, nil
}
// still need to do rename, etc