diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 963ef002dd..449a362291 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -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.** diff --git a/gopls/internal/regtest/misc/template_test.go b/gopls/internal/regtest/misc/template_test.go new file mode 100644 index 0000000000..2bb61fbcc0 --- /dev/null +++ b/gopls/internal/regtest/misc/template_test.go @@ -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 diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go index 50de35ee6b..f182ffaec3 100644 --- a/internal/lsp/cache/snapshot.go +++ b/internal/lsp/cache/snapshot.go @@ -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 diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index 8c273445a0..ad538520e8 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -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) diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go index 7a389b5878..ac22bc072d 100644 --- a/internal/lsp/code_action.go +++ b/internal/lsp/code_action.go @@ -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) } diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index fa0586a306..4bec6cda99 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -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)) diff --git a/internal/lsp/definition.go b/internal/lsp/definition.go index 46643e1708..a80f07ef70 100644 --- a/internal/lsp/definition.go +++ b/internal/lsp/definition.go @@ -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 diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index f892910c26..300b731fd5 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -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 { diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go index 9fc897bc87..a350dd54de 100644 --- a/internal/lsp/highlight.go +++ b/internal/lsp/highlight.go @@ -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)) diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index bc4719dfda..1e118bc3a8 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -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 } diff --git a/internal/lsp/references.go b/internal/lsp/references.go index 5ad83c9b21..d8f2f1e283 100644 --- a/internal/lsp/references.go +++ b/internal/lsp/references.go @@ -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 diff --git a/internal/lsp/semantic.go b/internal/lsp/semantic.go index 029e5bf04e..2399cc85b7 100644 --- a/internal/lsp/semantic.go +++ b/internal/lsp/semantic.go @@ -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 diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index 3cad876e26..bec11d57a2 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -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", diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index 702f5f9578..626b1c2c0b 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -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) diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go index 69ddb17684..a870732318 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -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" } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index b0aebbba17..51fe3eaa1e 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.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 diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go index 435eac1e7f..5bde1bdd47 100644 --- a/internal/lsp/symbols.go +++ b/internal/lsp/symbols.go @@ -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 diff --git a/internal/lsp/template/completion.go b/internal/lsp/template/completion.go new file mode 100644 index 0000000000..a593bf5732 --- /dev/null +++ b/internal/lsp/template/completion.go @@ -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") +} diff --git a/internal/lsp/template/highlight.go b/internal/lsp/template/highlight.go new file mode 100644 index 0000000000..65256fc69d --- /dev/null +++ b/internal/lsp/template/highlight.go @@ -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) +} diff --git a/internal/lsp/template/implementations.go b/internal/lsp/template/implementations.go new file mode 100644 index 0000000000..6cdab9ece7 --- /dev/null +++ b/internal/lsp/template/implementations.go @@ -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 diff --git a/internal/lsp/template/parse.go b/internal/lsp/template/parse.go new file mode 100644 index 0000000000..2b9c7046ae --- /dev/null +++ b/internal/lsp/template/parse.go @@ -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] +} diff --git a/internal/lsp/template/parse_test.go b/internal/lsp/template/parse_test.go new file mode 100644 index 0000000000..e6a95eff4e --- /dev/null +++ b/internal/lsp/template/parse_test.go @@ -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") + } +} diff --git a/internal/lsp/template/symbols.go b/internal/lsp/template/symbols.go new file mode 100644 index 0000000000..856f6e335f --- /dev/null +++ b/internal/lsp/template/symbols.go @@ -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 +} diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index e46c19912e..a6e0a264b8 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -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