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 }