mirror of https://github.com/golang/go.git
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:
parent
f03daeacec
commit
7cab0ef2e9
|
|
@ -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.**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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.Zü)}}
|
||||
{{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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue