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 }