github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/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  	"github.com/powerman/golang-tools/internal/lsp/protocol"
    16  	"github.com/powerman/golang-tools/internal/lsp/source"
    17  )
    18  
    19  // information needed for completion
    20  type completer struct {
    21  	p      *Parsed
    22  	pos    protocol.Position
    23  	offset int // offset of the start of the Token
    24  	ctx    protocol.CompletionContext
    25  	syms   map[string]symbol
    26  }
    27  
    28  func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) {
    29  	all := New(snapshot.Templates())
    30  	var start int // the beginning of the Token (completed or not)
    31  	syms := make(map[string]symbol)
    32  	var p *Parsed
    33  	for fn, fc := range all.files {
    34  		// collect symbols from all template files
    35  		filterSyms(syms, fc.symbols)
    36  		if fn.Filename() != fh.URI().Filename() {
    37  			continue
    38  		}
    39  		if start = inTemplate(fc, pos); start == -1 {
    40  			return nil, nil
    41  		}
    42  		p = fc
    43  	}
    44  	if p == nil {
    45  		// this cannot happen unless the search missed a template file
    46  		return nil, fmt.Errorf("%s not found", fh.FileIdentity().URI.Filename())
    47  	}
    48  	c := completer{
    49  		p:      p,
    50  		pos:    pos,
    51  		offset: start + len(Left),
    52  		ctx:    context,
    53  		syms:   syms,
    54  	}
    55  	return c.complete()
    56  }
    57  
    58  func filterSyms(syms map[string]symbol, ns []symbol) {
    59  	for _, xsym := range ns {
    60  		switch xsym.kind {
    61  		case protocol.Method, protocol.Package, protocol.Boolean, protocol.Namespace,
    62  			protocol.Function:
    63  			syms[xsym.name] = xsym // we don't care which symbol we get
    64  		case protocol.Variable:
    65  			if xsym.name != "dot" {
    66  				syms[xsym.name] = xsym
    67  			}
    68  		case protocol.Constant:
    69  			if xsym.name == "nil" {
    70  				syms[xsym.name] = xsym
    71  			}
    72  		}
    73  	}
    74  }
    75  
    76  // return the starting position of the enclosing token, or -1 if none
    77  func inTemplate(fc *Parsed, pos protocol.Position) int {
    78  	// pos is the pos-th character. if the cursor is at the beginning
    79  	// of the file, pos is 0. That is, we've only seen characters before pos
    80  	// 1. pos might be in a Token, return tk.Start
    81  	// 2. pos might be after an elided but before a Token, return elided
    82  	// 3. return -1 for false
    83  	offset := fc.FromPosition(pos)
    84  	// this could be a binary search, as the tokens are ordered
    85  	for _, tk := range fc.tokens {
    86  		if tk.Start < offset && offset <= tk.End {
    87  			return tk.Start
    88  		}
    89  	}
    90  	for _, x := range fc.elided {
    91  		if x > offset {
    92  			// fc.elided is sorted
    93  			break
    94  		}
    95  		// If the interval [x,offset] does not contain Left or Right
    96  		// then provide completions. (do we need the test for Right?)
    97  		if !bytes.Contains(fc.buf[x:offset], []byte(Left)) && !bytes.Contains(fc.buf[x:offset], []byte(Right)) {
    98  			return x
    99  		}
   100  	}
   101  	return -1
   102  }
   103  
   104  var (
   105  	keywords = []string{"if", "with", "else", "block", "range", "template", "end}}", "end"}
   106  	globals  = []string{"and", "call", "html", "index", "slice", "js", "len", "not", "or",
   107  		"urlquery", "printf", "println", "print", "eq", "ne", "le", "lt", "ge", "gt"}
   108  )
   109  
   110  // find the completions. start is the offset of either the Token enclosing pos, or where
   111  // the incomplete token starts.
   112  // The error return is always nil.
   113  func (c *completer) complete() (*protocol.CompletionList, error) {
   114  	ans := &protocol.CompletionList{IsIncomplete: true, Items: []protocol.CompletionItem{}}
   115  	start := c.p.FromPosition(c.pos)
   116  	sofar := c.p.buf[c.offset:start]
   117  	if len(sofar) == 0 || sofar[len(sofar)-1] == ' ' || sofar[len(sofar)-1] == '\t' {
   118  		return ans, nil
   119  	}
   120  	// sofar could be parsed by either c.analyzer() or scan(). The latter is precise
   121  	// and slower, but fast enough
   122  	words := scan(sofar)
   123  	// 1. if pattern starts $, show variables
   124  	// 2. if pattern starts ., show methods (and . by itself?)
   125  	// 3. if len(words) == 1, show firstWords (but if it were a |, show functions and globals)
   126  	// 4. ...? (parenthetical expressions, arguments, ...) (packages, namespaces, nil?)
   127  	if len(words) == 0 {
   128  		return nil, nil // if this happens, why were we called?
   129  	}
   130  	pattern := string(words[len(words)-1])
   131  	if pattern[0] == '$' {
   132  		// should we also return a raw "$"?
   133  		for _, s := range c.syms {
   134  			if s.kind == protocol.Variable && weakMatch(s.name, pattern) > 0 {
   135  				ans.Items = append(ans.Items, protocol.CompletionItem{
   136  					Label:  s.name,
   137  					Kind:   protocol.VariableCompletion,
   138  					Detail: "Variable",
   139  				})
   140  			}
   141  		}
   142  		return ans, nil
   143  	}
   144  	if pattern[0] == '.' {
   145  		for _, s := range c.syms {
   146  			if s.kind == protocol.Method && weakMatch("."+s.name, pattern) > 0 {
   147  				ans.Items = append(ans.Items, protocol.CompletionItem{
   148  					Label:  s.name,
   149  					Kind:   protocol.MethodCompletion,
   150  					Detail: "Method/member",
   151  				})
   152  			}
   153  		}
   154  		return ans, nil
   155  	}
   156  	// could we get completion attempts in strings or numbers, and if so, do we care?
   157  	// globals
   158  	for _, kw := range globals {
   159  		if weakMatch(kw, string(pattern)) != 0 {
   160  			ans.Items = append(ans.Items, protocol.CompletionItem{
   161  				Label:  kw,
   162  				Kind:   protocol.KeywordCompletion,
   163  				Detail: "Function",
   164  			})
   165  		}
   166  	}
   167  	// and functions
   168  	for _, s := range c.syms {
   169  		if s.kind == protocol.Function && weakMatch(s.name, pattern) != 0 {
   170  			ans.Items = append(ans.Items, protocol.CompletionItem{
   171  				Label:  s.name,
   172  				Kind:   protocol.FunctionCompletion,
   173  				Detail: "Function",
   174  			})
   175  		}
   176  	}
   177  	// keywords if we're at the beginning
   178  	if len(words) <= 1 || len(words[len(words)-2]) == 1 && words[len(words)-2][0] == '|' {
   179  		for _, kw := range keywords {
   180  			if weakMatch(kw, string(pattern)) != 0 {
   181  				ans.Items = append(ans.Items, protocol.CompletionItem{
   182  					Label:  kw,
   183  					Kind:   protocol.KeywordCompletion,
   184  					Detail: "keyword",
   185  				})
   186  			}
   187  		}
   188  	}
   189  	return ans, nil
   190  }
   191  
   192  // someday think about comments, strings, backslashes, etc
   193  // this would repeat some of the template parsing, but because the user is typing
   194  // there may be no parse tree here.
   195  // (go/scanner will report 2 tokens for $a, as $ is not a legal go identifier character)
   196  // (go/scanner is about 2.7 times more expensive)
   197  func (c *completer) analyze(buf []byte) [][]byte {
   198  	// we want to split on whitespace and before dots
   199  	var working []byte
   200  	var ans [][]byte
   201  	for _, ch := range buf {
   202  		if ch == '.' && len(working) > 0 {
   203  			ans = append(ans, working)
   204  			working = []byte{'.'}
   205  			continue
   206  		}
   207  		if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
   208  			if len(working) > 0 {
   209  				ans = append(ans, working)
   210  				working = []byte{}
   211  				continue
   212  			}
   213  		}
   214  		working = append(working, ch)
   215  	}
   216  	if len(working) > 0 {
   217  		ans = append(ans, working)
   218  	}
   219  	ch := buf[len(buf)-1]
   220  	if ch == ' ' || ch == '\t' {
   221  		// avoid completing on whitespace
   222  		ans = append(ans, []byte{ch})
   223  	}
   224  	return ans
   225  }
   226  
   227  // version of c.analyze that uses go/scanner.
   228  func scan(buf []byte) []string {
   229  	fset := token.NewFileSet()
   230  	fp := fset.AddFile("", -1, len(buf))
   231  	var sc scanner.Scanner
   232  	sc.Init(fp, buf, func(pos token.Position, msg string) {}, scanner.ScanComments)
   233  	ans := make([]string, 0, 10) // preallocating gives a measurable savings
   234  	for {
   235  		_, tok, lit := sc.Scan() // tok is an int
   236  		if tok == token.EOF {
   237  			break // done
   238  		} else if tok == token.SEMICOLON && lit == "\n" {
   239  			continue // don't care, but probably can't happen
   240  		} else if tok == token.PERIOD {
   241  			ans = append(ans, ".") // lit is empty
   242  		} else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "." {
   243  			ans[len(ans)-1] = "." + lit
   244  		} else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "$" {
   245  			ans[len(ans)-1] = "$" + lit
   246  		} else if lit != "" {
   247  			ans = append(ans, lit)
   248  		}
   249  	}
   250  	return ans
   251  }
   252  
   253  // pattern is what the user has typed
   254  func weakMatch(choice, pattern string) float64 {
   255  	lower := strings.ToLower(choice)
   256  	// for now, use only lower-case everywhere
   257  	pattern = strings.ToLower(pattern)
   258  	// The first char has to match
   259  	if pattern[0] != lower[0] {
   260  		return 0
   261  	}
   262  	// If they start with ., then the second char has to match
   263  	from := 1
   264  	if pattern[0] == '.' {
   265  		if len(pattern) < 2 {
   266  			return 1 // pattern just a ., so it matches
   267  		}
   268  		if pattern[1] != lower[1] {
   269  			return 0
   270  		}
   271  		from = 2
   272  	}
   273  	// check that all the characters of pattern occur as a subsequence of choice
   274  	i, j := from, from
   275  	for ; i < len(lower) && j < len(pattern); j++ {
   276  		if pattern[j] == lower[i] {
   277  			i++
   278  			if i >= len(lower) {
   279  				return 0
   280  			}
   281  		}
   282  	}
   283  	if j < len(pattern) {
   284  		return 0
   285  	}
   286  	return 1
   287  }
   288  
   289  // for debug printing
   290  func strContext(c protocol.CompletionContext) string {
   291  	switch c.TriggerKind {
   292  	case protocol.Invoked:
   293  		return "invoked"
   294  	case protocol.TriggerCharacter:
   295  		return fmt.Sprintf("triggered(%s)", c.TriggerCharacter)
   296  	case protocol.TriggerForIncompleteCompletions:
   297  		// gopls doesn't seem to handle these explicitly anywhere
   298  		return "incomplete"
   299  	}
   300  	return fmt.Sprintf("?%v", c)
   301  }