Skip to content

Commit a985a6c

Browse files
committed
gopls/internal/template: use protocol.Mapper and simplify
This CL replaces all the complex bookkeeping logic with the existing protocol.Mapper. This fixes the associated bug. Details: - eliminate parsed.{nls,lastnl,check,nonASCII}. - use UTF-16 or byte offsets, never runes. - propagate all Mapper errors upwards and handle them properly. - eliminate unnecessary "multiline" token distinction and alternative logic. Mapper works fine. - remove tests that reduced to tests of Mapper. - remove append to file.Handle.Content buffer (a data race). The only behavior changes to tests are: - the extent of a string token "foo" now includes its quote marks - the length of an identifier or literal is in bytes, not runes Also: - use clearer variable names. - mark existing comments as TODO where appropriate. - move symAtPosition - rename findWordAt -> wordAt Fixes golang/go#74635 Change-Id: Ia25b7dcbe28e3bc472ae103bd85f71e3c09e3a30 Reviewed-on: https://go-review.googlesource.com/c/tools/+/688937 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]>
1 parent b992291 commit a985a6c

File tree

9 files changed

+357
-560
lines changed

9 files changed

+357
-560
lines changed

gopls/internal/protocol/semtok/semtok.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import "sort"
99

1010
// A Token provides the extent and semantics of a token.
1111
type Token struct {
12-
Line, Start uint32
13-
Len uint32
12+
Line, Start uint32 // 0-based UTF-16 index
13+
Len uint32 // in UTF-16 codes
1414
Type Type
1515
Modifiers []Modifier
1616
}

