internal/lsp: support template files

Provide some support for template files, implementing most of
https://docs.google.com/document/d/1clKAywucZVBXvL_v4mMhLQXso59lmQPMk1gtSpkV-Xw

Template support is controlled by the option 'experimentalTemplateSupport'
which defaults to false.

Most of the code is in a new 'template' package. Implemented are
semantic tokens, diagnostics, definitions, hover, and references,
and there is a stub for completions.

This code treats all the template files of a package together, so as
to follow cross-references.

Change-Id: I793606d8a0c9e96a0c015162d68f56b5d8599294
Reviewed-on: https://go-review.googlesource.com/c/tools/+/297871
Run-TryBot: Peter Weinberger <pjw@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
Trust: Peter Weinberger <pjw@google.com>
This commit is contained in:
Peter Weinbergr 2021-03-02 10:16:22 -05:00 committed by Peter Weinberger
parent f03daeacec
commit 7cab0ef2e9
24 changed files with 1414 additions and 16 deletions

View File

@ -112,6 +112,15 @@ for multi-module workspaces.
Default: `false`.
#### **experimentalTemplateSupport** *bool*
**This setting is experimental and may be deleted.**
experimentalTemplateSupport opts into the experimental support
for template files.
Default: `false`.
#### **experimentalPackageCacheKey** *bool*
**This setting is experimental and may be deleted.**

View File

@ -0,0 +1,72 @@
// 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 misc
import (
"strings"
"testing"
"golang.org/x/tools/internal/lsp/protocol"
. "golang.org/x/tools/internal/lsp/regtest"
)
const filesA = `
-- go.mod --
module mod.com
go 1.12
-- b.gotmpl --
{{define "A"}}goo{{end}}
-- a.tmpl --
{{template "A"}}
`
func TestSuffixes(t *testing.T) {
WithOptions(
EditorConfig{
AllExperiments: true,
},
).Run(t, filesA, func(t *testing.T, env *Env) {
env.OpenFile("a.tmpl")
x := env.RegexpSearch("a.tmpl", `A`)
file, pos := env.GoToDefinition("a.tmpl", x)
refs := env.References(file, pos)
if len(refs) != 2 {
t.Fatalf("got %v reference(s), want 2", len(refs))
}
// make sure we got one from b.gotmpl
want := env.Sandbox.Workdir.URI("b.gotmpl")
if refs[0].URI != want && refs[1].URI != want {
t.Errorf("failed to find reference to %s", shorten(want))
for i, r := range refs {
t.Logf("%d: URI:%s %v", i, shorten(r.URI), r.Range)
}
}
content, npos := env.Hover(file, pos)
if pos != npos {
t.Errorf("pos? got %v, wanted %v", npos, pos)
}
if content.Value != "template A defined" {
t.Errorf("got %s, wanted 'template A defined", content.Value)
}
})
}
// shorten long URIs
func shorten(fn protocol.DocumentURI) string {
if len(fn) <= 20 {
return string(fn)
}
pieces := strings.Split(string(fn), "/")
if len(pieces) < 2 {
return string(fn)
}
j := len(pieces)
return pieces[j-2] + "/" + pieces[j-1]
}
// Hover, SemTok, Diagnose with errors
// and better coverage

View File

@ -147,6 +147,19 @@ func (s *snapshot) ModFiles() []span.URI {
return uris
}
func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle {
if !s.view.options.ExperimentalTemplateSupport {
return nil
}
ans := map[span.URI]source.VersionedFileHandle{}
for k, x := range s.files {
if strings.HasSuffix(filepath.Ext(k.Filename()), "tmpl") {
ans[k] = x
}
}
return ans
}
func (s *snapshot) ValidBuildConfiguration() bool {
return validBuildConfiguration(s.view.rootURI, &s.view.workspaceInformation, s.workspace.getActiveModFiles())
}
@ -677,6 +690,7 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
// applied to every folder in the workspace.
patterns := map[string]struct{}{
"**/*.{go,mod,sum}": {},
"**/*.*tmpl": {},
}
dirs := s.workspace.dirs(ctx, s)
for _, dir := range dirs {
@ -690,7 +704,7 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru
// TODO(rstambler): If microsoft/vscode#3025 is resolved before
// microsoft/vscode#101042, we will need a work-around for Windows
// drive letter casing.
patterns[fmt.Sprintf("%s/**/*.{go,mod,sum}", dirName)] = struct{}{}
patterns[fmt.Sprintf("%s/**/*.{go,mod,sum,tmpl}", dirName)] = struct{}{}
}
// Some clients do not send notifications for changes to directories that

View File

@ -332,6 +332,37 @@ func (s *snapshot) RunProcessEnvFunc(ctx context.Context, fn func(*imports.Optio
return s.view.importsState.runProcessEnvFunc(ctx, s, fn)
}
func (s *snapshot) locateTemplateFiles(ctx context.Context) {
if !s.view.Options().ExperimentalTemplateSupport {
return
}
dir := s.workspace.root.Filename()
searched := 0
// Change to WalkDir when we move up to 1.16
err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(filepath.Ext(path), "tmpl") && !pathExcludedByFilter(path, s.view.options) &&
!fi.IsDir() {
k := span.URIFromPath(path)
fh, err := s.GetVersionedFile(ctx, k)
if err != nil {
return nil
}
s.files[k] = fh
}
searched++
if fileLimit > 0 && searched > fileLimit {
return errExhausted
}
return nil
})
if err != nil {
event.Error(ctx, "searching for template files failed", err)
}
}
func (v *View) contains(uri span.URI) bool {
inRoot := source.InDir(v.rootURI.Filename(), uri.Filename())
inFolder := source.InDir(v.folder.Filename(), uri.Filename())
@ -548,6 +579,7 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) {
Message: err.Error(),
})
}
s.locateTemplateFiles(ctx)
if len(s.workspace.getActiveModFiles()) > 0 {
for modURI := range s.workspace.getActiveModFiles() {
fh, err := s.GetFile(ctx, modURI)

View File

@ -54,6 +54,9 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
wanted[only] = supportedCodeActions[only] || explicit[only]
}
}
if len(supportedCodeActions) == 0 {
return nil, nil // not an error if there are none supported
}
if len(wanted) == 0 {
return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only)
}

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/source/completion"
"golang.org/x/tools/internal/lsp/template"
"golang.org/x/tools/internal/span"
)
@ -31,6 +32,8 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context)
case source.Mod:
candidates, surrounding = nil, nil
case source.Tmpl:
candidates, surrounding, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context)
}
if err != nil {
event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position))

