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 <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/protocol/semtok/semtok.go b/gopls/internal/protocol/semtok/semtok.go
index 86332d3..9922839 100644
--- a/gopls/internal/protocol/semtok/semtok.go
+++ b/gopls/internal/protocol/semtok/semtok.go
@@ -9,8 +9,8 @@
// A Token provides the extent and semantics of a token.
type Token struct {
- Line, Start uint32
- Len uint32
+ Line, Start uint32 // 0-based UTF-16 index
+ Len uint32 // in UTF-16 codes
Type Type
Modifiers []Modifier
}
diff --git a/gopls/internal/template/completion.go b/gopls/internal/template/completion.go
index acd3be5..fb09ba4 100644
--- a/gopls/internal/template/completion.go
+++ b/gopls/internal/template/completion.go
@@ -31,15 +31,17 @@
var start int // the beginning of the Token (completed or not)
syms := make(map[string]symbol)
var p *parsed
- for fn, fc := range all.files {
+ for uri, fc := range all.files {
// collect symbols from all template files
filterSyms(syms, fc.symbols)
- if fn.Path() != fh.URI().Path() {
+ if uri.Path() != fh.URI().Path() {
continue
}
- if start = inTemplate(fc, pos); start == -1 {
- return nil, nil
+ offset, err := enclosingTokenStart(fc, pos)
+ if err != nil {
+ return nil, err
}
+ start = offset
p = fc
}
if p == nil {
@@ -74,20 +76,26 @@
}
}
-// return the starting position of the enclosing token, or -1 if none
-func inTemplate(fc *parsed, pos protocol.Position) int {
+// enclosingTokenStart returns the start offset of the enclosing token.
+// A (-1, non-nil) result indicates "no enclosing token".
+func enclosingTokenStart(fc *parsed, pos protocol.Position) (int, error) {
// pos is the pos-th character. if the cursor is at the beginning
// of the file, pos is 0. That is, we've only seen characters before pos
// 1. pos might be in a Token, return tk.Start
// 2. pos might be after an elided but before a Token, return elided
// 3. return -1 for false
- offset := fc.fromPosition(pos)
- // this could be a binary search, as the tokens are ordered
+ offset, err := fc.mapper.PositionOffset(pos)
+ if err != nil {
+ return 0, err
+ }
+
+ // TODO: opt: this could be a binary search, as the tokens are ordered
for _, tk := range fc.tokens {
if tk.start+len(lbraces) <= offset && offset+len(rbraces) <= tk.end {
- return tk.start
+ return tk.start, nil
}
}
+
for _, x := range fc.elided {
if x+len(lbraces) > offset {
// fc.elided is sorted, and x is the position where a '{{' was replaced
@@ -98,10 +106,10 @@
// If the interval [x,offset] does not contain Left or Right
// then provide completions. (do we need the test for Right?)
if !bytes.Contains(fc.buf[x:offset], lbraces) && !bytes.Contains(fc.buf[x:offset], rbraces) {
- return x
+ return x, nil
}
}
- return -1
+ return -1, fmt.Errorf("no token enclosing %d", pos)
}
var (
@@ -115,7 +123,10 @@
// The error return is always nil.
func (c *completer) complete() (*protocol.CompletionList, error) {
ans := &protocol.CompletionList{IsIncomplete: true, Items: []protocol.CompletionItem{}}
- start := c.p.fromPosition(c.pos)
+ start, err := c.p.mapper.PositionOffset(c.pos)
+ if err != nil {
+ return ans, err
+ }
sofar := c.p.buf[c.offset:start]
if len(sofar) == 0 || sofar[len(sofar)-1] == ' ' || sofar[len(sofar)-1] == '\t' {
return ans, nil
diff --git a/gopls/internal/template/completion_test.go b/gopls/internal/template/completion_test.go
index 8863e05..279864a 100644
--- a/gopls/internal/template/completion_test.go
+++ b/gopls/internal/template/completion_test.go
@@ -19,13 +19,13 @@
type tparse struct {
marked string // ^ shows where to ask for completions. (The user just typed the following character.)
- wanted []string // expected completions
+ wanted []string // expected completions; nil => no enclosing token
}
// Test completions in templates that parse enough (if completion needs symbols)
// Seen characters up to the ^
func TestParsed(t *testing.T) {
- var tests = []tparse{
+ for _, test := range []tparse{
{"{{x}}{{12. xx^", nil}, // https://github.com/golang/go/issues/50430
{`<table class="chroma" data-new-comment-url="{{if $.PageIsPullFiles}}{{$.Issue.HTMLURL}}/files/reviews/new_comment{{else}}{{$.CommitHTML}}/new_comment^{{end}}">`, nil},
{"{{i^f}}", []string{"index", "if"}},
@@ -50,53 +50,56 @@
{"{{`e^", []string{}},
{"{{`No i^", []string{}}, // example of why go/scanner is used
{"{{xavier}}{{12. x^", []string{"xavier"}},
- }
- for _, tx := range tests {
- c := testCompleter(t, tx)
- var v []string
- if c != nil {
- ans, _ := c.complete()
- for _, a := range ans.Items {
- v = append(v, a.Label)
+ } {
+ t.Run("", func(t *testing.T) {
+ var got []string
+ if c := testCompleter(t, test); c != nil {
+ ans, _ := c.complete()
+ for _, a := range ans.Items {
+ got = append(got, a.Label)
+ }
}
- }
- if len(v) != len(tx.wanted) {
- t.Errorf("%q: got %q, wanted %q %d,%d", tx.marked, v, tx.wanted, len(v), len(tx.wanted))
- continue
- }
- sort.Strings(tx.wanted)
- sort.Strings(v)
- for i := 0; i < len(v); i++ {
- if tx.wanted[i] != v[i] {
- t.Errorf("%q at %d: got %v, wanted %v", tx.marked, i, v, tx.wanted)
- break
+ if len(got) != len(test.wanted) {
+ t.Fatalf("%q: got %q, wanted %q %d,%d", test.marked, got, test.wanted, len(got), len(test.wanted))
}
- }
+ sort.Strings(test.wanted)
+ sort.Strings(got)
+ for i := 0; i < len(got); i++ {
+ if test.wanted[i] != got[i] {
+ t.Fatalf("%q at %d: got %v, wanted %v", test.marked, i, got, test.wanted)
+ }
+ }
+ })
}
}
func testCompleter(t *testing.T, tx tparse) *completer {
- t.Helper()
// seen chars up to ^
- col := strings.Index(tx.marked, "^")
+ offset := strings.Index(tx.marked, "^")
buf := strings.Replace(tx.marked, "^", "", 1)
- p := parseBuffer([]byte(buf))
- pos := protocol.Position{Line: 0, Character: uint32(col)}
+ p := parseBuffer("", []byte(buf))
if p.parseErr != nil {
- log.Printf("%q: %v", tx.marked, p.parseErr)
+ t.Logf("%q: %v", tx.marked, p.parseErr)
}
- offset := inTemplate(p, pos)
- if offset == -1 {
- return nil
+ pos, err := p.mapper.OffsetPosition(offset)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ start, err := enclosingTokenStart(p, pos)
+ if err != nil {
+ if start == -1 {
+ return nil // no enclosing token
+ }
+ t.Fatal(err)
}
syms := make(map[string]symbol)
filterSyms(syms, p.symbols)
- c := &completer{
+ return &completer{
p: p,
- pos: protocol.Position{Line: 0, Character: uint32(col)},
- offset: offset + len(lbraces),
+ pos: pos,
+ offset: start + len(lbraces),
ctx: protocol.CompletionContext{TriggerKind: protocol.Invoked},
syms: syms,
}
- return c
}
diff --git a/gopls/internal/template/highlight.go b/gopls/internal/template/highlight.go
index 9e458ff..8a8244d 100644
--- a/gopls/internal/template/highlight.go
+++ b/gopls/internal/template/highlight.go
@@ -19,29 +19,34 @@
if err != nil {
return nil, err
}
- p := parseBuffer(buf)
- pos := p.fromPosition(loc)
- var ans []protocol.DocumentHighlight
+ p := parseBuffer(fh.URI(), buf)
+ pos, err := p.mapper.PositionOffset(loc)
+ if err != nil {
+ return nil, err
+ }
+
if p.parseErr == nil {
for _, s := range p.symbols {
- if s.start <= pos && pos < s.start+s.length {
+ if s.start <= pos && pos < s.start+s.len {
return markSymbols(p, s)
}
}
}
+
// these tokens exist whether or not there was a parse error
// (symbols require a successful parse)
for _, tok := range p.tokens {
if tok.start <= pos && pos < tok.end {
- wordAt := findWordAt(p, pos)
+ wordAt := wordAt(p.buf, pos)
if len(wordAt) > 0 {
return markWordInToken(p, wordAt)
}
}
}
- // find the 'word' at pos, etc: someday
+
+ // TODO: find the 'word' at pos, etc: someday
// until then we get the default action, which doesn't respect word boundaries
- return ans, nil
+ return nil, nil
}
func markSymbols(p *parsed, sym symbol) ([]protocol.DocumentHighlight, error) {
@@ -52,8 +57,12 @@
if s.vardef {
kind = protocol.Write
}
+ rng, err := p.mapper.OffsetRange(s.offsets())
+ if err != nil {
+ return nil, err
+ }
ans = append(ans, protocol.DocumentHighlight{
- Range: p._range(s.start, s.length),
+ Range: rng,
Kind: kind,
})
}
@@ -69,10 +78,14 @@
return nil, fmt.Errorf("%q: unmatchable word (%v)", wordAt, err)
}
for _, tok := range p.tokens {
- got := pat.FindAllIndex(p.buf[tok.start:tok.end], -1)
- for i := range got {
+ matches := pat.FindAllIndex(p.buf[tok.start:tok.end], -1)
+ for _, match := range matches {
+ rng, err := p.mapper.OffsetRange(match[0], match[1])
+ if err != nil {
+ return nil, err
+ }
ans = append(ans, protocol.DocumentHighlight{
- Range: p._range(got[i][0], got[i][1]-got[i][0]),
+ Range: rng,
Kind: protocol.Text,
})
}
@@ -80,18 +93,20 @@
return ans, nil
}
-var wordRe = regexp.MustCompile(`[$]?\w+$`)
-var moreRe = regexp.MustCompile(`^[$]?\w+`)
-
-// findWordAt finds the word the cursor is in (meaning in or just before)
-func findWordAt(p *parsed, pos int) string {
- if pos >= len(p.buf) {
- return "" // can't happen, as we are called with pos < tok.End
+// wordAt returns the word the cursor is in (meaning in or just before)
+func wordAt(buf []byte, pos int) string {
+ if pos >= len(buf) {
+ return ""
}
- after := moreRe.Find(p.buf[pos:])
+ after := moreRe.Find(buf[pos:])
if len(after) == 0 {
return "" // end of the word
}
- got := wordRe.Find(p.buf[:pos+len(after)])
+ got := wordRe.Find(buf[:pos+len(after)])
return string(got)
}
+
+var (
+ wordRe = regexp.MustCompile(`[$]?\w+$`)
+ moreRe = regexp.MustCompile(`^[$]?\w+`)
+)
diff --git a/gopls/internal/template/implementations.go b/gopls/internal/template/implementations.go
index 303ccbb..7c69c01 100644
--- a/gopls/internal/template/implementations.go
+++ b/gopls/internal/template/implementations.go
@@ -36,46 +36,61 @@
// snapshot's template files
buf, err := fh.Content()
if err != nil {
- // Is a Diagnostic with no Range useful? event.Error also?
+ // TODO: Is a Diagnostic with no Range useful? event.Error also?
msg := fmt.Sprintf("failed to read %s (%v)", fh.URI().Path(), err)
- d := cache.Diagnostic{Message: msg, Severity: protocol.SeverityError, URI: fh.URI(),
- Source: cache.TemplateError}
- return []*cache.Diagnostic{&d}
+ return []*cache.Diagnostic{{
+ Message: msg,
+ Severity: protocol.SeverityError,
+ URI: fh.URI(),
+ Source: cache.TemplateError,
+ }}
}
- p := parseBuffer(buf)
+ p := parseBuffer(fh.URI(), buf)
if p.parseErr == nil {
return nil
}
- unknownError := func(msg string) []*cache.Diagnostic {
- s := fmt.Sprintf("malformed template error %q: %s", p.parseErr.Error(), msg)
- d := cache.Diagnostic{
- Message: s, Severity: protocol.SeverityError, Range: p._range(p.nls[0], 1),
- URI: fh.URI(), Source: cache.TemplateError}
- return []*cache.Diagnostic{&d}
+
+ errorf := func(format string, args ...any) []*cache.Diagnostic {
+ msg := fmt.Sprintf("malformed template error %q: %s",
+ p.parseErr.Error(),
+ fmt.Sprintf(format, args))
+ rng, err := p.mapper.OffsetRange(0, 1) // first UTF-16 code
+ if err != nil {
+ rng = protocol.Range{} // start of file
+ }
+ return []*cache.Diagnostic{{
+ Message: msg,
+ Severity: protocol.SeverityError,
+ Range: rng,
+ URI: fh.URI(),
+ Source: cache.TemplateError,
+ }}
}
+
// errors look like `template: :40: unexpected "}" in operand`
// so the string needs to be parsed
matches := errRe.FindStringSubmatch(p.parseErr.Error())
if len(matches) != 3 {
- msg := fmt.Sprintf("expected 3 matches, got %d (%v)", len(matches), matches)
- return unknownError(msg)
+ return errorf("expected 3 matches, got %d (%v)", len(matches), matches)
}
lineno, err := strconv.Atoi(matches[1])
if err != nil {
- msg := fmt.Sprintf("couldn't convert %q to int, %v", matches[1], err)
- return unknownError(msg)
+ return errorf("couldn't convert %q to int, %v", matches[1], err)
}
msg := matches[2]
- d := cache.Diagnostic{Message: msg, Severity: protocol.SeverityError,
- Source: cache.TemplateError}
- start := p.nls[lineno-1]
- if lineno < len(p.nls) {
- size := p.nls[lineno] - start
- d.Range = p._range(start, size)
- } else {
- d.Range = p._range(start, 1)
+
+ // Compute the range for the whole (1-based) line.
+ rng, err := lineRange(p.mapper, lineno)
+ if err != nil {
+ return errorf("invalid position: %v", err)
}
- return []*cache.Diagnostic{&d}
+
+ return []*cache.Diagnostic{{
+ Message: msg,
+ Severity: protocol.SeverityError,
+ Range: rng,
+ Source: cache.TemplateError,
+ }}
}
// Definition finds the definitions of the symbol at loc. It
@@ -91,12 +106,16 @@
ans := []protocol.Location{}
// PJW: this is probably a pattern to abstract
a := parseSet(snapshot.Templates())
- for k, p := range a.files {
+ for _, p := range a.files {
for _, s := range p.symbols {
if !s.vardef || s.name != sym {
continue
}
- ans = append(ans, k.Location(p._range(s.start, s.length)))
+ loc, err := p.mapper.OffsetLocation(s.offsets())
+ if err != nil {
+ return nil, err
+ }
+ ans = append(ans, loc)
}
}
return ans, nil
@@ -104,44 +123,60 @@
func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
sym, p, err := symAtPosition(fh, position)
- if sym == nil || err != nil {
+ if err != nil {
return nil, err
}
- ans := protocol.Hover{Range: p._range(sym.start, sym.length), Contents: protocol.MarkupContent{Kind: protocol.Markdown}}
+
+ var value string
switch sym.kind {
case protocol.Function:
- ans.Contents.Value = fmt.Sprintf("function: %s", sym.name)
+ value = fmt.Sprintf("function: %s", sym.name)
case protocol.Variable:
- ans.Contents.Value = fmt.Sprintf("variable: %s", sym.name)
+ value = fmt.Sprintf("variable: %s", sym.name)
case protocol.Constant:
- ans.Contents.Value = fmt.Sprintf("constant %s", sym.name)
+ value = fmt.Sprintf("constant %s", sym.name)
case protocol.Method: // field or method
- ans.Contents.Value = fmt.Sprintf("%s: field or method", sym.name)
+ value = fmt.Sprintf("%s: field or method", sym.name)
case protocol.Package: // template use, template def (PJW: do we want two?)
- ans.Contents.Value = fmt.Sprintf("template %s\n(add definition)", sym.name)
+ value = fmt.Sprintf("template %s\n(add definition)", sym.name)
case protocol.Namespace:
- ans.Contents.Value = fmt.Sprintf("template %s defined", sym.name)
+ value = fmt.Sprintf("template %s defined", sym.name)
case protocol.Number:
- ans.Contents.Value = "number"
+ value = "number"
case protocol.String:
- ans.Contents.Value = "string"
+ value = "string"
case protocol.Boolean:
- ans.Contents.Value = "boolean"
+ value = "boolean"
default:
- ans.Contents.Value = fmt.Sprintf("oops, sym=%#v", sym)
+ value = fmt.Sprintf("oops, sym=%#v", sym)
}
- return &ans, nil
+
+ rng, err := p.mapper.OffsetRange(sym.offsets())
+ if err != nil {
+ return nil, err
+ }
+
+ return &protocol.Hover{
+ Range: rng,
+ Contents: protocol.MarkupContent{
+ Kind: protocol.Markdown,
+ Value: value,
+ },
+ }, nil
}
func References(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, params *protocol.ReferenceParams) ([]protocol.Location, error) {
sym, _, err := symAtPosition(fh, params.Position)
- if sym == nil || err != nil || sym.name == "" {
+ if err != nil {
return nil, err
}
+ if sym.name == "" {
+ return nil, fmt.Errorf("no symbol at position")
+ }
ans := []protocol.Location{}
a := parseSet(snapshot.Templates())
- for k, p := range a.files {
+ for _, p := range a.files {
for _, s := range p.symbols {
if s.name != sym.name {
continue
@@ -149,10 +184,14 @@
if s.vardef && !params.Context.IncludeDeclaration {
continue
}
- ans = append(ans, k.Location(p._range(s.start, s.length)))
+ loc, err := p.mapper.OffsetLocation(s.offsets())
+ if err != nil {
+ return nil, err
+ }
+ ans = append(ans, loc)
}
}
- // do these need to be sorted? (a.files is a map)
+ // TODO: do these need to be sorted? (a.files is a map)
return ans, nil
}
@@ -165,47 +204,54 @@
if err != nil {
return nil, err
}
- p := parseBuffer(buf)
+ p := parseBuffer(fh.URI(), buf)
var items []semtok.Token
- add := func(line, start, len uint32) {
- if len == 0 {
- return // vscode doesn't like 0-length Tokens
- }
- // TODO(adonovan): don't ignore the rng restriction, if any.
- items = append(items, semtok.Token{
- Line: line,
- Start: start,
- Len: len,
- Type: semtok.TokMacro,
- })
- }
-
for _, t := range p.tokens {
- if t.multiline {
- la, ca := p.lineCol(t.start)
- lb, cb := p.lineCol(t.end)
- add(la, ca, p.runeCount(la, ca, 0))
- for l := la + 1; l < lb; l++ {
- add(l, 0, p.runeCount(l, 0, 0))
- }
- add(lb, 0, p.runeCount(lb, 0, cb))
- continue
+ if t.start == t.end {
+ continue // vscode doesn't like 0-length tokens
}
- sz, err := p.tokenSize(t)
+ pos, err := p.mapper.OffsetPosition(t.start)
if err != nil {
return nil, err
}
- line, col := p.lineCol(t.start)
- add(line, col, uint32(sz))
+ // TODO(adonovan): don't ignore the rng restriction, if any.
+ items = append(items, semtok.Token{
+ Line: pos.Line,
+ Start: pos.Character,
+ Len: uint32(protocol.UTF16Len(p.buf[t.start:t.end])),
+ Type: semtok.TokMacro,
+ })
}
- ans := &protocol.SemanticTokens{
+ return &protocol.SemanticTokens{
Data: semtok.Encode(items, nil, nil),
// for small cache, some day. for now, the LSP client ignores this
// (that is, when the LSP client starts returning these, we can cache)
ResultID: fmt.Sprintf("%v", time.Now()),
- }
- return ans, nil
+ }, nil
}
-// still need to do rename, etc
+// TODO: still need to do rename, etc
+
+func symAtPosition(fh file.Handle, posn protocol.Position) (*symbol, *parsed, error) {
+ buf, err := fh.Content()
+ if err != nil {
+ return nil, nil, err
+ }
+ p := parseBuffer(fh.URI(), buf)
+ offset, err := p.mapper.PositionOffset(posn)
+ if err != nil {
+ return nil, nil, err
+ }
+ var syms []symbol
+ for _, s := range p.symbols {
+ if s.start <= offset && offset < s.start+s.len {
+ syms = append(syms, s)
+ }
+ }
+ if len(syms) == 0 {
+ return nil, p, fmt.Errorf("no symbol found")
+ }
+ sym := syms[0]
+ return &sym, p, nil
+}
diff --git a/gopls/internal/template/parse.go b/gopls/internal/template/parse.go
index 5cb825c..2050b32 100644
--- a/gopls/internal/template/parse.go
+++ b/gopls/internal/template/parse.go
@@ -10,20 +10,16 @@
import (
"bytes"
- "context"
"fmt"
"io"
"log"
"regexp"
- "runtime"
"sort"
"text/template"
"text/template/parse"
- "unicode/utf8"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
- "golang.org/x/tools/internal/event"
)
var (
@@ -32,11 +28,11 @@
)
type parsed struct {
- buf []byte //contents
- lines [][]byte // needed?, other than for debugging?
- elided []int // offsets where Left was replaced by blanks
+ buf []byte // contents
+ mapper *protocol.Mapper
+ elided []int // offsets where lbraces was replaced by blanks
- // tokens are matched Left-Right pairs, computed before trying to parse
+ // tokens are matched lbraces-rbraces pairs, computed before trying to parse
tokens []token
// result of parsing
@@ -44,19 +40,11 @@
parseErr error
symbols []symbol
stack []parse.Node // used while computing symbols
-
- // for mapping from offsets in buf to LSP coordinates
- // See FromPosition() and LineCol()
- nls []int // offset of newlines before each line (nls[0]==-1)
- lastnl int // last line seen
- check int // used to decide whether to use lastnl or search through nls
- nonASCII bool // are there any non-ascii runes in buf?
}
-// token is a single {{...}}. More precisely, Left...Right
+// A token is a single {{...}}.
type token struct {
- start, end int // offset from start of template
- multiline bool
+ start, end int // 0-based byte offset from start of template
}
// set contains the Parse of all the template files
@@ -70,43 +58,27 @@
// TODO(adonovan): why doesn't parseSet return an error?
func parseSet(tmpls map[protocol.DocumentURI]file.Handle) *set {
all := make(map[protocol.DocumentURI]*parsed)
- for k, v := range tmpls {
- buf, err := v.Content()
- if err != nil { // PJW: decide what to do with these errors
- log.Printf("failed to read %s (%v)", v.URI().Path(), err)
+ for uri, fh := range tmpls {
+ buf, err := fh.Content()
+ if err != nil {
+ // TODO(pjw): decide what to do with these errors
+ log.Printf("failed to read %s (%v)", fh.URI().Path(), err)
continue
}
- all[k] = parseBuffer(buf)
+ all[uri] = parseBuffer(uri, buf)
}
return &set{files: all}
}
-func parseBuffer(buf []byte) *parsed {
+func parseBuffer(uri protocol.DocumentURI, buf []byte) *parsed {
ans := &parsed{
- buf: buf,
- check: -1,
- nls: []int{-1},
+ buf: buf,
+ mapper: protocol.NewMapper(uri, buf),
}
if len(buf) == 0 {
return ans
}
- // how to compute allAscii...
- for _, b := range buf {
- if b >= utf8.RuneSelf {
- ans.nonASCII = true
- break
- }
- }
- if buf[len(buf)-1] != '\n' {
- ans.buf = append(buf, '\n')
- }
- for i, p := range ans.buf {
- if p == '\n' {
- ans.nls = append(ans.nls, i)
- }
- }
ans.setTokens() // ans.buf may be a new []byte
- ans.lines = bytes.Split(ans.buf, []byte{'\n'})
t, err := template.New("").Parse(string(ans.buf))
if err != nil {
funcs := make(template.FuncMap)
@@ -132,7 +104,7 @@
if t.Name() != "" {
// defining a template. The pos is just after {{define...}} (or {{block...}}?)
at, sz := ans.findLiteralBefore(int(t.Root.Pos))
- s := symbol{start: at, length: sz, name: t.Name(), kind: protocol.Namespace, vardef: true}
+ s := symbol{start: at, len: sz, name: t.Name(), kind: protocol.Namespace, vardef: true}
ans.symbols = append(ans.symbols, s)
}
}
@@ -151,8 +123,7 @@
}
// findLiteralBefore locates the first preceding string literal
-// returning its position and length in buf
-// or returns -1 if there is none.
+// returning its offset and length in buf or (-1, 0) if there is none.
// Assume double-quoted string rather than backquoted string for now.
func (p *parsed) findLiteralBefore(pos int) (int, int) {
left, right := -1, -1
@@ -211,15 +182,11 @@
}
if bytes.HasPrefix(p.buf[n:], rbraces) {
right := n + len(rbraces)
- tok := token{
- start: left,
- end: right,
- multiline: bytes.Contains(p.buf[left:right], []byte{'\n'}),
- }
+ tok := token{start: left, end: right}
p.tokens = append(p.tokens, tok)
state = Start
}
- // If we see (unquoted) Left then the original left is probably the user
+ // If we see (unquoted) lbraces then the original left is probably the user
// typing. Suppress the original left
if bytes.HasPrefix(p.buf[n:], lbraces) {
p.elideAt(left)
@@ -236,7 +203,7 @@
}
// this error occurs after typing {{ at the end of the file
if state != Start {
- // Unclosed Left. remove the Left at left
+ // Unclosed lbraces. remove the lbraces at left
p.elideAt(left)
}
}
@@ -245,11 +212,9 @@
if p.elided == nil {
// p.buf is the same buffer that v.Read() returns, so copy it.
// (otherwise the next time it's parsed, elided information is lost)
- b := make([]byte, len(p.buf))
- copy(b, p.buf)
- p.buf = b
+ p.buf = bytes.Clone(p.buf)
}
- for i := 0; i < len(lbraces); i++ {
+ for i := range lbraces {
p.buf[left+i] = ' '
}
p.elided = append(p.elided, left)
@@ -264,140 +229,24 @@
return backSlashes%2 == 1
}
-// TODO(adonovan): the next 100 lines could perhaps replaced by use of protocol.Mapper.
+// lineRange returns the range for the entire specified (1-based) line.
+func lineRange(m *protocol.Mapper, line int) (protocol.Range, error) {
+ posn := protocol.Position{Line: uint32(line - 1)}
-func (p *parsed) utf16len(buf []byte) int {
- cnt := 0
- if !p.nonASCII {
- return len(buf)
- }
- // we need a utf16len(rune), but we don't have it
- for _, r := range string(buf) {
- cnt++
- if r >= 1<<16 {
- cnt++
- }
- }
- return cnt
-}
-
-func (p *parsed) tokenSize(t token) (int, error) {
- if t.multiline {
- return -1, fmt.Errorf("TokenSize called with Multiline token %#v", t)
- }
- ans := p.utf16len(p.buf[t.start:t.end])
- return ans, nil
-}
-
-// runeCount counts runes in line l, from col s to e
-// (e==0 for end of line. called only for multiline tokens)
-func (p *parsed) runeCount(l, s, e uint32) uint32 {
- start := p.nls[l] + 1 + int(s)
- end := p.nls[l] + 1 + int(e)
- if e == 0 || end > p.nls[l+1] {
- end = p.nls[l+1]
- }
- return uint32(utf8.RuneCount(p.buf[start:end]))
-}
-
-// lineCol converts from a 0-based byte offset to 0-based line, col. col in runes
-func (p *parsed) lineCol(x int) (uint32, uint32) {
- if x < p.check {
- p.lastnl = 0
- }
- p.check = x
- for i := p.lastnl; i < len(p.nls); i++ {
- if p.nls[i] <= x {
- continue
- }
- p.lastnl = i
- var count int
- if i > 0 && x == p.nls[i-1] { // \n
- count = 0
- } else {
- count = p.utf16len(p.buf[p.nls[i-1]+1 : x])
- }
- return uint32(i - 1), uint32(count)
- }
- if x == len(p.buf)-1 { // trailing \n
- return uint32(len(p.nls) - 1), 0
- }
- // shouldn't happen
- for i := 1; i < 4; i++ {
- _, f, l, ok := runtime.Caller(i)
- if !ok {
- break
- }
- log.Printf("%d: %s:%d", i, f, l)
- }
-
- msg := fmt.Errorf("LineCol off the end, %d of %d, nls=%v, %q", x, len(p.buf), p.nls, p.buf[x:])
- event.Error(context.Background(), "internal error", msg)
- return 0, 0
-}
-
-// position produces a protocol.position from an offset in the template
-func (p *parsed) position(pos int) protocol.Position {
- line, col := p.lineCol(pos)
- return protocol.Position{Line: line, Character: col}
-}
-
-func (p *parsed) _range(x, length int) protocol.Range {
- line, col := p.lineCol(x)
- ans := protocol.Range{
- Start: protocol.Position{Line: line, Character: col},
- End: protocol.Position{Line: line, Character: col + uint32(length)},
- }
- return ans
-}
-
-// fromPosition translates a protocol.Position into an offset into the template
-func (p *parsed) fromPosition(x protocol.Position) int {
- l, c := int(x.Line), int(x.Character)
- if l >= len(p.nls) || p.nls[l]+1 >= len(p.buf) {
- // paranoia to avoid panic. return the largest offset
- return len(p.buf)
- }
- line := p.buf[p.nls[l]+1:]
- cnt := 0
- for w := range string(line) {
- if cnt >= c {
- return w + p.nls[l] + 1
- }
- cnt++
- }
- // do we get here? NO
- pos := int(x.Character) + p.nls[int(x.Line)] + 1
- event.Error(context.Background(), "internal error", fmt.Errorf("surprise %#v", x))
- return pos
-}
-
-func symAtPosition(fh file.Handle, loc protocol.Position) (*symbol, *parsed, error) {
- buf, err := fh.Content()
+ // start of line
+ start, err := m.PositionOffset(posn)
if err != nil {
- return nil, nil, err
+ return protocol.Range{}, err
}
- p := parseBuffer(buf)
- pos := p.fromPosition(loc)
- syms := p.symsAtPos(pos)
- if len(syms) == 0 {
- return nil, p, fmt.Errorf("no symbol found")
- }
- if len(syms) > 1 {
- log.Printf("Hover: %d syms, not 1 %v", len(syms), syms)
- }
- sym := syms[0]
- return &sym, p, nil
-}
-func (p *parsed) symsAtPos(pos int) []symbol {
- ans := []symbol{}
- for _, s := range p.symbols {
- if s.start <= pos && pos < s.start+s.length {
- ans = append(ans, s)
- }
+ // end of line (or file)
+ posn.Line++
+ end := len(m.Content) // EOF
+ if offset, err := m.PositionOffset(posn); err != nil {
+ end = offset - len("\n")
}
- return ans
+
+ return m.OffsetRange(start, end)
}
// -- debugging --
@@ -417,8 +266,12 @@
return
}
at := func(pos parse.Pos) string {
- line, col := wr.p.lineCol(int(pos))
- return fmt.Sprintf("(%d)%v:%v", pos, line, col)
+ offset := int(pos)
+ posn, err := wr.p.mapper.OffsetPosition(offset)
+ if err != nil {
+ return fmt.Sprintf("<bad pos %d: %v>", pos, err)
+ }
+ return fmt.Sprintf("(%d)%v:%v", pos, posn.Line, posn.Character)
}
switch x := n.(type) {
case *parse.ActionNode:
diff --git a/gopls/internal/template/parse_test.go b/gopls/internal/template/parse_test.go
index 507e39e..dc023f3 100644
--- a/gopls/internal/template/parse_test.go
+++ b/gopls/internal/template/parse_test.go
@@ -4,51 +4,64 @@
package template
-import (
- "strings"
- "testing"
-)
+import "testing"
-type datum struct {
- buf string
- cnt int
- syms []string // the symbols in the parse of buf
-}
-
-var tmpl = []datum{{`
+func TestSymbols(t *testing.T) {
+ for i, test := range []struct {
+ buf string
+ wantNamed int // expected number of named templates
+ syms []string // expected symbols (start, len, name, kind, def?)
+ }{
+ {`
{{if (foo .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}}
{{$A.X 12}}
{{foo (.X.Y) 23 ($A.Zü)}}
-{{end}}`, 1, []string{"{7,3,foo,Function,false}", "{12,1,X,Method,false}",
- "{14,1,Y,Method,false}", "{21,2,$A,Variable,true}", "{26,2,,String,false}",
- "{35,1,Z,Method,false}", "{38,2,$A,Variable,false}",
- "{53,2,$A,Variable,false}", "{56,1,X,Method,false}", "{57,2,,Number,false}",
- "{64,3,foo,Function,false}", "{70,1,X,Method,false}",
- "{72,1,Y,Method,false}", "{75,2,,Number,false}", "{80,2,$A,Variable,false}",
- "{83,2,Zü,Method,false}", "{94,3,,Constant,false}"}},
-
- {`{{define "zzz"}}{{.}}{{end}}
-{{template "zzz"}}`, 2, []string{"{10,3,zzz,Namespace,true}", "{18,1,dot,Variable,false}",
- "{41,3,zzz,Package,false}"}},
-
- {`{{block "aaa" foo}}b{{end}}`, 2, []string{"{9,3,aaa,Namespace,true}",
- "{9,3,aaa,Package,false}", "{14,3,foo,Function,false}", "{19,1,,Constant,false}"}},
- {"", 0, nil},
-}
-
-func TestSymbols(t *testing.T) {
- for i, x := range tmpl {
- got := parseBuffer([]byte(x.buf))
+{{end}}`, 1, []string{
+ "{7,3,foo,Function,false}",
+ "{12,1,X,Method,false}",
+ "{14,1,Y,Method,false}",
+ "{21,2,$A,Variable,true}",
+ "{26,4,,String,false}",
+ "{35,1,Z,Method,false}",
+ "{38,2,$A,Variable,false}",
+ "{53,2,$A,Variable,false}",
+ "{56,1,X,Method,false}",
+ "{57,2,,Number,false}",
+ "{64,3,foo,Function,false}",
+ "{70,1,X,Method,false}",
+ "{72,1,Y,Method,false}",
+ "{75,2,,Number,false}",
+ "{80,2,$A,Variable,false}",
+ "{83,3,Zü,Method,false}",
+ "{94,3,,Constant,false}",
+ }},
+ {`{{define "zzz"}}{{.}}{{end}}
+{{template "zzz"}}`, 2, []string{
+ "{10,3,zzz,Namespace,true}",
+ "{18,1,dot,Variable,false}",
+ "{41,3,zzz,Package,false}",
+ }},
+ {`{{block "aaa" foo}}b{{end}}`, 2, []string{
+ "{9,3,aaa,Namespace,true}",
+ "{9,3,aaa,Package,false}",
+ "{14,3,foo,Function,false}",
+ "{19,1,,Constant,false}",
+ }},
+ {"", 0, nil},
+ {`{{/* this is
+a comment */}}`, 1, nil}, // https://go.dev/issue/74635
+ } {
+ got := parseBuffer("", []byte(test.buf))
if got.parseErr != nil {
- t.Errorf("error:%v", got.parseErr)
+ t.Error(got.parseErr)
continue
}
- if len(got.named) != x.cnt {
- t.Errorf("%d: got %d, expected %d", i, len(got.named), x.cnt)
+ if len(got.named) != test.wantNamed {
+ t.Errorf("%d: got %d, expected %d", i, len(got.named), test.wantNamed)
}
for n, s := range got.symbols {
- if s.String() != x.syms[n] {
- t.Errorf("%d: got %s, expected %s", i, s.String(), x.syms[n])
+ if s.String() != test.syms[n] {
+ t.Errorf("%d: got %s, expected %s", i, s.String(), test.syms[n])
}
}
}
@@ -58,174 +71,29 @@
want := []string{"", "", "$A", "$A", "", "", "", "", "", "",
"", "", "", "if", "if", "", "$A", "$A", "", "",
"B", "", "", "end", "end", "end", "", "", ""}
- p := parseBuffer([]byte("{{$A := .}}{{if $A}}B{{end}}"))
- for i := 0; i < len(p.buf); i++ {
- got := findWordAt(p, i)
+ buf := []byte("{{$A := .}}{{if $A}}B{{end}}")
+ for i := 0; i < len(buf); i++ {
+ got := wordAt(buf, i)
if got != want[i] {
t.Errorf("for %d, got %q, wanted %q", i, got, want[i])
}
}
}
-func TestNLS(t *testing.T) {
- buf := `{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}}
- {{$A.X 12}}
- {{foo (.X.Y) 23 ($A.Z)}}
- {{end}}
- `
- p := parseBuffer([]byte(buf))
- if p.parseErr != nil {
- t.Fatal(p.parseErr)
- }
- // line 0 doesn't have a \n in front of it
- for i := 1; i < len(p.nls)-1; i++ {
- if buf[p.nls[i]] != '\n' {
- t.Errorf("line %d got %c", i, buf[p.nls[i]])
- }
- }
- // fake line at end of file
- if p.nls[len(p.nls)-1] != len(buf) {
- t.Errorf("got %d expected %d", p.nls[len(p.nls)-1], len(buf))
- }
-}
-
-func TestLineCol(t *testing.T) {
- buf := `{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}}
- {{$A.X 12}}
- {{foo (.X.Y) 23 ($A.Z)}}
- {{end}}`
- if false {
- t.Error(buf)
- }
- for n, cx := range tmpl {
- buf := cx.buf
- p := parseBuffer([]byte(buf))
- if p.parseErr != nil {
- t.Fatal(p.parseErr)
- }
- type loc struct {
- offset int
- l, c uint32
- }
- saved := []loc{}
- // forwards
- var lastl, lastc uint32
- for offset := range buf {
- l, c := p.lineCol(offset)
- saved = append(saved, loc{offset, l, c})
- if l > lastl {
- lastl = l
- if c != 0 {
- t.Errorf("line %d, got %d instead of 0", l, c)
- }
- }
- if c > lastc {
- lastc = c
- }
- }
- lines := strings.Split(buf, "\n")
- mxlen := -1
- for _, l := range lines {
- if len(l) > mxlen {
- mxlen = len(l)
- }
- }
- if int(lastl) != len(lines)-1 && int(lastc) != mxlen {
- // lastl is 0 if there is only 1 line(?)
- t.Errorf("expected %d, %d, got %d, %d for case %d", len(lines)-1, mxlen, lastl, lastc, n)
- }
- // backwards
- for j := len(saved) - 1; j >= 0; j-- {
- s := saved[j]
- xl, xc := p.lineCol(s.offset)
- if xl != s.l || xc != s.c {
- t.Errorf("at offset %d(%d), got (%d,%d), expected (%d,%d)", s.offset, j, xl, xc, s.l, s.c)
- }
- }
- }
-}
-
-func TestLineColNL(t *testing.T) {
- buf := "\n\n\n\n\n"
- p := parseBuffer([]byte(buf))
- if p.parseErr != nil {
- t.Fatal(p.parseErr)
- }
- for i := 0; i < len(buf); i++ {
- l, c := p.lineCol(i)
- if c != 0 || int(l) != i+1 {
- t.Errorf("got (%d,%d), expected (%d,0)", l, c, i)
- }
- }
-}
-
-func TestPos(t *testing.T) {
- buf := `
- {{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}}
- {{$A.X 12}}
- {{foo (.X.Y) 23 ($A.Z)}}
- {{end}}`
- p := parseBuffer([]byte(buf))
- if p.parseErr != nil {
- t.Fatal(p.parseErr)
- }
- for pos, r := range buf {
- if r == '\n' {
- continue
- }
- x := p.position(pos)
- n := p.fromPosition(x)
- if n != pos {
- // once it's wrong, it will be wrong forever
- t.Fatalf("at pos %d (rune %c) got %d {%#v]", pos, r, n, x)
- }
-
- }
-}
-
-func TestLen(t *testing.T) {
- data := []struct {
- cnt int
- v string
- }{{1, "a"}, {1, "膈"}, {4, "😆🥸"}, {7, "3😀4567"}}
- p := &parsed{nonASCII: true}
- for _, d := range data {
- got := p.utf16len([]byte(d.v))
- if got != d.cnt {
- t.Errorf("%v, got %d wanted %d", d, got, d.cnt)
- }
- }
-}
-
-func TestUtf16(t *testing.T) {
- buf := `
- {{if (foÜx .X.Y)}}😀{{$A := "hi"}}{{.Z $A}}{{else}}
- {{$A.X 12}}
- {{foo (.X.Y) 23 ($A.Z)}}
- {{end}}`
- p := parseBuffer([]byte(buf))
- if p.nonASCII == false {
- t.Error("expected nonASCII to be true")
- }
-}
-
-type ttest struct {
- tmpl string
- tokCnt int
- elidedCnt int8
-}
-
func TestQuotes(t *testing.T) {
- tsts := []ttest{
+ for _, s := range []struct {
+ tmpl string
+ tokCnt int
+ elidedCnt int8
+ }{
{"{{- /*comment*/ -}}", 1, 0},
{"{{/*`\ncomment\n`*/}}", 1, 0},
//{"{{foo\nbar}}\n", 1, 0}, // this action spanning lines parses in 1.16
{"{{\"{{foo}}{{\"}}", 1, 0},
{"{{\n{{- when}}", 1, 1}, // corrected
{"{{{{if .}}xx{{\n{{end}}", 2, 2}, // corrected
- }
- for _, s := range tsts {
- p := parseBuffer([]byte(s.tmpl))
+ } {
+ p := parseBuffer("", []byte(s.tmpl))
if len(p.tokens) != s.tokCnt {
t.Errorf("%q: got %d tokens, expected %d", s, len(p.tokens), s.tokCnt)
}
diff --git a/gopls/internal/template/symbols.go b/gopls/internal/template/symbols.go
index 72add91..00745c2 100644
--- a/gopls/internal/template/symbols.go
+++ b/gopls/internal/template/symbols.go
@@ -9,7 +9,6 @@
"context"
"fmt"
"text/template/parse"
- "unicode/utf8"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
@@ -19,8 +18,8 @@
// in local coordinates, to be translated to protocol.DocumentSymbol
type symbol struct {
- start int // for sorting
- length int // in runes (unicode code points)
+ start int // 0-based byte offset, for sorting
+ len int // of source, in bytes
name string
kind protocol.SymbolKind
vardef bool // is this a variable definition?
@@ -28,8 +27,12 @@
// no children yet, and selection range is the same as range
}
+func (s symbol) offsets() (start, end int) {
+ return s.start, s.start + s.len
+}
+
func (s symbol) String() string {
- return fmt.Sprintf("{%d,%d,%s,%s,%v}", s.start, s.length, s.name, s.kind, s.vardef)
+ return fmt.Sprintf("{%d,%d,%s,%s,%v}", s.start, s.len, s.name, s.kind, s.vardef)
}
// for FieldNode or VariableNode (or ChainNode?)
@@ -80,7 +83,7 @@
if f[0] == '$' {
kind = protocol.Variable
}
- sym := symbol{name: f, kind: kind, start: at, length: utf8.RuneCountInString(f)}
+ sym := symbol{name: f, kind: kind, start: at, len: len(f)}
if kind == protocol.Variable && len(p.stack) > 1 {
if pipe, ok := p.stack[len(p.stack)-2].(*parse.PipeNode); ok {
for _, y := range pipe.Decl {
@@ -118,7 +121,7 @@
case *parse.BoolNode:
// need to compute the length from the value
msg := fmt.Sprintf("%v", x.True)
- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(msg), kind: protocol.Boolean})
+ p.symbols = append(p.symbols, symbol{start: int(x.Pos), len: len(msg), kind: protocol.Boolean})
case *parse.BranchNode:
nxt(x.Pipe)
nxt(x.List)
@@ -133,13 +136,12 @@
//case *parse.CommentNode: // go 1.16
// log.Printf("implement %d", x.Type())
case *parse.DotNode:
- sym := symbol{name: "dot", kind: protocol.Variable, start: int(x.Pos), length: 1}
+ sym := symbol{name: "dot", kind: protocol.Variable, start: int(x.Pos), len: 1}
p.symbols = append(p.symbols, sym)
case *parse.FieldNode:
p.symbols = append(p.symbols, p.fields(x.Ident, x)...)
case *parse.IdentifierNode:
- sym := symbol{name: x.Ident, kind: protocol.Function, start: int(x.Pos),
- length: utf8.RuneCountInString(x.Ident)}
+ sym := symbol{name: x.Ident, kind: protocol.Function, start: int(x.Pos), len: len(x.Ident)}
p.symbols = append(p.symbols, sym)
case *parse.IfNode:
nxt(&x.BranchNode)
@@ -150,11 +152,11 @@
}
}
case *parse.NilNode:
- sym := symbol{name: "nil", kind: protocol.Constant, start: int(x.Pos), length: 3}
+ sym := symbol{name: "nil", kind: protocol.Constant, start: int(x.Pos), len: 3}
p.symbols = append(p.symbols, sym)
case *parse.NumberNode:
// no name; ascii
- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(x.Text), kind: protocol.Number})
+ p.symbols = append(p.symbols, symbol{start: int(x.Pos), len: len(x.Text), kind: protocol.Number})
case *parse.PipeNode:
if x == nil { // {{template "foo"}}
return
@@ -169,25 +171,23 @@
nxt(&x.BranchNode)
case *parse.StringNode:
// no name
- sz := utf8.RuneCountInString(x.Text)
- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.String})
- case *parse.TemplateNode: // invoking a template
- // x.Pos points to the quote before the name
- p.symbols = append(p.symbols, symbol{name: x.Name, kind: protocol.Package, start: int(x.Pos) + 1,
- length: utf8.RuneCountInString(x.Name)})
+ p.symbols = append(p.symbols, symbol{start: int(x.Pos), len: len(x.Quoted), kind: protocol.String})
+ case *parse.TemplateNode:
+ // invoking a template, e.g. {{define "foo"}}
+ // x.Pos is the index of "foo".
+ // The logic below assumes that the literal is trivial.
+ p.symbols = append(p.symbols, symbol{name: x.Name, kind: protocol.Package, start: int(x.Pos) + len(`"`), len: len(x.Name)})
nxt(x.Pipe)
case *parse.TextNode:
if len(x.Text) == 1 && x.Text[0] == '\n' {
break
}
// nothing to report, but build one for hover
- sz := utf8.RuneCount(x.Text)
- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.Constant})
+ p.symbols = append(p.symbols, symbol{start: int(x.Pos), len: len(x.Text), kind: protocol.Constant})
case *parse.VariableNode:
p.symbols = append(p.symbols, p.fields(x.Ident, x)...)
case *parse.WithNode:
nxt(&x.BranchNode)
-
}
pop()
}
@@ -199,33 +199,35 @@
if err != nil {
return nil, err
}
- p := parseBuffer(buf)
+ p := parseBuffer(fh.URI(), buf)
if p.parseErr != nil {
return nil, p.parseErr
}
var ans []protocol.DocumentSymbol
- for _, s := range p.symbols {
- if s.kind == protocol.Constant {
+ for _, sym := range p.symbols {
+ if sym.kind == protocol.Constant {
continue
}
- d := kindStr(s.kind)
- if d == "Namespace" {
- d = "Template"
+ detail := kindStr(sym.kind)
+ if detail == "Namespace" {
+ detail = "Template"
}
- if s.vardef {
- d += "(def)"
+ if sym.vardef {
+ detail += "(def)"
} else {
- d += "(use)"
+ detail += "(use)"
}
- r := p._range(s.start, s.length)
- y := protocol.DocumentSymbol{
- Name: s.name,
- Detail: d,
- Kind: s.kind,
- Range: r,
- SelectionRange: r, // or should this be the entire {{...}}?
+ rng, err := p.mapper.OffsetRange(sym.offsets())
+ if err != nil {
+ return nil, err
}
- ans = append(ans, y)
+ ans = append(ans, protocol.DocumentSymbol{
+ Name: sym.name,
+ Detail: detail,
+ Kind: sym.kind,
+ Range: rng,
+ SelectionRange: rng, // or should this be the entire {{...}}?
+ })
}
return ans, nil
}
diff --git a/gopls/internal/test/integration/template/template_test.go b/gopls/internal/test/integration/template/template_test.go
index 22eff5e..0cf3592 100644
--- a/gopls/internal/test/integration/template/template_test.go
+++ b/gopls/internal/test/integration/template/template_test.go
@@ -53,8 +53,7 @@
}
func TestMultilineTokensAgain(t *testing.T) {
- t.Skip("skipping due to go.dev/issue/74635")
-
+ // Regression tests for a crash; see go.dev/issue/74635.
const files = `
-- go.mod --
module mod.com