gopls/internal/template/completion.go

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,17 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
3131
var start int // the beginning of the Token (completed or not)
3232
syms := make(map[string]symbol)
3333
var p *parsed
34-
for fn, fc := range all.files {
34+
for uri, fc := range all.files {
3535
// collect symbols from all template files
3636
filterSyms(syms, fc.symbols)
37-
if fn.Path() != fh.URI().Path() {
37+
if uri.Path() != fh.URI().Path() {
3838
continue
3939
}
40-
if start = inTemplate(fc, pos); start == -1 {
41-
return nil, nil
40+
offset, err := enclosingTokenStart(fc, pos)
41+
if err != nil {
42+
return nil, err
4243
}
44+
start = offset
4345
p = fc
4446
}
4547
if p == nil {
@@ -74,20 +76,26 @@ func filterSyms(syms map[string]symbol, ns []symbol) {
7476
}
7577
}
7678

77-
// return the starting position of the enclosing token, or -1 if none
78-
func inTemplate(fc *parsed, pos protocol.Position) int {
79+
// enclosingTokenStart returns the start offset of the enclosing token.
80+
// A (-1, non-nil) result indicates "no enclosing token".
81+
func enclosingTokenStart(fc *parsed, pos protocol.Position) (int, error) {
7982
// pos is the pos-th character. if the cursor is at the beginning
8083
// of the file, pos is 0. That is, we've only seen characters before pos
8184
// 1. pos might be in a Token, return tk.Start
8285
// 2. pos might be after an elided but before a Token, return elided
8386
// 3. return -1 for false
84-
offset := fc.fromPosition(pos)
85-
// this could be a binary search, as the tokens are ordered
87+
offset, err := fc.mapper.PositionOffset(pos)
88+
if err != nil {
89+
return 0, err
90+
}
91+
92+
// TODO: opt: this could be a binary search, as the tokens are ordered
8693
for _, tk := range fc.tokens {
8794
if tk.start+len(lbraces) <= offset && offset+len(rbraces) <= tk.end {
88-
return tk.start
95+
return tk.start, nil
8996
}
9097
}
98+
9199
for _, x := range fc.elided {
92100
if x+len(lbraces) > offset {
93101
// fc.elided is sorted, and x is the position where a '{{' was replaced
@@ -98,10 +106,10 @@ func inTemplate(fc *parsed, pos protocol.Position) int {
98106
// If the interval [x,offset] does not contain Left or Right
99107
// then provide completions. (do we need the test for Right?)
100108
if !bytes.Contains(fc.buf[x:offset], lbraces) && !bytes.Contains(fc.buf[x:offset], rbraces) {
101-
return x
109+
return x, nil
102110
}
103111
}
104-
return -1
112+
return -1, fmt.Errorf("no token enclosing %d", pos)
105113
}
106114

107115
var (
@@ -115,7 +123,10 @@ var (
115123
// The error return is always nil.
116124
func (c *completer) complete() (*protocol.CompletionList, error) {
117125
ans := &protocol.CompletionList{IsIncomplete: true, Items: []protocol.CompletionItem{}}
118-
start := c.p.fromPosition(c.pos)
126+
start, err := c.p.mapper.PositionOffset(c.pos)
127+
if err != nil {
128+
return ans, err
129+
}
119130
sofar := c.p.buf[c.offset:start]
120131
if len(sofar) == 0 || sofar[len(sofar)-1] == ' ' || sofar[len(sofar)-1] == '\t' {
121132
return ans, nil

gopls/internal/template/completion_test.go

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ func init() {
1919

2020
type tparse struct {
2121
marked string // ^ shows where to ask for completions. (The user just typed the following character.)
22-
wanted []string // expected completions
22+
wanted []string // expected completions; nil => no enclosing token
2323
}
2424

2525
// Test completions in templates that parse enough (if completion needs symbols)
2626
// Seen characters up to the ^
2727
func TestParsed(t *testing.T) {
28-
var tests = []tparse{
28+
for _, test := range []tparse{
2929
{"{{x}}{{12. xx^", nil}, // https://github.com/golang/go/issues/50430
3030
{`<table class="chroma" data-new-comment-url="{{if $.PageIsPullFiles}}{{$.Issue.HTMLURL}}/files/reviews/new_comment{{else}}{{$.CommitHTML}}/new_comment^{{end}}">`, nil},
3131
{"{{i^f}}", []string{"index", "if"}},
@@ -50,53 +50,56 @@ func TestParsed(t *testing.T) {
5050
{"{{`e^", []string{}},
5151
{"{{`No i^", []string{}}, // example of why go/scanner is used
5252
{"{{xavier}}{{12. x^", []string{"xavier"}},
53-
}
54-
for _, tx := range tests {
55-
c := testCompleter(t, tx)
56-
var v []string
57-
if c != nil {
58-
ans, _ := c.complete()
59-
for _, a := range ans.Items {
60-
v = append(v, a.Label)
53+
} {
54+
t.Run("", func(t *testing.T) {
55+
var got []string
56+
if c := testCompleter(t, test); c != nil {
57+
ans, _ := c.complete()
58+
for _, a := range ans.Items {
59+
got = append(got, a.Label)
60+
}
6161
}
62-
}
63-
if len(v) != len(tx.wanted) {
64-
t.Errorf("%q: got %q, wanted %q %d,%d", tx.marked, v, tx.wanted, len(v), len(tx.wanted))
65-
continue
66-
}
67-
sort.Strings(tx.wanted)
68-
sort.Strings(v)
69-
for i := 0; i < len(v); i++ {
70-
if tx.wanted[i] != v[i] {
71-
t.Errorf("%q at %d: got %v, wanted %v", tx.marked, i, v, tx.wanted)
72-
break
62+
if len(got) != len(test.wanted) {
63+
t.Fatalf("%q: got %q, wanted %q %d,%d", test.marked, got, test.wanted, len(got), len(test.wanted))
7364
}
74-
}
65+
sort.Strings(test.wanted)
66+
sort.Strings(got)
67+
for i := 0; i < len(got); i++ {
68+
if test.wanted[i] != got[i] {
69+
t.Fatalf("%q at %d: got %v, wanted %v", test.marked, i, got, test.wanted)
70+
}
71+
}
72+
})
7573
}
7674
}
7775

7876
func testCompleter(t *testing.T, tx tparse) *completer {
79-
t.Helper()
8077
// seen chars up to ^
81-
col := strings.Index(tx.marked, "^")
78+
offset := strings.Index(tx.marked, "^")
8279
buf := strings.Replace(tx.marked, "^", "", 1)
83-
p := parseBuffer([]byte(buf))
84-
pos := protocol.Position{Line: 0, Character: uint32(col)}
80+
p := parseBuffer("", []byte(buf))
8581
if p.parseErr != nil {
86-
log.Printf("%q: %v", tx.marked, p.parseErr)
82+
t.Logf("%q: %v", tx.marked, p.parseErr)
83+
}
84+
pos, err := p.mapper.OffsetPosition(offset)
85+
if err != nil {
86+
t.Fatal(err)
8787
}
88-
offset := inTemplate(p, pos)
89-
if offset == -1 {
90-
return nil
88+
89+
start, err := enclosingTokenStart(p, pos)
90+
if err != nil {
91+
if start == -1 {
92+
return nil // no enclosing token
93+
}
94+
t.Fatal(err)
9195
}
9296
syms := make(map[string]symbol)
9397
filterSyms(syms, p.symbols)
94-
c := &completer{
98+
return &completer{
9599
p: p,
96-
pos: protocol.Position{Line: 0, Character: uint32(col)},
97-
offset: offset + len(lbraces),
100+
pos: pos,
101+
offset: start + len(lbraces),
98102
ctx: protocol.CompletionContext{TriggerKind: protocol.Invoked},
99103
syms: syms,
100104
}
101-
return c
102105
}

gopls/internal/template/highlight.go

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,34 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lo
1919
if err != nil {
2020
return nil, err
2121
}
22-
p := parseBuffer(buf)
23-
pos := p.fromPosition(loc)
24-
var ans []protocol.DocumentHighlight
22+
p := parseBuffer(fh.URI(), buf)
23+
pos, err := p.mapper.PositionOffset(loc)
24+
if err != nil {
25+
return nil, err
26+
}
27+
2528
if p.parseErr == nil {
2629
for _, s := range p.symbols {
27-
if s.start <= pos && pos < s.start+s.length {
30+
if s.start <= pos && pos < s.start+s.len {
2831
return markSymbols(p, s)
2932
}
3033
}
3134
}
35+
3236
// these tokens exist whether or not there was a parse error
3337
// (symbols require a successful parse)
3438
for _, tok := range p.tokens {
3539
if tok.start <= pos && pos < tok.end {
36-
wordAt := findWordAt(p, pos)
40+
wordAt := wordAt(p.buf, pos)
3741
if len(wordAt) > 0 {
3842
return markWordInToken(p, wordAt)
3943
}
4044
}
4145
}
42-
// find the 'word' at pos, etc: someday
46+
47+
// TODO: find the 'word' at pos, etc: someday
4348
// until then we get the default action, which doesn't respect word boundaries
44-
return ans, nil
49+
return nil, nil
4550
}
4651

4752
func markSymbols(p *parsed, sym symbol) ([]protocol.DocumentHighlight, error) {
@@ -52,8 +57,12 @@ func markSymbols(p *parsed, sym symbol) ([]protocol.DocumentHighlight, error) {
5257
if s.vardef {
5358
kind = protocol.Write
5459
}
60+
rng, err := p.mapper.OffsetRange(s.offsets())
61+
if err != nil {
62+
return nil, err
63+
}
5564
ans = append(ans, protocol.DocumentHighlight{
56-
Range: p._range(s.start, s.length),
65+
Range: rng,
5766
Kind: kind,
5867
})
5968
}
@@ -69,29 +78,35 @@ func markWordInToken(p *parsed, wordAt string) ([]protocol.DocumentHighlight, er
6978
return nil, fmt.Errorf("%q: unmatchable word (%v)", wordAt, err)
7079
}
7180
for _, tok := range p.tokens {
72-
got := pat.FindAllIndex(p.buf[tok.start:tok.end], -1)
73-
for i := range got {
81+
matches := pat.FindAllIndex(p.buf[tok.start:tok.end], -1)
82+
for _, match := range matches {
83+
rng, err := p.mapper.OffsetRange(match[0], match[1])
84+
if err != nil {
85+
return nil, err
86+
}
7487
ans = append(ans, protocol.DocumentHighlight{
75-
Range: p._range(got[i][0], got[i][1]-got[i][0]),
88+
Range: rng,
7689
Kind: protocol.Text,
7790
})
7891
}
7992
}
8093
return ans, nil
8194
}
8295

83-
var wordRe = regexp.MustCompile(`[$]?\w+$`)
84-
var moreRe = regexp.MustCompile(`^[$]?\w+`)
85-
86-
// findWordAt finds the word the cursor is in (meaning in or just before)
87-
func findWordAt(p *parsed, pos int) string {
88-
if pos >= len(p.buf) {
89-
return "" // can't happen, as we are called with pos < tok.End
96+
// wordAt returns the word the cursor is in (meaning in or just before)
97+
func wordAt(buf []byte, pos int) string {
98+
if pos >= len(buf) {
99+
return ""
90100
}
91-
after := moreRe.Find(p.buf[pos:])
101+
after := moreRe.Find(buf[pos:])
92102
if len(after) == 0 {
93103
return "" // end of the word
94104
}
95-
got := wordRe.Find(p.buf[:pos+len(after)])
105+
got := wordRe.Find(buf[:pos+len(after)])
96106
return string(got)
97107
}
108+
109+
var (
110+
wordRe = regexp.MustCompile(`[$]?\w+$`)
111+
moreRe = regexp.MustCompile(`^[$]?\w+`)
112+
)

0 commit comments

Comments
 (0)