View File

@ -9,14 +9,19 @@ import (
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/template"
)
func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) {
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
kind := source.DetectLanguage("", params.TextDocument.URI.SpanURI().Filename())
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, kind)
defer release()
if !ok {
return nil, err
}
if fh.Kind() == source.Tmpl {
return template.Definition(snapshot, fh, params.Position)
}
ident, err := source.Identifier(ctx, snapshot, fh, params.Position)
if err != nil {
return nil, err

View File

@ -19,6 +19,7 @@ import (
"golang.org/x/tools/internal/lsp/mod"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/template"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/xcontext"
errors "golang.org/x/xerrors"
@ -209,6 +210,12 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn
// error progress reports will be closed.
s.showCriticalErrorStatus(ctx, snapshot, criticalErr)
// There may be .tmpl files.
for _, f := range snapshot.Templates() {
diags := template.Diagnose(f)
s.storeDiagnostics(snapshot, f.URI(), typeCheckSource, diags)
}
// If there are no workspace packages, there is nothing to diagnose and
// there are no orphaned files.
if len(wsPkgs) == 0 {

View File

@ -11,6 +11,7 @@ import (
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/template"
)
func (s *Server) documentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) {
@ -19,6 +20,11 @@ func (s *Server) documentHighlight(ctx context.Context, params *protocol.Documen
if !ok {
return nil, err
}
if fh.Kind() == source.Tmpl {
return template.Highlight(ctx, snapshot, fh, params.Position)
}
rngs, err := source.Highlight(ctx, snapshot, fh, params.Position)
if err != nil {
event.Error(ctx, "no highlight", err, tag.URI.Of(params.TextDocument.URI))

View File

@ -10,6 +10,7 @@ import (
"golang.org/x/tools/internal/lsp/mod"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/template"
)
func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
@ -23,6 +24,8 @@ func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*prot
return mod.Hover(ctx, snapshot, fh, params.Position)
case source.Go:
return source.Hover(ctx, snapshot, fh, params.Position)
case source.Tmpl:
return template.Hover(ctx, snapshot, fh, params.Position)
}
return nil, nil
}

View File

@ -9,14 +9,18 @@ import (
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/template"
)
func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) {
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
defer release()
if !ok {
return nil, err
}
if fh.Kind() == source.Tmpl {
return template.References(ctx, snapshot, fh, params)
}
references, err := source.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration)
if err != nil {
return nil, err

View File

@ -19,6 +19,7 @@ import (
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/template"
errors "golang.org/x/xerrors"
)
@ -48,7 +49,8 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu
ans := protocol.SemanticTokens{
Data: []uint32{},
}
snapshot, _, ok, release, err := s.beginFileRequest(ctx, td.URI, source.Go)
kind := source.DetectLanguage("", td.URI.SpanURI().Filename())
snapshot, _, ok, release, err := s.beginFileRequest(ctx, td.URI, kind)
defer release()
if !ok {
return nil, err
@ -59,6 +61,23 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu
// the client won't remember the wrong answer
return nil, errors.Errorf("semantictokens are disabled")
}
if kind == source.Tmpl {
// this is a little cumbersome to avoid both exporting 'encoded' and its methods
// and to avoid import cycles
e := &encoded{
ctx: ctx,
rng: rng,
tokTypes: s.session.Options().SemanticTypes,
tokMods: s.session.Options().SemanticMods,
}
add := func(line, start uint32, len uint32) {
e.add(line, start, len, tokMacro, nil)
}
data := func() ([]uint32, error) {
return e.Data()
}
return template.SemanticTokens(ctx, snapshot, td.URI.SpanURI(), add, data)
}
pkg, err := snapshot.PackageForFile(ctx, td.URI.SpanURI(), source.TypecheckFull, source.WidestPackage)
if err != nil {
return nil, err
@ -132,6 +151,8 @@ const (
tokString tokenType = "string"
tokNumber tokenType = "number"
tokOperator tokenType = "operator"
tokMacro tokenType = "macro" // for templates
)
func (e *encoded) token(start token.Pos, leng int, typ tokenType, mods []string) {
@ -414,7 +435,7 @@ func (e *encoded) ident(x *ast.Ident) {
case *types.Var:
e.token(x.Pos(), len(x.Name), tokVariable, nil)
default:
// replace with panic after extensive testing
// can't happen
if use == nil {
msg := fmt.Sprintf("%#v/%#v %#v %#v", x, x.Obj, e.ti.Defs[x], e.ti.Uses[x])
e.unexpected(msg)
@ -489,7 +510,7 @@ func (e *encoded) definitionFor(x *ast.Ident) (tokenType, []string) {
return tokType, mods
}
}
// panic after extensive testing
// can't happen
msg := fmt.Sprintf("failed to find the decl for %s", e.pgf.Tok.PositionFor(x.Pos(), false))
e.unexpected(msg)
return "", []string{""}
@ -582,11 +603,9 @@ func (e *encoded) importSpec(d *ast.ImportSpec) {
e.token(start, len(nm), tokNamespace, nil)
}
// panic on unexpected state
// log unexpected state
func (e *encoded) unexpected(msg string) {
log.Print(msg)
log.Print(e.strStack())
panic(msg)
event.Error(e.ctx, e.strStack(), errors.New(msg))
}
// SemType returns a string equivalent of the type, for gopls semtok

View File

@ -92,6 +92,19 @@ var GeneratedAPIJSON = &APIJSON{
Status: "experimental",
Hierarchy: "build",
},
{
Name: "experimentalTemplateSupport",
Type: "bool",
Doc: "experimentalTemplateSupport opts into the experimental support\nfor template files.\n",
EnumKeys: EnumKeys{
ValueType: "",
Keys: nil,
},
EnumValues: nil,
Default: "false",
Status: "experimental",
Hierarchy: "build",
},
{
Name: "experimentalPackageCacheKey",
Type: "bool",

View File

@ -99,7 +99,8 @@ func DefaultOptions() *Options {
protocol.SourceOrganizeImports: true,
protocol.QuickFix: true,
},
Sum: {},
Sum: {},
Tmpl: {},
},
SupportedCommands: commands,
},
@ -242,6 +243,10 @@ type BuildOptions struct {
// for multi-module workspaces.
ExperimentalWorkspaceModule bool `status:"experimental"`
// ExperimentalTemplateSupport opts into the experimental support
// for template files.
ExperimentalTemplateSupport bool `status:"experimental"`
// ExperimentalPackageCacheKey controls whether to use a coarser cache key
// for package type information to increase cache hits. This setting removes
// the user's environment, build flags, and working directory from the cache
@ -715,6 +720,7 @@ func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer, enabled bool) {
func (o *Options) enableAllExperiments() {
o.SemanticTokens = true
o.ExperimentalPostfixCompletions = true
o.ExperimentalTemplateSupport = true
}
func (o *Options) enableAllExperimentMaps() {
@ -899,6 +905,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{})
case "experimentalWorkspaceModule":
result.setBool(&o.ExperimentalWorkspaceModule)
case "experimentalTemplateSupport":
result.setBool(&o.ExperimentalTemplateSupport)
case "experimentalDiagnosticsDelay":
result.setDuration(&o.ExperimentalDiagnosticsDelay)

View File

@ -168,14 +168,21 @@ func DetectLanguage(langID, filename string) FileKind {
return Mod
case "go.sum":
return Sum
case "tmpl":
return Tmpl
}
// Fallback to detecting the language based on the file extension.
switch filepath.Ext(filename) {
switch ext := filepath.Ext(filename); ext {
case ".mod":
return Mod
case ".sum":
return Sum
default: // fallback to Go
default:
if strings.HasSuffix(ext, "tmpl") {
// .tmpl, .gotmpl, etc
return Tmpl
}
// It's a Go file, or we shouldn't be seeing it
return Go
}
}
@ -186,6 +193,8 @@ func (k FileKind) String() string {
return "go.mod"
case Sum:
return "go.sum"
case Tmpl:
return "tmpl"
default:
return "go"
}

View File

@ -69,6 +69,9 @@ type Snapshot interface {
// workspace.
IgnoredFile(uri span.URI) bool
// Templates returns the .tmpl files
Templates() map[span.URI]VersionedFileHandle
// ParseGo returns the parsed AST for the file.
// If the file is not available, returns nil and an error.
ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error)
@ -509,6 +512,8 @@ const (
Mod
// Sum is a go.sum file.
Sum
// Tmpl is a template file.
Tmpl
)
// Analyzer represents a go/analysis analyzer with some boolean properties

View File

@ -11,18 +11,24 @@ import (
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/template"
)
func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) {
ctx, done := event.Start(ctx, "lsp.Server.documentSymbol")
defer done()
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
defer release()
if !ok {
return []interface{}{}, err
}
docSymbols, err := source.DocumentSymbols(ctx, snapshot, fh)
var docSymbols []protocol.DocumentSymbol
if fh.Kind() == source.Tmpl {
docSymbols, err = template.DocumentSymbols(snapshot, fh)
} else {
docSymbols, err = source.DocumentSymbols(ctx, snapshot, fh)
}
if err != nil {
event.Error(ctx, "DocumentSymbols failed", err, tag.URI.Of(fh.URI()))
return []interface{}{}, nil

View File

@ -0,0 +1,21 @@
// 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"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/source/completion"
)
func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, pos protocol.Position, context protocol.CompletionContext) ([]completion.CompletionItem, *completion.Selection, error) {
if skipTemplates(snapshot) {
return nil, nil, nil
}
return nil, nil, fmt.Errorf("implement template completion")
}

View File

@ -0,0 +1,99 @@
// 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"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
)
func Highlight(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.DocumentHighlight, error) {
if skipTemplates(snapshot) {
return nil, nil
}
buf, err := fh.Read()
if err != nil {
return nil, err
}
p := parseBuffer(buf)
pos := p.FromPosition(loc)
var ans []protocol.DocumentHighlight
if p.ParseErr == nil {
for _, s := range p.symbols {
if s.start <= pos && pos < s.start+s.length {
return markSymbols(p, s)
}
}
}
// these tokens exist whether or not there was a parse error
// (symbols require a successful parse)
for _, tok := range p.tokens {
if tok.Start <= pos && pos < tok.End {
wordAt := findWordAt(p, pos)
if len(wordAt) > 0 {
return markWordInToken(p, wordAt)
}
}
}
// find the 'word' at pos, etc: someday
// until then we get the default action, which doesn't respect word boundaries
return ans, nil
}
func markSymbols(p *Parsed, sym symbol) ([]protocol.DocumentHighlight, error) {
var ans []protocol.DocumentHighlight
for _, s := range p.symbols {
if s.name == sym.name {
kind := protocol.Read
if s.vardef {
kind = protocol.Write
}
ans = append(ans, protocol.DocumentHighlight{
Range: p.Range(s.start, s.length),
Kind: kind,
})
}
}
return ans, nil
}
// A token is {{...}}, and this marks words in the token that equal the give word
func markWordInToken(p *Parsed, wordAt string) ([]protocol.DocumentHighlight, error) {
var ans []protocol.DocumentHighlight
pat, err := regexp.Compile(fmt.Sprintf(`\b%s\b`, wordAt))
if err != nil {
return nil, fmt.Errorf("%q: unmatchable word (%v)", wordAt, err)
}
for _, tok := range p.tokens {
got := pat.FindAllIndex(p.buf[tok.Start:tok.End], -1)
for i := 0; i < len(got); i++ {
ans = append(ans, protocol.DocumentHighlight{
Range: p.Range(got[i][0], got[i][1]-got[i][0]),
Kind: protocol.Text,
})
}
}
return ans, nil
}
var wordRe = regexp.MustCompile(`[$]?\w+$`)
var moreRe = regexp.MustCompile(`^[$]?\w+`)
// findWordAt finds the word the cursor is in (meaning in or just before)
func findWordAt(p *Parsed, pos int) string {
if pos >= len(p.buf) {
return "" // can't happen, as we are called with pos < tok.End
}
after := moreRe.Find(p.buf[pos:])
if len(after) == 0 {
return "" // end of the word
}
got := wordRe.Find(p.buf[:pos+len(after)])
return string(got)
}

View File

@ -0,0 +1,202 @@
// 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, error)) (*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, err := d()
if err != nil {
// this is an internal error, likely caused by a typo
// for a token or modifier
return nil, err
}
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

View File

@ -0,0 +1,439 @@
// 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 contains code for dealing with templates
package template
// template files are small enough that the code reprocesses them each time
// this may be a bad choice for projects with lots of template files.
// This file contains the parsing code, some debugging printing, and
// implementations for Diagnose, Definition, HJover, References
import (
"bytes"
"context"
"fmt"
"io"
"log"
"regexp"
"runtime"
"sort"
"text/template"
"text/template/parse"
"unicode/utf8"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
var (
Left = []byte("{{")
Right = []byte("}}")
)
type Parsed struct {
buf []byte //contents
lines [][]byte // needed?, other than for debugging?
// tokens, computed before trying to parse
tokens []Token
// result of parsing
named []*template.Template // the template and embedded templates
ParseErr error
symbols []symbol
stack []parse.Node // used while computing symbols
// for mapping from offsets in buf to LSP coordinates
nls []int // offset of newlines before each line (nls[0]==-1)
lastnl int // last line seen
check int // used to decide whether to use lastnl or search through nls
nonASCII bool // are there any non-ascii runes in buf?
}
// Token is a single {{...}}. More precisely, Left...Right
type Token struct {
Start, End int // offset from start of template
Multiline bool
}
// All contains the Parse of all the template files
type All struct {
files map[span.URI]*Parsed
}
// New returns the Parses of the snapshot's tmpl files
// (maybe cache these, but then avoiding import cycles needs code rearrangements)
func New(tmpls map[span.URI]source.VersionedFileHandle) *All {
all := make(map[span.URI]*Parsed)
for k, v := range tmpls {
buf, err := v.Read()
if err != nil { // PJW: decide what to do with these errors
log.Printf("failed to read %s (%v)", v.URI().Filename(), err)
continue
}
all[k] = parseBuffer(buf)
}
return &All{files: all}
}
func parseBuffer(buf []byte) *Parsed {
ans := &Parsed{
buf: buf,
check: -1,
}
// how to compute allAscii...
for _, b := range buf {
if b >= utf8.RuneSelf {
ans.nonASCII = true
break
}
}
if buf[len(buf)-1] != '\n' {
ans.buf = append(buf, '\n')
}
// at the cost of complexity we could fold this into the allAscii loop
ans.lines = bytes.Split(buf, []byte{'\n'})
ans.nls = []int{-1}
for i, p := range ans.buf {
if p == '\n' {
ans.nls = append(ans.nls, i)
}
}
ans.setTokens()
t, err := template.New("").Parse(string(buf))
if err != nil {
funcs := make(template.FuncMap)
for t == nil && ans.ParseErr == nil {
// template: :2: function "foo" not defined
matches := parseErrR.FindStringSubmatch(err.Error())
if len(matches) < 2 { // uncorrectable error
ans.ParseErr = err
return ans
}
// suppress the error by giving it a function with the right name
funcs[matches[1]] = func(interface{}) interface{} { return nil }
t, err = template.New("").Funcs(funcs).Parse(string(buf))
}
}
ans.named = t.Templates()
// set the symbols
for _, t := range ans.named {
ans.stack = append(ans.stack, t.Root)
ans.findSymbols()
if t.Name() != "" {
// defining a template. The pos is just after {{define...}} (or {{block...}}?)
at, sz := ans.FindLiteralBefore(int(t.Root.Pos))
s := symbol{start: at, length: sz, name: t.Name(), kind: protocol.Namespace, vardef: true}
ans.symbols = append(ans.symbols, s)
}
}
sort.Slice(ans.symbols, func(i, j int) bool {
left, right := ans.symbols[i], ans.symbols[j]
if left.start != right.start {
return left.start < right.start
}
if left.vardef != right.vardef {
return left.vardef
}
return left.kind < right.kind
})
return ans
}
// FindLiteralBefore locates the first preceding string literal
// returning its position and length in buf
// or returns -1 if there is none. Assume "", rather than ``, for now
func (p *Parsed) FindLiteralBefore(pos int) (int, int) {
left, right := -1, -1
for i := pos - 1; i >= 0; i-- {
if p.buf[i] != '"' {
continue
}
if right == -1 {
right = i
continue
}
left = i
break
}
if left == -1 {
return -1, 0
}
return left + 1, right - left - 1
}
var parseErrR = regexp.MustCompile(`template:.*function "([^"]+)" not defined`)
func (p *Parsed) setTokens() {
last := 0
for left := bytes.Index(p.buf[last:], Left); left != -1; left = bytes.Index(p.buf[last:], Left) {
left += last
tok := Token{Start: left}
last = left + len(Left)
right := bytes.Index(p.buf[last:], Right)
if right == -1 {
break
}
right += last + len(Right)
tok.End = right
tok.Multiline = bytes.Contains(p.buf[left:right], []byte{'\n'})
p.tokens = append(p.tokens, tok)
last = right
}
}
func (p *Parsed) Tokens() []Token {
return p.tokens
}
func (p *Parsed) utf16len(buf []byte) int {
cnt := 0
if !p.nonASCII {
return len(buf)
}
// we need a utf16len(rune), but we don't have it
for _, r := range string(buf) {
cnt++
if r >= 1<<16 {
cnt++
}
}
return cnt
}
func (p *Parsed) TokenSize(t Token) (int, error) {
if t.Multiline {
return -1, fmt.Errorf("TokenSize called with Multiline token %#v", t)
}
ans := p.utf16len(p.buf[t.Start:t.End])
return ans, nil
}
// RuneCount counts runes in a line
func (p *Parsed) RuneCount(l, s, e uint32) uint32 {
start := p.nls[l] + 1 + int(s)
end := int(e)
if e == 0 || int(e) >= p.nls[l+1] {
end = p.nls[l+1]
}
return uint32(utf8.RuneCount(p.buf[start:end]))
}
// LineCol converts from a 0-based byte offset to 0-based line, col. col in runes
func (p *Parsed) LineCol(x int) (uint32, uint32) {
if x < p.check {
p.lastnl = 0
}
p.check = x
for i := p.lastnl; i < len(p.nls); i++ {
if p.nls[i] <= x {
continue
}
p.lastnl = i
var count int
if i > 0 && x == p.nls[i-1] { // \n
count = 0
} else {
count = p.utf16len(p.buf[p.nls[i-1]+1 : x])
}
return uint32(i - 1), uint32(count)
}
if x == len(p.buf)-1 { // trailing \n
return uint32(len(p.nls)), 1
}
// shouldn't happen
for i := 1; i < 4; i++ {
_, f, l, ok := runtime.Caller(i)
if !ok {
break
}
log.Printf("%d: %s:%d", i, f, l)
}
msg := fmt.Errorf("LineCol off the end, %d of %d, nls=%v, %q", x, len(p.buf), p.nls, p.buf[x:])
event.Error(context.Background(), "internal error", msg)
return 0, 0
}
// Position produces a protocol.Position from an offset in the template
func (p *Parsed) Position(pos int) protocol.Position {
line, col := p.LineCol(pos)
return protocol.Position{Line: line, Character: col}
}
func (p *Parsed) Range(x, length int) protocol.Range {
line, col := p.LineCol(x)
ans := protocol.Range{
Start: protocol.Position{Line: line, Character: col},
End: protocol.Position{Line: line, Character: col + uint32(length)},
}
return ans
}
// FromPosition translates a protocol.Position into an offset into the template
func (p *Parsed) FromPosition(x protocol.Position) int {
l, c := int(x.Line), int(x.Character)
line := p.buf[p.nls[l]+1:]
cnt := 0
for w := range string(line) {
if cnt >= c {
return w + p.nls[l] + 1
}
cnt++
}
// do we get here? NO
pos := int(x.Character) + p.nls[int(x.Line)] + 1
event.Error(context.Background(), "internal error", fmt.Errorf("surprise %#v", x))
return pos
}
func symAtPosition(fh source.FileHandle, loc protocol.Position) (*symbol, *Parsed, error) {
buf, err := fh.Read()
if err != nil {
return nil, nil, err
}
p := parseBuffer(buf)
pos := p.FromPosition(loc)
syms := p.SymsAtPos(pos)
if len(syms) == 0 {
return nil, p, fmt.Errorf("no symbol found")
}
if len(syms) > 1 {
log.Printf("Hover: %d syms, not 1 %v", len(syms), syms)
}
sym := syms[0]
return &sym, p, nil
}
func (p *Parsed) SymsAtPos(pos int) []symbol {
ans := []symbol{}
for _, s := range p.symbols {
if s.start <= pos && pos < s.start+s.length {
ans = append(ans, s)
}
}
return ans
}
type wrNode struct {
p *Parsed
w io.Writer
}
// WriteNode is for debugging
func (p *Parsed) WriteNode(w io.Writer, n parse.Node) {
wr := wrNode{p: p, w: w}
wr.writeNode(n, "")
}
func (wr wrNode) writeNode(n parse.Node, indent string) {
if n == nil {
return
}
at := func(pos parse.Pos) string {
line, col := wr.p.LineCol(int(pos))
return fmt.Sprintf("(%d)%v:%v", pos, line, col)
}
switch x := n.(type) {
case *parse.ActionNode:
fmt.Fprintf(wr.w, "%sActionNode at %s\n", indent, at(x.Pos))
wr.writeNode(x.Pipe, indent+". ")
case *parse.BoolNode:
fmt.Fprintf(wr.w, "%sBoolNode at %s, %v\n", indent, at(x.Pos), x.True)
case *parse.BranchNode:
fmt.Fprintf(wr.w, "%sBranchNode at %s\n", indent, at(x.Pos))
wr.writeNode(x.Pipe, indent+"Pipe. ")
wr.writeNode(x.List, indent+"List. ")
wr.writeNode(x.ElseList, indent+"Else. ")
case *parse.ChainNode:
fmt.Fprintf(wr.w, "%sChainNode at %s, %v\n", indent, at(x.Pos), x.Field)
case *parse.CommandNode:
fmt.Fprintf(wr.w, "%sCommandNode at %s, %d children\n", indent, at(x.Pos), len(x.Args))
for _, a := range x.Args {
wr.writeNode(a, indent+". ")
}
//case *parse.CommentNode: // 1.16
case *parse.DotNode:
fmt.Fprintf(wr.w, "%sDotNode at %s\n", indent, at(x.Pos))
case *parse.FieldNode:
fmt.Fprintf(wr.w, "%sFieldNode at %s, %v\n", indent, at(x.Pos), x.Ident)
case *parse.IdentifierNode:
fmt.Fprintf(wr.w, "%sIdentifierNode at %s, %v\n", indent, at(x.Pos), x.Ident)
case *parse.IfNode:
fmt.Fprintf(wr.w, "%sIfNode at %s\n", indent, at(x.Pos))
wr.writeNode(&x.BranchNode, indent+". ")
case *parse.ListNode:
if x == nil {
return // nil BranchNode.ElseList
}
fmt.Fprintf(wr.w, "%sListNode at %s, %d children\n", indent, at(x.Pos), len(x.Nodes))
for _, n := range x.Nodes {
wr.writeNode(n, indent+". ")
}
case *parse.NilNode:
fmt.Fprintf(wr.w, "%sNilNode at %s\n", indent, at(x.Pos))
case *parse.NumberNode:
fmt.Fprintf(wr.w, "%sNumberNode at %s, %s\n", indent, at(x.Pos), x.Text)
case *parse.PipeNode:
if x == nil {
return // {{template "xxx"}}
}
fmt.Fprintf(wr.w, "%sPipeNode at %s, %d vars, %d cmds, IsAssign:%v\n",
indent, at(x.Pos), len(x.Decl), len(x.Cmds), x.IsAssign)
for _, d := range x.Decl {
wr.writeNode(d, indent+"Decl. ")
}
for _, c := range x.Cmds {
wr.writeNode(c, indent+"Cmd. ")
}
case *parse.RangeNode:
fmt.Fprintf(wr.w, "%sRangeNode at %s\n", indent, at(x.Pos))
wr.writeNode(&x.BranchNode, indent+". ")
case *parse.StringNode:
fmt.Fprintf(wr.w, "%sStringNode at %s, %s\n", indent, at(x.Pos), x.Quoted)
case *parse.TemplateNode:
fmt.Fprintf(wr.w, "%sTemplateNode at %s, %s\n", indent, at(x.Pos), x.Name)
wr.writeNode(x.Pipe, indent+". ")
case *parse.TextNode:
fmt.Fprintf(wr.w, "%sTextNode at %s, len %d\n", indent, at(x.Pos), len(x.Text))
case *parse.VariableNode:
fmt.Fprintf(wr.w, "%sVariableNode at %s, %v\n", indent, at(x.Pos), x.Ident)
case *parse.WithNode:
fmt.Fprintf(wr.w, "%sWithNode at %s\n", indent, at(x.Pos))
wr.writeNode(&x.BranchNode, indent+". ")
}
}
// short prints at most 40 bytes of node.String(), for debugging
func short(n parse.Node) (ret string) {
defer func() {
if x := recover(); x != nil {
// all because of typed nils
ret = "NIL"
}
}()
s := n.String()
if len(s) > 40 {
return s[:40] + "..."
}
return s
}
var kindNames = []string{"", "File", "Module", "Namespace", "Package", "Class", "Method", "Property",
"Field", "Constructor", "Enum", "Interface", "Function", "Variable", "Constant", "String",
"Number", "Boolean", "Array", "Object", "Key", "Null", "EnumMember", "Struct", "Event",
"Operator", "TypeParameter"}
func kindStr(k protocol.SymbolKind) string {
n := int(k)
if n < 1 || n >= len(kindNames) {
return fmt.Sprintf("?SymbolKind %d?", n)
}
return kindNames[n]
}

View File

@ -0,0 +1,192 @@
// 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 (
"strings"
"testing"
)
type datum struct {
buf string
cnt int
syms []string // the symbols in the parse of buf
}
var tmpl = []datum{{`
{{if (foo .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}}
{{$A.X 12}}
{{foo (.X.Y) 23 ($A.)}}
{{end}}`, 1, []string{"{7,3,foo,Function,false}", "{12,1,X,Method,false}",
"{14,1,Y,Method,false}", "{21,2,$A,Variable,true}", "{26,2,,String,false}",
"{35,1,Z,Method,false}", "{38,2,$A,Variable,false}",
"{53,2,$A,Variable,false}", "{56,1,X,Method,false}", "{57,2,,Number,false}",
"{64,3,foo,Function,false}", "{70,1,X,Method,false}",
"{72,1,Y,Method,false}", "{75,2,,Number,false}", "{80,2,$A,Variable,false}",
"{83,2,Zü,Method,false}", "{94,3,,Constant,false}"}},
{`{{define "zzz"}}{{.}}{{end}}
{{template "zzz"}}`, 2, []string{"{10,3,zzz,Namespace,true}", "{18,1,dot,Variable,false}",
"{41,3,zzz,Package,false}"}},
{`{{block "aaa" foo}}b{{end}}`, 2, []string{"{9,3,aaa,Namespace,true}",
"{9,3,aaa,Package,false}", "{14,3,foo,Function,false}", "{19,1,,Constant,false}"}},
}
func TestSymbols(t *testing.T) {
for i, x := range tmpl {
got := parseBuffer([]byte(x.buf))
if got.ParseErr != nil {
t.Errorf("error:%v", got.ParseErr)
continue
}
if len(got.named) != x.cnt {
t.Errorf("%d: got %d, expected %d", i, len(got.named), x.cnt)
}
for n, s := range got.symbols {
if s.String() != x.syms[n] {
t.Errorf("%d: got %s, expected %s", i, s.String(), x.syms[n])
}
}
}
}
func TestWordAt(t *testing.T) {
want := []string{"", "", "if", "if", "", "$A", "$A", "", "", "B", "", "", "end", "end", "end", "", ""}
p := parseBuffer([]byte("{{if $A}}B{{end}}"))
for i := 0; i < len(want); i++ {
got := findWordAt(p, i)
if got != want[i] {
t.Errorf("for %d, got %q, wanted %q", i, got, want[i])
}
}
}
func TestNLS(t *testing.T) {
buf := `{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}}
{{$A.X 12}}
{{foo (.X.Y) 23 ($A.Z)}}
{{end}}
`
p := parseBuffer([]byte(buf))
if p.ParseErr != nil {
t.Fatal(p.ParseErr)
}
// line 0 doesn't have a \n in front of it
for i := 1; i < len(p.nls)-1; i++ {
if buf[p.nls[i]] != '\n' {
t.Errorf("line %d got %c", i, buf[p.nls[i]])
}
}
// fake line at end of file
if p.nls[len(p.nls)-1] != len(buf) {
t.Errorf("got %d expected %d", p.nls[len(p.nls)-1], len(buf))
}
}
func TestLineCol(t *testing.T) {
buf := `{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}}
{{$A.X 12}}
{{foo (.X.Y) 23 ($A.Z)}}
{{end}}`
if false {
t.Error(buf)
}
for n, cx := range tmpl {
buf := cx.buf
p := parseBuffer([]byte(buf))
if p.ParseErr != nil {
t.Fatal(p.ParseErr)
}
type loc struct {
offset int
l, c uint32
}
saved := []loc{}
// forwards
var lastl, lastc uint32
for offset := range buf {
l, c := p.LineCol(offset)
saved = append(saved, loc{offset, l, c})
if l > lastl {
lastl = l
if c != 0 {
t.Errorf("line %d, got %d instead of 0", l, c)
}
}
if c > lastc {
lastc = c
}
}
lines := strings.Split(buf, "\n")
mxlen := -1
for _, l := range lines {
if len(l) > mxlen {
mxlen = len(l)
}
}
if int(lastl) != len(lines)-1 && int(lastc) != mxlen {
// lastl is 0 if there is only 1 line(?)
t.Errorf("expected %d, %d, got %d, %d for case %d", len(lines)-1, mxlen, lastl, lastc, n)
}
// backwards
for j := len(saved) - 1; j >= 0; j-- {
s := saved[j]
xl, xc := p.LineCol(s.offset)
if xl != s.l || xc != s.c {
t.Errorf("at offset %d(%d), got (%d,%d), expected (%d,%d)", s.offset, j, xl, xc, s.l, s.c)
}
}
}
}
func TestPos(t *testing.T) {
buf := `
{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}}
{{$A.X 12}}
{{foo (.X.Y) 23 ($A.Z)}}
{{end}}`
p := parseBuffer([]byte(buf))
if p.ParseErr != nil {
t.Fatal(p.ParseErr)
}
for pos, r := range buf {
if r == '\n' {
continue
}
x := p.Position(pos)
n := p.FromPosition(x)
if n != pos {
// once it's wrong, it will be wrong forever
t.Fatalf("at pos %d (rune %c) got %d {%#v]", pos, r, n, x)
}
}
}
func TestLen(t *testing.T) {
data := []struct {
cnt int
v string
}{{1, "a"}, {1, "膈"}, {4, "😆🥸"}, {7, "3😀4567"}}
p := &Parsed{nonASCII: true}
for _, d := range data {
got := p.utf16len([]byte(d.v))
if got != d.cnt {
t.Errorf("%v, got %d wanted %d", d, got, d.cnt)
}
}
}
func TestUtf16(t *testing.T) {
buf := `
{{if (foÜx .X.Y)}}😀{{$A := "hi"}}{{.Z $A}}{{else}}
{{$A.X 12}}
{{foo (.X.Y) 23 ($A.Z)}}
{{end}}`
p := parseBuffer([]byte(buf))
if p.nonASCII == false {
t.Error("expected nonASCII to be true")
}
}

View File

@ -0,0 +1,225 @@
// 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 (
"bytes"
"fmt"
"text/template/parse"
"unicode/utf8"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
)
// in local coordinates, to be translated to protocol.DocumentSymbol
type symbol struct {
start int // for sorting
length int // in runes (unicode code points)
name string
kind protocol.SymbolKind
vardef bool // is this a variable definition?
// do we care about selection range, or children?
// no children yet, and selection range is the same as range
}
func (s symbol) String() string {
return fmt.Sprintf("{%d,%d,%s,%s,%v}", s.start, s.length, s.name, s.kind, s.vardef)
}
// for FieldNode or VariableNode (or ChainNode?)
func (p *Parsed) fields(flds []string, x parse.Node) []symbol {
ans := []symbol{}
// guessing that there are no embedded blanks allowed. The doc is unclear
lookfor := ""
switch x.(type) {
case *parse.FieldNode:
for _, f := range flds {
lookfor += "." + f // quadratic, but probably ok
}
case *parse.VariableNode:
lookfor = flds[0]
for i := 1; i < len(flds); i++ {
lookfor += "." + flds[i]
}
case *parse.ChainNode: // PJW, what are these?
for _, f := range flds {
lookfor += "." + f // quadratic, but probably ok
}
default:
panic(fmt.Sprintf("%T unexpected in fields()", x))
}
if len(lookfor) == 0 {
panic(fmt.Sprintf("no strings in fields() %#v", x))
}
startsAt := int(x.Position())
ix := bytes.Index(p.buf[startsAt:], []byte(lookfor)) // HasPrefix? PJW?
if ix < 0 || ix > len(lookfor) { // lookfor expected to be at start (or so)
// probably golang.go/#43388, so back up
startsAt -= len(flds[0]) + 1
ix = bytes.Index(p.buf[startsAt:], []byte(lookfor)) // ix might be 1? PJW
if ix < 0 {
return ans
}
}
at := ix + startsAt
for _, f := range flds {
at += 1 // .
kind := protocol.Method
if f[0] == '$' {
kind = protocol.Variable
}
sym := symbol{name: f, kind: kind, start: at, length: utf8.RuneCount([]byte(f))}
if kind == protocol.Variable && len(p.stack) > 1 {
if pipe, ok := p.stack[len(p.stack)-2].(*parse.PipeNode); ok {
for _, y := range pipe.Decl {
if x == y {
sym.vardef = true
}
}
}
}
ans = append(ans, sym)
at += len(f)
}
return ans
}
func (p *Parsed) findSymbols() {
if len(p.stack) == 0 {
return
}
n := p.stack[len(p.stack)-1]
pop := func() {
p.stack = p.stack[:len(p.stack)-1]
}
if n == nil { // allowing nil simplifies the code
pop()
return
}
nxt := func(nd parse.Node) {
p.stack = append(p.stack, nd)
p.findSymbols()
}
switch x := n.(type) {
case *parse.ActionNode:
nxt(x.Pipe)
case *parse.BoolNode:
// need to compute the length from the value
msg := fmt.Sprintf("%v", x.True)
p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(msg), kind: protocol.Boolean})
case *parse.BranchNode:
nxt(x.Pipe)
nxt(x.List)
nxt(x.ElseList)
case *parse.ChainNode:
p.symbols = append(p.symbols, p.fields(x.Field, x)...)
nxt(x.Node)
case *parse.CommandNode:
for _, a := range x.Args {
nxt(a)
}
//case *parse.CommentNode: // go 1.16
// log.Printf("implement %d", x.Type())
case *parse.DotNode:
sym := symbol{name: "dot", kind: protocol.Variable, start: int(x.Pos), length: 1}
p.symbols = append(p.symbols, sym)
case *parse.FieldNode:
p.symbols = append(p.symbols, p.fields(x.Ident, x)...)
case *parse.IdentifierNode:
sym := symbol{name: x.Ident, kind: protocol.Function, start: int(x.Pos),
length: utf8.RuneCount([]byte(x.Ident))}
p.symbols = append(p.symbols, sym)
case *parse.IfNode:
nxt(&x.BranchNode)
case *parse.ListNode:
if x != nil { // wretched typed nils. Node should have an IfNil
for _, nd := range x.Nodes {
nxt(nd)
}
}
case *parse.NilNode:
sym := symbol{name: "nil", kind: protocol.Constant, start: int(x.Pos), length: 3}
p.symbols = append(p.symbols, sym)
case *parse.NumberNode:
// no name; ascii
p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(x.Text), kind: protocol.Number})
case *parse.PipeNode:
if x == nil { // {{template "foo"}}
return
}
for _, d := range x.Decl {
nxt(d)
}
for _, c := range x.Cmds {
nxt(c)
}
case *parse.RangeNode:
nxt(&x.BranchNode)
case *parse.StringNode:
// no name
sz := utf8.RuneCount([]byte(x.Text))
p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.String})
case *parse.TemplateNode: // invoking a template
// x.Pos points to the quote before the name
p.symbols = append(p.symbols, symbol{name: x.Name, kind: protocol.Package, start: int(x.Pos) + 1,
length: utf8.RuneCount([]byte(x.Name))})
nxt(x.Pipe)
case *parse.TextNode:
if len(x.Text) == 1 && x.Text[0] == '\n' {
break
}
// nothing to report, but build one for hover
sz := utf8.RuneCount([]byte(x.Text))
p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.Constant})
case *parse.VariableNode:
p.symbols = append(p.symbols, p.fields(x.Ident, x)...)
case *parse.WithNode:
nxt(&x.BranchNode)
}
pop()
}
// DocumentSymbols returns a heirarchy of the symbols defined in a template file.
// (The heirarchy is flat. SymbolInformation might be better.)
func DocumentSymbols(snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentSymbol, error) {
if skipTemplates(snapshot) {
return nil, nil
}
buf, err := fh.Read()
if err != nil {
return nil, err
}
p := parseBuffer(buf)
if p.ParseErr != nil {
return nil, p.ParseErr
}
var ans []protocol.DocumentSymbol
for _, s := range p.symbols {
if s.kind == protocol.Constant {
continue
}
d := kindStr(s.kind)
if d == "Namespace" {
d = "Template"
}
if s.vardef {
d += "(def)"
} else {
d += "(use)"
}
r := p.Range(s.start, s.length)
y := protocol.DocumentSymbol{
Name: s.name,
Detail: d,
Kind: s.kind,
Range: r,
SelectionRange: r, // or should this be the entire {{...}}?
}
ans = append(ans, y)
}
return ans, nil
}

View File

@ -233,7 +233,8 @@ func DefaultOptions(o *source.Options) {
source.Mod: {
protocol.SourceOrganizeImports: true,
},
source.Sum: {},
source.Sum: {},
source.Tmpl: {},
}
o.UserOptions.Codelenses[string(command.Test)] = true
o.HoverKind = source.SynopsisDocumentation