aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/template/completion.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/template/completion.go')
-rw-r--r--gopls/internal/lsp/template/completion.go287
1 files changed, 287 insertions, 0 deletions
diff --git a/gopls/internal/lsp/template/completion.go b/gopls/internal/lsp/template/completion.go
new file mode 100644
index 000000000..292563a88
--- /dev/null
+++ b/gopls/internal/lsp/template/completion.go
@@ -0,0 +1,287 @@
+// 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"
+ "go/scanner"
+ "go/token"
+ "strings"
+
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+)
+
+// information needed for completion
+type completer struct {
+ p *Parsed
+ pos protocol.Position
+ offset int // offset of the start of the Token
+ ctx protocol.CompletionContext
+ syms map[string]symbol
+}
+
+func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) {
+ all := New(snapshot.Templates())
+ var start int // the beginning of the Token (completed or not)
+ syms := make(map[string]symbol)
+ var p *Parsed
+ for fn, fc := range all.files {
+ // collect symbols from all template files
+ filterSyms(syms, fc.symbols)
+ if fn.Filename() != fh.URI().Filename() {
+ continue
+ }
+ if start = inTemplate(fc, pos); start == -1 {
+ return nil, nil
+ }
+ p = fc
+ }
+ if p == nil {
+ // this cannot happen unless the search missed a template file
+ return nil, fmt.Errorf("%s not found", fh.FileIdentity().URI.Filename())
+ }
+ c := completer{
+ p: p,
+ pos: pos,
+ offset: start + len(Left),
+ ctx: context,
+ syms: syms,
+ }
+ return c.complete()
+}
+
+func filterSyms(syms map[string]symbol, ns []symbol) {
+ for _, xsym := range ns {
+ switch xsym.kind {
+ case protocol.Method, protocol.Package, protocol.Boolean, protocol.Namespace,
+ protocol.Function:
+ syms[xsym.name] = xsym // we don't care which symbol we get
+ case protocol.Variable:
+ if xsym.name != "dot" {
+ syms[xsym.name] = xsym
+ }
+ case protocol.Constant:
+ if xsym.name == "nil" {
+ syms[xsym.name] = xsym
+ }
+ }
+ }
+}
+
+// return the starting position of the enclosing token, or -1 if none
+func inTemplate(fc *Parsed, pos protocol.Position) int {
+ // pos is the pos-th character. if the cursor is at the beginning
+ // of the file, pos is 0. That is, we've only seen characters before pos
+ // 1. pos might be in a Token, return tk.Start
+ // 2. pos might be after an elided but before a Token, return elided
+ // 3. return -1 for false
+ offset := fc.FromPosition(pos)
+ // this could be a binary search, as the tokens are ordered
+ for _, tk := range fc.tokens {
+ if tk.Start < offset && offset <= tk.End {
+ return tk.Start
+ }
+ }
+ for _, x := range fc.elided {
+ if x > offset {
+ // fc.elided is sorted
+ break
+ }
+ // If the interval [x,offset] does not contain Left or Right
+ // then provide completions. (do we need the test for Right?)
+ if !bytes.Contains(fc.buf[x:offset], []byte(Left)) && !bytes.Contains(fc.buf[x:offset], []byte(Right)) {
+ return x
+ }
+ }
+ return -1
+}
+
+var (
+ keywords = []string{"if", "with", "else", "block", "range", "template", "end}}", "end"}
+ globals = []string{"and", "call", "html", "index", "slice", "js", "len", "not", "or",
+ "urlquery", "printf", "println", "print", "eq", "ne", "le", "lt", "ge", "gt"}
+)
+
+// find the completions. start is the offset of either the Token enclosing pos, or where
+// the incomplete token starts.
+// The error return is always nil.
+func (c *completer) complete() (*protocol.CompletionList, error) {
+ ans := &protocol.CompletionList{IsIncomplete: true, Items: []protocol.CompletionItem{}}
+ start := c.p.FromPosition(c.pos)
+ sofar := c.p.buf[c.offset:start]
+ if len(sofar) == 0 || sofar[len(sofar)-1] == ' ' || sofar[len(sofar)-1] == '\t' {
+ return ans, nil
+ }
+ // sofar could be parsed by either c.analyzer() or scan(). The latter is precise
+ // and slower, but fast enough
+ words := scan(sofar)
+ // 1. if pattern starts $, show variables
+ // 2. if pattern starts ., show methods (and . by itself?)
+ // 3. if len(words) == 1, show firstWords (but if it were a |, show functions and globals)
+ // 4. ...? (parenthetical expressions, arguments, ...) (packages, namespaces, nil?)
+ if len(words) == 0 {
+ return nil, nil // if this happens, why were we called?
+ }
+ pattern := string(words[len(words)-1])
+ if pattern[0] == '$' {
+ // should we also return a raw "$"?
+ for _, s := range c.syms {
+ if s.kind == protocol.Variable && weakMatch(s.name, pattern) > 0 {
+ ans.Items = append(ans.Items, protocol.CompletionItem{
+ Label: s.name,
+ Kind: protocol.VariableCompletion,
+ Detail: "Variable",
+ })
+ }
+ }
+ return ans, nil
+ }
+ if pattern[0] == '.' {
+ for _, s := range c.syms {
+ if s.kind == protocol.Method && weakMatch("."+s.name, pattern) > 0 {
+ ans.Items = append(ans.Items, protocol.CompletionItem{
+ Label: s.name,
+ Kind: protocol.MethodCompletion,
+ Detail: "Method/member",
+ })
+ }
+ }
+ return ans, nil
+ }
+ // could we get completion attempts in strings or numbers, and if so, do we care?
+ // globals
+ for _, kw := range globals {
+ if weakMatch(kw, string(pattern)) != 0 {
+ ans.Items = append(ans.Items, protocol.CompletionItem{
+ Label: kw,
+ Kind: protocol.KeywordCompletion,
+ Detail: "Function",
+ })
+ }
+ }
+ // and functions
+ for _, s := range c.syms {
+ if s.kind == protocol.Function && weakMatch(s.name, pattern) != 0 {
+ ans.Items = append(ans.Items, protocol.CompletionItem{
+ Label: s.name,
+ Kind: protocol.FunctionCompletion,
+ Detail: "Function",
+ })
+ }
+ }
+ // keywords if we're at the beginning
+ if len(words) <= 1 || len(words[len(words)-2]) == 1 && words[len(words)-2][0] == '|' {
+ for _, kw := range keywords {
+ if weakMatch(kw, string(pattern)) != 0 {
+ ans.Items = append(ans.Items, protocol.CompletionItem{
+ Label: kw,
+ Kind: protocol.KeywordCompletion,
+ Detail: "keyword",
+ })
+ }
+ }
+ }
+ return ans, nil
+}
+
+// someday think about comments, strings, backslashes, etc
+// this would repeat some of the template parsing, but because the user is typing
+// there may be no parse tree here.
+// (go/scanner will report 2 tokens for $a, as $ is not a legal go identifier character)
+// (go/scanner is about 2.7 times more expensive)
+func (c *completer) analyze(buf []byte) [][]byte {
+ // we want to split on whitespace and before dots
+ var working []byte
+ var ans [][]byte
+ for _, ch := range buf {
+ if ch == '.' && len(working) > 0 {
+ ans = append(ans, working)
+ working = []byte{'.'}
+ continue
+ }
+ if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
+ if len(working) > 0 {
+ ans = append(ans, working)
+ working = []byte{}
+ continue
+ }
+ }
+ working = append(working, ch)
+ }
+ if len(working) > 0 {
+ ans = append(ans, working)
+ }
+ ch := buf[len(buf)-1]
+ if ch == ' ' || ch == '\t' {
+ // avoid completing on whitespace
+ ans = append(ans, []byte{ch})
+ }
+ return ans
+}
+
+// version of c.analyze that uses go/scanner.
+func scan(buf []byte) []string {
+ fset := token.NewFileSet()
+ fp := fset.AddFile("", -1, len(buf))
+ var sc scanner.Scanner
+ sc.Init(fp, buf, func(pos token.Position, msg string) {}, scanner.ScanComments)
+ ans := make([]string, 0, 10) // preallocating gives a measurable savings
+ for {
+ _, tok, lit := sc.Scan() // tok is an int
+ if tok == token.EOF {
+ break // done
+ } else if tok == token.SEMICOLON && lit == "\n" {
+ continue // don't care, but probably can't happen
+ } else if tok == token.PERIOD {
+ ans = append(ans, ".") // lit is empty
+ } else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "." {
+ ans[len(ans)-1] = "." + lit
+ } else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "$" {
+ ans[len(ans)-1] = "$" + lit
+ } else if lit != "" {
+ ans = append(ans, lit)
+ }
+ }
+ return ans
+}
+
+// pattern is what the user has typed
+func weakMatch(choice, pattern string) float64 {
+ lower := strings.ToLower(choice)
+ // for now, use only lower-case everywhere
+ pattern = strings.ToLower(pattern)
+ // The first char has to match
+ if pattern[0] != lower[0] {
+ return 0
+ }
+ // If they start with ., then the second char has to match
+ from := 1
+ if pattern[0] == '.' {
+ if len(pattern) < 2 {
+ return 1 // pattern just a ., so it matches
+ }
+ if pattern[1] != lower[1] {
+ return 0
+ }
+ from = 2
+ }
+ // check that all the characters of pattern occur as a subsequence of choice
+ i, j := from, from
+ for ; i < len(lower) && j < len(pattern); j++ {
+ if pattern[j] == lower[i] {
+ i++
+ if i >= len(lower) {
+ return 0
+ }
+ }
+ }
+ if j < len(pattern) {
+ return 0
+ }
+ return 1
+}