Skip to content

Commit 258ee27

Browse files
committed
internal/lsp/template: implement completions for template files
The suggesteds completions are based on a superficial parse of all the template files in the package. The code errs on the side of too many suggestions. Change-Id: If956ad548327be25517878aab70802cf62d42a50 Reviewed-on: https://go-review.googlesource.com/c/tools/+/341649 Trust: Peter Weinberger <[email protected]> Run-TryBot: Peter Weinberger <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent 384e5da commit 258ee27

File tree

4 files changed

+389
-5
lines changed

4 files changed

+389
-5
lines changed

internal/lsp/completion.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
3333
case source.Mod:
3434
candidates, surrounding = nil, nil
3535
case source.Tmpl:
36-
candidates, surrounding, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context)
36+
var cl *protocol.CompletionList
37+
cl, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context)
38+
if err != nil {
39+
break // use common error handling, candidates==nil
40+
}
41+
return cl, nil
3742
}
3843
if err != nil {
3944
event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position))

internal/lsp/template/completion.go

Lines changed: 281 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,294 @@
55
package template
66

77
import (
8+
"bytes"
89
"context"
910
"fmt"
11+
"go/scanner"
12+
"go/token"
13+
"strings"
1014

1115
"golang.org/x/tools/internal/lsp/protocol"
1216
"golang.org/x/tools/internal/lsp/source"
13-
"golang.org/x/tools/internal/lsp/source/completion"
1417
)
1518

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) {
1729
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"
19296
}
20-
return nil, nil, fmt.Errorf("implement template completion")
297+
return fmt.Sprintf("?%v", c)
21298
}

0 commit comments

Comments
 (0)