golang.org/x/tools/gopls@v0.15.3/internal/template/completion.go (about)

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package template
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"go/scanner"
    12  	"go/token"
    13  	"strings"
    14  
    15  	"golang.org/x/tools/gopls/internal/cache"
    16  	"golang.org/x/tools/gopls/internal/file"
    17  	"golang.org/x/tools/gopls/internal/protocol"
    18  )
    19  
    20  // information needed for completion
    21  type completer struct {
    22  	p      *Parsed
    23  	pos    protocol.Position
    24  	offset int // offset of the start of the Token
    25  	ctx    protocol.CompletionContext
    26  	syms   map[string]symbol
    27  }
    28  
    29  func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) {
    30  	all := New(snapshot.Templates())
    31  	var start int // the beginning of the Token (completed or not)
    32  	syms := make(map[string]symbol)
    33  	var p *Parsed
    34  	for fn, fc := range all.files {
    35  		// collect symbols from all template files
    36  		filterSyms(syms, fc.symbols)
    37  		if fn.Path() != fh.URI().Path() {
    38  			continue
    39  		}
    40  		if start = inTemplate(fc, pos); start == -1 {
    41  			return nil, nil
    42  		}
    43  		p = fc
    44  	}
    45  	if p == nil {
    46  		// this cannot happen unless the search missed a template file
    47  		return nil, fmt.Errorf("%s not found", fh.Identity().URI.Path())
    48  	}
    49  	c := completer{
    50  		p:      p,
    51  		pos:    pos,
    52  		offset: start + len(Left),
    53  		ctx:    context,
    54  		syms:   syms,
    55  	}
    56  	return c.complete()
    57  }
    58  
    59  func filterSyms(syms map[string]symbol, ns []symbol) {
    60  	for _, xsym := range ns {
    61  		switch xsym.kind {
    62  		case protocol.Method, protocol.Package, protocol.Boolean, protocol.Namespace,
    63  			protocol.Function:
    64  			syms[xsym.name] = xsym // we don't care which symbol we get
    65  		case protocol.Variable:
    66  			if xsym.name != "dot" {
    67  				syms[xsym.name] = xsym
    68  			}
    69  		case protocol.Constant:
    70  			if xsym.name == "nil" {
    71  				syms[xsym.name] = xsym
    72  			}
    73  		}
    74  	}
    75  }
    76  
    77  // return the starting position of the enclosing token, or -1 if none
    78  func inTemplate(fc *Parsed, pos protocol.Position) int {
    79  	// pos is the pos-th character. if the cursor is at the beginning
    80  	// of the file, pos is 0. That is, we've only seen characters before pos
    81  	// 1. pos might be in a Token, return tk.Start
    82  	// 2. pos might be after an elided but before a Token, return elided
    83  	// 3. return -1 for false
    84  	offset := fc.FromPosition(pos)
    85  	// this could be a binary search, as the tokens are ordered
    86  	for _, tk := range fc.tokens {
    87  		if tk.Start < offset && offset <= tk.End {
    88  			return tk.Start
    89  		}
    90  	}
    91  	for _, x := range fc.elided {
    92  		if x > offset {
    93  			// fc.elided is sorted
    94  			break
    95  		}
    96  		// If the interval [x,offset] does not contain Left or Right
    97  		// then provide completions. (do we need the test for Right?)
    98  		if !bytes.Contains(fc.buf[x:offset], Left) && !bytes.Contains(fc.buf[x:offset], Right) {
    99  			return x
   100  		}
   101  	}
   102  	return -1
   103  }
   104  
   105  var (
   106  	keywords = []string{"if", "with", "else", "block", "range", "template", "end}}", "end"}
   107  	globals  = []string{"and", "call", "html", "index", "slice", "js", "len", "not", "or",
   108  		"urlquery", "printf", "println", "print", "eq", "ne", "le", "lt", "ge", "gt"}
   109  )
   110  
   111  // find the completions. start is the offset of either the Token enclosing pos, or where
   112  // the incomplete token starts.
   113  // The error return is always nil.
   114  func (c *completer) complete() (*protocol.CompletionList, error) {
   115  	ans := &protocol.CompletionList{IsIncomplete: true, Items: []protocol.CompletionItem{}}
   116  	start := c.p.FromPosition(c.pos)
   117  	sofar := c.p.buf[c.offset:start]
   118  	if len(sofar) == 0 || sofar[len(sofar)-1] == ' ' || sofar[len(sofar)-1] == '\t' {
   119  		return ans, nil
   120  	}
   121  	// sofar could be parsed by either c.analyzer() or scan(). The latter is precise
   122  	// and slower, but fast enough
   123  	words := scan(sofar)
   124  	// 1. if pattern starts $, show variables
   125  	// 2. if pattern starts ., show methods (and . by itself?)
   126  	// 3. if len(words) == 1, show firstWords (but if it were a |, show functions and globals)
   127  	// 4. ...? (parenthetical expressions, arguments, ...) (packages, namespaces, nil?)
   128  	if len(words) == 0 {
   129  		return nil, nil // if this happens, why were we called?
   130  	}
   131  	pattern := words[len(words)-1]
   132  	if pattern[0] == '$' {
   133  		// should we also return a raw "$"?
   134  		for _, s := range c.syms {
   135  			if s.kind == protocol.Variable && weakMatch(s.name, pattern) > 0 {
   136  				ans.Items = append(ans.Items, protocol.CompletionItem{
   137  					Label:  s.name,
   138  					Kind:   protocol.VariableCompletion,
   139  					Detail: "Variable",
   140  				})
   141  			}
   142  		}
   143  		return ans, nil
   144  	}
   145  	if pattern[0] == '.' {
   146  		for _, s := range c.syms {
   147  			if s.kind == protocol.Method && weakMatch("."+s.name, pattern) > 0 {
   148  				ans.Items = append(ans.Items, protocol.CompletionItem{
   149  					Label:  s.name,
   150  					Kind:   protocol.MethodCompletion,
   151  					Detail: "Method/member",
   152  				})
   153  			}
   154  		}
   155  		return ans, nil
   156  	}
   157  	// could we get completion attempts in strings or numbers, and if so, do we care?
   158  	// globals
   159  	for _, kw := range globals {
   160  		if weakMatch(kw, pattern) != 0 {
   161  			ans.Items = append(ans.Items, protocol.CompletionItem{
   162  				Label:  kw,
   163  				Kind:   protocol.KeywordCompletion,
   164  				Detail: "Function",
   165  			})
   166  		}
   167  	}
   168  	// and functions
   169  	for _, s := range c.syms {
   170  		if s.kind == protocol.Function && weakMatch(s.name, pattern) != 0 {
   171  			ans.Items = append(ans.Items, protocol.CompletionItem{
   172  				Label:  s.name,
   173  				Kind:   protocol.FunctionCompletion,
   174  				Detail: "Function",
   175  			})
   176  		}
   177  	}
   178  	// keywords if we're at the beginning
   179  	if len(words) <= 1 || len(words[len(words)-2]) == 1 && words[len(words)-2][0] == '|' {
   180  		for _, kw := range keywords {
   181  			if weakMatch(kw, pattern) != 0 {
   182  				ans.Items = append(ans.Items, protocol.CompletionItem{
   183  					Label:  kw,
   184  					Kind:   protocol.KeywordCompletion,
   185  					Detail: "keyword",
   186  				})
   187  			}
   188  		}
   189  	}
   190  	return ans, nil
   191  }
   192  
   193  // version of c.analyze that uses go/scanner.
   194  func scan(buf []byte) []string {
   195  	fset := token.NewFileSet()
   196  	fp := fset.AddFile("", -1, len(buf))
   197  	var sc scanner.Scanner
   198  	sc.Init(fp, buf, func(pos token.Position, msg string) {}, scanner.ScanComments)
   199  	ans := make([]string, 0, 10) // preallocating gives a measurable savings
   200  	for {
   201  		_, tok, lit := sc.Scan() // tok is an int
   202  		if tok == token.EOF {
   203  			break // done
   204  		} else if tok == token.SEMICOLON && lit == "\n" {
   205  			continue // don't care, but probably can't happen
   206  		} else if tok == token.PERIOD {
   207  			ans = append(ans, ".") // lit is empty
   208  		} else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "." {
   209  			ans[len(ans)-1] = "." + lit
   210  		} else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "$" {
   211  			ans[len(ans)-1] = "$" + lit
   212  		} else if lit != "" {
   213  			ans = append(ans, lit)
   214  		}
   215  	}
   216  	return ans
   217  }
   218  
   219  // pattern is what the user has typed
   220  func weakMatch(choice, pattern string) float64 {
   221  	lower := strings.ToLower(choice)
   222  	// for now, use only lower-case everywhere
   223  	pattern = strings.ToLower(pattern)
   224  	// The first char has to match
   225  	if pattern[0] != lower[0] {
   226  		return 0
   227  	}
   228  	// If they start with ., then the second char has to match
   229  	from := 1
   230  	if pattern[0] == '.' {
   231  		if len(pattern) < 2 {
   232  			return 1 // pattern just a ., so it matches
   233  		}
   234  		if pattern[1] != lower[1] {
   235  			return 0
   236  		}
   237  		from = 2
   238  	}
   239  	// check that all the characters of pattern occur as a subsequence of choice
   240  	i, j := from, from
   241  	for ; i < len(lower) && j < len(pattern); j++ {
   242  		if pattern[j] == lower[i] {
   243  			i++
   244  			if i >= len(lower) {
   245  				return 0
   246  			}
   247  		}
   248  	}
   249  	if j < len(pattern) {
   250  		return 0
   251  	}
   252  	return 1
   253  }