| // 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" |
| "context" |
| "fmt" |
| "text/template/parse" |
| "unicode/utf8" |
| |
| "golang.org/x/tools/internal/event" |
| "golang.org/x/tools/gopls/internal/lsp/protocol" |
| "golang.org/x/tools/gopls/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: |
| // If these happen they will happen even if gopls is restarted |
| // and the users does the same thing, so it is better not to panic. |
| // context.Background() is used because we don't have access |
| // to any other context. [we could, but it would be complicated] |
| event.Log(context.Background(), fmt.Sprintf("%T unexpected in fields()", x)) |
| return nil |
| } |
| if len(lookfor) == 0 { |
| event.Log(context.Background(), fmt.Sprintf("no strings in fields() %#v", x)) |
| return nil |
| } |
| 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 hierarchy of the symbols defined in a template file. |
| // (The hierarchy is flat. SymbolInformation might be better.) |
| func DocumentSymbols(snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentSymbol, error) { |
| 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 |
| } |