|
5 | 5 | package template
|
6 | 6 |
|
7 | 7 | import (
|
| 8 | + "bytes" |
8 | 9 | "context"
|
9 | 10 | "fmt"
|
| 11 | + "go/scanner" |
| 12 | + "go/token" |
| 13 | + "strings" |
10 | 14 |
|
11 | 15 | "golang.org/x/tools/internal/lsp/protocol"
|
12 | 16 | "golang.org/x/tools/internal/lsp/source"
|
13 |
| - "golang.org/x/tools/internal/lsp/source/completion" |
14 | 17 | )
|
15 | 18 |
|
16 |
| -func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, pos protocol.Position, context protocol.CompletionContext) ([]completion.CompletionItem, *completion.Selection, error) { |
| 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) { |
17 | 29 | if skipTemplates(snapshot) {
|
18 |
| - return nil, nil, nil |
| 30 | + return nil, nil |
| 31 | + } |
| 32 | + all := New(snapshot.Templates()) |
| 33 | + var start int // the beginning of the Token (completed or not) |
| 34 | + syms := make(map[string]symbol) |
| 35 | + var p *Parsed |
| 36 | + for fn, fc := range all.files { |
| 37 | + // collect symbols from all template files |
| 38 | + filterSyms(syms, fc.symbols) |
| 39 | + if fn.Filename() != fh.URI().Filename() { |
| 40 | + continue |
| 41 | + } |
| 42 | + if start = inTemplate(fc, pos); start == -1 { |
| 43 | + return nil, nil |
| 44 | + } |
| 45 | + p = fc |
| 46 | + } |
| 47 | + if p == nil { |
| 48 | + // this cannot happen unless the search missed a template file |
| 49 | + return nil, fmt.Errorf("%s not found", fh.FileIdentity().URI.Filename()) |
| 50 | + } |
| 51 | + c := completer{ |
| 52 | + p: p, |
| 53 | + pos: pos, |
| 54 | + offset: start + len(Left), |
| 55 | + ctx: context, |
| 56 | + syms: syms, |
| 57 | + } |
| 58 | + return c.complete() |
| 59 | +} |
| 60 | + |
| 61 | +func filterSyms(syms map[string]symbol, ns []symbol) { |
| 62 | + for _, xsym := range ns { |
| 63 | + switch xsym.kind { |
| 64 | + case protocol.Method, protocol.Package, protocol.Boolean, protocol.Namespace, |
| 65 | + protocol.Function: |
| 66 | + syms[xsym.name] = xsym // we don't care which symbol we get |
| 67 | + case protocol.Variable: |
| 68 | + if xsym.name != "dot" { |
| 69 | + syms[xsym.name] = xsym |
| 70 | + } |
| 71 | + case protocol.Constant: |
| 72 | + if xsym.name == "nil" { |
| 73 | + syms[xsym.name] = xsym |
| 74 | + } |
| 75 | + } |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +// return the starting position of the enclosing token, or -1 if none |
| 80 | +func inTemplate(fc *Parsed, pos protocol.Position) int { |
| 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], []byte(Left)) && !bytes.Contains(fc.buf[x:offset], []byte(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 := string(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, string(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, string(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 | +// someday think about comments, strings, backslashes, etc |
| 194 | +// this would repeat some of the template parsing, but because the user is typing |
| 195 | +// there may be no parse tree here. |
| 196 | +// (go/scanner will report 2 tokens for $a, as $ is not a legal go identifier character) |
| 197 | +// (go/scanner is about 2.7 times more expensive) |
| 198 | +func (c *completer) analyze(buf []byte) [][]byte { |
| 199 | + // we want to split on whitespace and before dots |
| 200 | + var working []byte |
| 201 | + var ans [][]byte |
| 202 | + for _, ch := range buf { |
| 203 | + if ch == '.' && len(working) > 0 { |
| 204 | + ans = append(ans, working) |
| 205 | + working = []byte{'.'} |
| 206 | + continue |
| 207 | + } |
| 208 | + if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' { |
| 209 | + if len(working) > 0 { |
| 210 | + ans = append(ans, working) |
| 211 | + working = []byte{} |
| 212 | + continue |
| 213 | + } |
| 214 | + } |
| 215 | + working = append(working, ch) |
| 216 | + } |
| 217 | + if len(working) > 0 { |
| 218 | + ans = append(ans, working) |
| 219 | + } |
| 220 | + ch := buf[len(buf)-1] |
| 221 | + if ch == ' ' || ch == '\t' { |
| 222 | + // avoid completing on whitespace |
| 223 | + ans = append(ans, []byte{ch}) |
| 224 | + } |
| 225 | + return ans |
| 226 | +} |
| 227 | + |
| 228 | +// version of c.analyze that uses go/scanner. |
| 229 | +func scan(buf []byte) []string { |
| 230 | + fset := token.NewFileSet() |
| 231 | + fp := fset.AddFile("", -1, len(buf)) |
| 232 | + var sc scanner.Scanner |
| 233 | + sc.Init(fp, buf, func(pos token.Position, msg string) {}, scanner.ScanComments) |
| 234 | + ans := make([]string, 0, 10) // preallocating gives a measurable savings |
| 235 | + for { |
| 236 | + _, tok, lit := sc.Scan() // tok is an int |
| 237 | + if tok == token.EOF { |
| 238 | + break // done |
| 239 | + } else if tok == token.SEMICOLON && lit == "\n" { |
| 240 | + continue // don't care, but probably can't happen |
| 241 | + } else if tok == token.PERIOD { |
| 242 | + ans = append(ans, ".") // lit is empty |
| 243 | + } else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "." { |
| 244 | + ans[len(ans)-1] = "." + lit |
| 245 | + } else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "$" { |
| 246 | + ans[len(ans)-1] = "$" + lit |
| 247 | + } else { |
| 248 | + ans = append(ans, lit) |
| 249 | + } |
| 250 | + } |
| 251 | + return ans |
| 252 | +} |
| 253 | + |
| 254 | +// pattern is what the user has typed |
| 255 | +func weakMatch(choice, pattern string) float64 { |
| 256 | + lower := strings.ToLower(choice) |
| 257 | + // for now, use only lower-case everywhere |
| 258 | + pattern = strings.ToLower(pattern) |
| 259 | + // The first char has to match |
| 260 | + if pattern[0] != lower[0] { |
| 261 | + return 0 |
| 262 | + } |
| 263 | + // If they start with ., then the second char has to match |
| 264 | + from := 1 |
| 265 | + if pattern[0] == '.' { |
| 266 | + if len(pattern) < 2 { |
| 267 | + return 1 // pattern just a ., so it matches |
| 268 | + } |
| 269 | + if pattern[1] != lower[1] { |
| 270 | + return 0 |
| 271 | + } |
| 272 | + from = 2 |
| 273 | + } |
| 274 | + // check that all the characters of pattern occur as a subsequence of choice |
| 275 | + for i, j := from, from; j < len(pattern); j++ { |
| 276 | + if pattern[j] == lower[i] { |
| 277 | + i++ |
| 278 | + if i >= len(lower) { |
| 279 | + return 0 |
| 280 | + } |
| 281 | + } |
| 282 | + } |
| 283 | + return 1 |
| 284 | +} |
| 285 | + |
| 286 | +// for debug printing |
| 287 | +func strContext(c protocol.CompletionContext) string { |
| 288 | + switch c.TriggerKind { |
| 289 | + case protocol.Invoked: |
| 290 | + return "invoked" |
| 291 | + case protocol.TriggerCharacter: |
| 292 | + return fmt.Sprintf("triggered(%s)", c.TriggerCharacter) |
| 293 | + case protocol.TriggerForIncompleteCompletions: |
| 294 | + // gopls doesn't seem to handle these explicitly anywhere |
| 295 | + return "incomplete" |
19 | 296 | }
|
20 |
| - return nil, nil, fmt.Errorf("implement template completion") |
| 297 | + return fmt.Sprintf("?%v", c) |
21 | 298 | }
|
0 commit comments